From ecf295f6ef2b83c5e51cc74adf833fd8e18b6cfb Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Tue, 14 Jun 2011 09:45:26 -0400
Subject: [PATCH] - Added searching in all addressbook sources (global-search) - Added addressbook source selection in contacts import

---
 program/steps/addressbook/list.inc    |   62 +++
 CHANGELOG                             |    2 
 program/steps/addressbook/edit.inc    |   61 ++-
 program/steps/addressbook/mailto.inc  |   39 +-
 program/steps/addressbook/save.inc    |   18 
 program/steps/addressbook/func.inc    |  209 ++++++++---
 program/steps/addressbook/delete.inc  |  136 ++++++-
 config/main.inc.php.dist              |    1 
 program/steps/addressbook/groups.inc  |    5 
 program/steps/addressbook/import.inc  |   65 ++-
 program/localization/en_US/labels.inc |    1 
 program/steps/addressbook/show.inc    |   22 
 program/localization/pl_PL/labels.inc |    1 
 program/steps/addressbook/export.inc  |   85 +++-
 program/js/app.js                     |   87 +++-
 program/steps/addressbook/copy.inc    |  116 +++--
 program/steps/addressbook/search.inc  |  119 +++++-
 17 files changed, 739 insertions(+), 290 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 00556dd..19a73b2 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,8 @@
 CHANGELOG Roundcube Webmail
 ===========================
 
+- Added searching in all addressbook sources
+- Added addressbook source selection in contacts import
 - Implement LDAPv3 Virtual List View (VLV) for paged results listing
 - Use 'address_template' config option when adding a new address block (#1487944)
 - Added addressbook advanced search
diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist
index 44e80c0..7eac777 100644
--- a/config/main.inc.php.dist
+++ b/config/main.inc.php.dist
@@ -450,6 +450,7 @@
 
 // In order to enable public ldap search, configure an array like the Verisign
 // example further below. if you would like to test, simply uncomment the example.
+// Array key must contain only safe characters, ie. a-zA-Z0-9_
 $rcmail_config['ldap_public'] = array();
 
 // If you are going to use LDAP for individual address books, you will need to 
diff --git a/program/js/app.js b/program/js/app.js
index 88297e9..2c8fad6 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -336,7 +336,7 @@
         if (this.contact_list && this.contact_list.rowcount > 0)
           this.enable_command('export', true);
 
-        this.enable_command('add', 'import', !this.env.readonly);
+        this.enable_command('add', 'import', this.env.writable_source);
         this.enable_command('list', 'listgroup', 'advanced-search', true);
         break;
 
@@ -529,12 +529,12 @@
           if (this.env.trash_mailbox)
             this.set_alttext('delete', this.env.mailbox != this.env.trash_mailbox ? 'movemessagetotrash' : 'deletemessage');
         }
-        else if (this.task=='addressbook') {
+        else if (this.task == 'addressbook') {
           if (!this.env.search_request || (props != this.env.source))
             this.reset_qsearch();
 
           this.list_contacts(props);
-          this.enable_command('add', 'import', (this.env.address_sources && !this.env.address_sources[this.env.source].readonly));
+          this.enable_command('add', 'import', this.env.writable_source);
         }
         break;
 
@@ -988,8 +988,14 @@
 
         if (s && this.env.mailbox)
           this.list_mailbox(this.env.mailbox);
-        else if (s && this.task == 'addressbook')
+        else if (s && this.task == 'addressbook') {
+          if (this.env.source == '') {
+            for (var n in this.env.address_sources) break;
+            this.env.source = n;
+            this.env.group = '';
+          }
           this.list_contacts(this.env.source, this.env.group);
+        }
         break;
 
       case 'listgroup':
@@ -3651,15 +3657,34 @@
     if (this.preview_timer)
       clearTimeout(this.preview_timer);
 
-    var id, frame, ref = this;
+    var n, id, sid, ref = this, writable = false,
+      source = this.env.source ? this.env.address_sources[this.env.source] : null;
+
     if (id = list.get_single_selection())
       this.preview_timer = window.setTimeout(function(){ ref.load_contact(id, 'show'); }, 200);
     else if (this.env.contentframe)
       this.show_contentframe(false);
 
+    // no source = search result, we'll need to detect if any of
+    // selected contacts are in writable addressbook to enable edit/delete
+    if (list.selection.length) {
+      if (!source) {
+        for (n in list.selection) {
+          sid = String(list.selection[n]).replace(/^[^-]+-/, '');
+          if (sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly) {
+            writable = true;
+            break;
+          }
+        }
+      }
+      else {
+        writable = !source.readonly;
+      }
+    }
+
     this.enable_command('compose', list.selection.length > 0);
-    this.enable_command('edit', (id && this.env.address_sources && !this.env.address_sources[this.env.source].readonly) ? true : false);
-    this.enable_command('delete', list.selection.length && this.env.address_sources && !this.env.address_sources[this.env.source].readonly);
+    this.enable_command('edit', id && writable);
+    this.enable_command('delete', list.selection.length && writable);
 
     return false;
   };
@@ -3797,12 +3822,12 @@
     if (!(selection.length || this.env.cid) || !confirm(this.get_label('deletecontactconfirm')))
       return;
 
-    var id, a_cids = [], qs = '';
+    var id, n, a_cids = [], qs = '';
 
     if (this.env.cid)
       a_cids.push(this.env.cid);
     else {
-      for (var n=0; n<selection.length; n++) {
+      for (n=0; n<selection.length; n++) {
         id = selection[n];
         a_cids.push(id);
         this.contact_list.remove_row(id, (n == selection.length-1));
@@ -3817,7 +3842,7 @@
       qs += '&_gid='+urlencode(this.env.group);
 
     // also send search request to get the right records from the next page
-    if (this.env.search_request) 
+    if (this.env.search_request)
       qs += '&_search='+this.env.search_request;
 
     // send request to server
@@ -4119,7 +4144,7 @@
             for (childcol in colprop.childs)
               cols.push(childcol);
           }
-          
+
           for (var i=0; i < cols.length; i++) {
             childcol = cols[i];
             cp = colprop.childs[childcol];
@@ -4241,14 +4266,20 @@
       target = window.frames[this.env.contentframe];
       this.contact_list.clear_selection();
     }
-    else if (framed)
-      return false;
 
-    this.location_href(this.env.comm_path+'&_action=search'+add_url
-      +'&_source='+urlencode(this.env.source)
-      +(this.env.group ? '&_gid='+urlencode(this.env.group) : ''), target);
+    this.location_href(this.env.comm_path+'&_action=search'+add_url, target);
 
     return true;
+  };
+
+  // unselect directory/group
+  this.unselect_directory = function()
+  {
+    if (this.env.address_sources.length > 1 || this.env.group != '') {
+      this.select_folder('', (this.env.group ? 'G'+this.env.source+this.env.group : this.env.source));
+      this.env.group = '';
+      this.env.source = '';
+    }
   };
 
 
@@ -5468,9 +5499,20 @@
     switch (response.action) {
       case 'delete':
         if (this.task == 'addressbook') {
-          var uid = this.contact_list.get_selection();
+          var sid, uid = this.contact_list.get_selection(), writable = false;
+
+          if (uid && this.contact_list.rows[uid]) {
+            // search results, get source ID from record ID
+            if (this.env.source == '') {
+              sid = String(uid).replace(/^[^-]+-/, '');
+              writable = sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly;
+            }
+            else {
+              writable = !this.env.address_sources[this.env.source].readonly;
+            }
+          }
           this.enable_command('compose', (uid && this.contact_list.rows[uid]));
-          this.enable_command('delete', 'edit', (uid && this.contact_list.rows[uid] && this.env.address_sources && !this.env.address_sources[this.env.source].readonly));
+          this.enable_command('delete', 'edit', writable);
           this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
         }
 
@@ -5519,10 +5561,9 @@
           this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
 
           if (response.action == 'list' || response.action == 'search') {
-            this.enable_command('group-create',
-              (this.env.address_sources[this.env.source].groups && !this.env.address_sources[this.env.source].readonly));
-            this.enable_command('group-rename', 'group-delete',
-              (this.env.address_sources[this.env.source].groups && this.env.group && !this.env.address_sources[this.env.source].readonly));
+            var source = this.env.source != '' ? this.env.address_sources[this.env.source] : null;
+            this.enable_command('group-create', (source && source.groups && !source.readonly));
+            this.enable_command('group-rename', 'group-delete', (source && source.groups && this.env.group && !source.readonly));
             this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
           }
         }
@@ -5580,7 +5621,7 @@
 
     return frame_name;
   };
-  
+
   // starts interval for keep-alive/check-recent signal
   this.start_keepalive = function()
   {
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index fa0fab5..e6927e8 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -326,6 +326,7 @@
 $labels['import'] = 'Import';
 $labels['importcontacts'] = 'Import contacts';
 $labels['importfromfile'] = 'Import from file:';
+$labels['importtarget'] = 'Add new contacts to address book:';
 $labels['importreplace'] = 'Replace the entire address book';
 $labels['importtext'] = 'You can upload contacts from an existing address book.<br/>We currently support importing addresses from the <a href="http://en.wikipedia.org/wiki/VCard">vCard</a> data format.';
 $labels['done'] = 'Done';
diff --git a/program/localization/pl_PL/labels.inc b/program/localization/pl_PL/labels.inc
index e425f12..f51e2e0 100644
--- a/program/localization/pl_PL/labels.inc
+++ b/program/localization/pl_PL/labels.inc
@@ -413,5 +413,6 @@
 $labels['search'] = 'Szukaj';
 $labels['advsearch'] = 'Wyszukiwanie zaawansowane';
 $labels['other'] = 'Inne';
+$labels['importtarget'] = 'Dodaj nowe kontakty do książki adresowej:';
 
 ?>
diff --git a/program/steps/addressbook/copy.inc b/program/steps/addressbook/copy.inc
index b891e01..4ee885b 100644
--- a/program/steps/addressbook/copy.inc
+++ b/program/steps/addressbook/copy.inc
@@ -23,75 +23,91 @@
 if (!$OUTPUT->ajax_call)
   return;
 
-$cid = get_input_value('_cid', RCUBE_INPUT_POST);
-$target = get_input_value('_to', RCUBE_INPUT_POST);
+
+$cids         = rcmail_get_cids();
+$target       = get_input_value('_to', RCUBE_INPUT_POST);
 $target_group = get_input_value('_togid', RCUBE_INPUT_POST);
 
-if ($cid && preg_match('/^[a-zA-Z0-9\+\/=_-]+(,[a-zA-Z0-9\+\/=_-]+)*$/', $cid) && strlen($target) && $target !== $source)
-{
-  $success = 0;
-  $TARGET = $RCMAIL->get_address_book($target);
+$success = 0;
+$maxnum  = $RCMAIL->config->get('max_group_members', 0);
 
-  if ($TARGET && $TARGET->ready && !$TARGET->readonly) {
-    $arr_cids = explode(',', $cid);
+foreach ($cids as $source => $cid)
+{
+    // Something wrong, target not specified
+    if (!strlen($target)) {
+        break;
+    }
+
+    // It maight happen when copying records from search result
+    // Do nothing, go to next source
+    if ($target == $source) {
+        continue;
+    }
+
+    $CONTACTS = $RCMAIL->get_address_book($source);
+    $TARGET   = $RCMAIL->get_address_book($target);
+
+    if (!$TARGET || !$TARGET->ready || $TARGET->readonly) {
+        break;
+    }
+
     $ids = array();
 
-    foreach ($arr_cids as $cid) {
-      $a_record = $CONTACTS->get_record($cid, true);
+    foreach ($cid as $cid) {
+        $a_record = $CONTACTS->get_record($cid, true);
 
-      // check if contact exists, if so, we'll need it's ID
-      $result = $TARGET->search('email', $a_record['email'], true, true);
+        // check if contact exists, if so, we'll need it's ID
+        $result = $TARGET->search('email', $a_record['email'], true, true);
 
-      // insert contact record
-      if (!$result->count) {
-        $plugin = $RCMAIL->plugins->exec_hook('contact_create', array(
-          'record' => $a_record, 'source' => $target, 'group' => $target_group));
+        // insert contact record
+        if (!$result->count) {
+            $plugin = $RCMAIL->plugins->exec_hook('contact_create', array(
+                'record' => $a_record, 'source' => $target, 'group' => $target_group));
 
-        if (!$plugin['abort']) {
-          if ($insert_id = $TARGET->insert($plugin['record'], false)) {
-            $ids[] = $insert_id;
-            $success++;
-          }
+            if (!$plugin['abort']) {
+                if ($insert_id = $TARGET->insert($plugin['record'], false)) {
+                    $ids[] = $insert_id;
+                    $success++;
+                }
+            }
+            else if ($plugin['result']) {
+                $ids = array_merge($ids, $plugin['result']);
+                $success++;
+            }
         }
-        else if ($plugin['result']) {
-          $ids = array_merge($ids, $plugin['result']);
-          $success++;
+        else {
+            $record = $result->first();
+            $ids[] = $record['ID'];
         }
-      }
-      else {
-        $record = $result->first();
-        $ids[] = $record['ID'];
-      }
     }
 
     // assign to group
     if ($target_group && $TARGET->groups && !empty($ids)) {
-      $plugin = $RCMAIL->plugins->exec_hook('group_addmembers', array(
-        'group_id' => $target_group, 'ids' => $ids, 'source' => $target));
+        $plugin = $RCMAIL->plugins->exec_hook('group_addmembers', array(
+            'group_id' => $target_group, 'ids' => $ids, 'source' => $target));
 
-      if (!$plugin['abort']) {
-        $TARGET->reset();
-        $TARGET->set_group($target_group);
+        if (!$plugin['abort']) {
+            $TARGET->reset();
+            $TARGET->set_group($target_group);
 
-        if (($maxnum = $RCMAIL->config->get('max_group_members', 0)) && ($TARGET->count()->count + count($plugin['ids']) > $maxnum)) {
-          $OUTPUT->show_message('maxgroupmembersreached', 'warning', array('max' => $maxnum));
-          $OUTPUT->send();
+            if ($maxnum && ($TARGET->count()->count + count($plugin['ids']) > $maxnum)) {
+                $OUTPUT->show_message('maxgroupmembersreached', 'warning', array('max' => $maxnum));
+                $OUTPUT->send();
+            }
+
+            if (($cnt = $TARGET->add_to_group($target_group, $plugin['ids'])) && $cnt > $success)
+                $success = $cnt;
         }
-
-        if (($cnt = $TARGET->add_to_group($target_group, $plugin['ids'])) && $cnt > $success)
-          $success = $cnt;
-      }
-      else if ($plugin['result'])
-        $success = $plugin['result'];
+        else if ($plugin['result']) {
+            $success = $plugin['result'];
+        }
     }
-  }
-
-  if ($success == 0)
-    $OUTPUT->show_message('copyerror', 'error');
-  else
-    $OUTPUT->show_message('copysuccess', 'notice', array('nr' => $success));
 }
+
+if ($success == 0)
+    $OUTPUT->show_message('copyerror', 'error');
+else
+    $OUTPUT->show_message('copysuccess', 'notice', array('nr' => $success));
 
 // send response
 $OUTPUT->send();
-
diff --git a/program/steps/addressbook/delete.inc b/program/steps/addressbook/delete.inc
index 1cd4f35..af9bdb1 100644
--- a/program/steps/addressbook/delete.inc
+++ b/program/steps/addressbook/delete.inc
@@ -19,42 +19,126 @@
 
 */
 
-if ($OUTPUT->ajax_call &&
-    ($cid = get_input_value('_cid', RCUBE_INPUT_POST)) &&
-    preg_match('/^[a-zA-Z0-9\+\/=_-]+(,[a-zA-Z0-9\+\/=_-]+)*$/', $cid)
-) {
+// process ajax requests only
+if (!$OUTPUT->ajax_call)
+    return;
+
+$cids   = rcmail_get_cids();
+$delcnt = 0;
+
+foreach ($cids as $source => $cid)
+{
+    $CONTACTS = rcmail_contact_source($source);
+
+    if ($CONTACTS->readonly) {
+        // more sources? do nothing, probably we have search results from
+        // more than one source, some of these sources can be readonly
+        if (count($cids) == 1) {
+            $OUTPUT->show_message('contactdelerror', 'error');
+            $OUTPUT->command('list_contacts');
+        }
+        continue;
+    }
+
     $plugin = $RCMAIL->plugins->exec_hook('contact_delete', array(
-        'id' => $cid, 'source' => get_input_value('_source', RCUBE_INPUT_GPC)));
+        'id' => $cid, 'source' => $source));
 
     $deleted = !$plugin['abort'] ? $CONTACTS->delete($cid) : $plugin['result'];
 
     if (!$deleted) {
         $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'contactdelerror', 'error');
         $OUTPUT->command('list_contacts');
+        $OUTPUT->send();
     }
     else {
-        $OUTPUT->show_message('contactdeleted', 'confirmation');
-
-        // count contacts for this user
-        $result = $CONTACTS->count();
-
-        // update saved search after data changed
-        if (($search_request = $_REQUEST['_search']) && isset($_SESSION['search'][$search_request]))
-            $_SESSION['search'][$search_request] = $CONTACTS->refresh_search();
-
-        // update message count display
-        $OUTPUT->set_env('pagecount', ceil($result->count / $CONTACTS->page_size));
-        $OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result->count));
-
-        // add new rows from next page (if any)
-        $pages = ceil(($result->count + $deleted) / $CONTACTS->page_size);
-        if ($_GET['_from'] != 'show' && $pages > 1 && $CONTACTS->list_page < $pages)
-            rcmail_js_contacts_list($CONTACTS->list_records(null, -$deleted));
+        $delcnt += $deleted;
     }
-
-    // send response
-    $OUTPUT->send();
 }
 
-exit;
+$OUTPUT->show_message('contactdeleted', 'confirmation');
 
+$page = isset($_SESSION['page']) ? $_SESSION['page'] : 1;
+
+// update saved search after data changed
+if (($search_request = $_REQUEST['_search']) && isset($_SESSION['search'][$search_request])) {
+    $search  = (array)$_SESSION['search'][$search_request];
+    $records = array();
+
+    // Get records from all sources (refresh search)
+    foreach ($search as $s => $set) {
+        $source = $RCMAIL->get_address_book($s);
+
+        // reset page
+        $source->set_page(1);
+        $source->set_pagesize(9999);
+        $source->set_search_set($set);
+
+        // get records
+        $result = $source->list_records(array('name', 'email'));
+
+        if (!$result->count) {
+            unset($search[$s]);
+            continue;
+        }
+
+        while ($row = $result->next()) {
+            $row['sourceid'] = $s;
+            $key = $row['name'] . ':' . $row['sourceid'];
+            $records[$key] = $row;
+        }
+        unset($result);
+
+        $search[$s] = $source->get_search_set();
+    }
+
+    $_SESSION['search'][$search_request] = $search;
+
+    // create resultset object
+    $count  = count($records);
+    $first  = ($page-1) * $CONFIG['pagesize'];
+    $result = new rcube_result_set($count, $first);
+
+    // get records from the next page to add to the list
+    $pages = ceil((count($records) + $delcnt) / $CONFIG['pagesize']);
+    if ($_GET['_from'] != 'show' && $pages > 1 && $page < $pages) {
+        // sort the records
+        ksort($records, SORT_LOCALE_STRING);
+
+        $first += $CONFIG['pagesize'];
+        // create resultset object
+        $res = new rcube_result_set($count, $first - $delcnt);
+
+        if ($CONFIG['pagesize'] < $count) {
+            $records = array_slice($records, $first - $delcnt, $delcnt);
+        }
+
+        $res->records = array_values($records);
+        $records = $res;
+    }
+    else {
+        unset($records);
+    }
+}
+else {
+    // count contacts for this user
+    $result = $CONTACTS->count();
+
+    // get records from the next page to add to the list
+    $pages = ceil(($result->count + $delcnt) / $CONFIG['pagesize']);
+    if ($_GET['_from'] != 'show' && $pages > 1 && $page < $pages) {
+        $CONTACTS->set_page($page);
+        $records = $CONTACTS->list_records(null, -$delcnt);
+    }
+}
+
+// update message count display
+$OUTPUT->set_env('pagecount', ceil($result->count / $CONFIG['pagesize']));
+$OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result));
+
+// add new rows from next page (if any)
+if (!empty($records)) {
+    rcmail_js_contacts_list($records);
+}
+
+// send response
+$OUTPUT->send();
diff --git a/program/steps/addressbook/edit.inc b/program/steps/addressbook/edit.inc
index 96c4870..05572de 100644
--- a/program/steps/addressbook/edit.inc
+++ b/program/steps/addressbook/edit.inc
@@ -19,15 +19,38 @@
 
 */
 
+if ($RCMAIL->action == 'edit') {
+    // Get contact ID and source ID from request
+    $cids   = rcmail_get_cids();
+    $source = key($cids);
+    $cid    = array_shift($cids[$source]);
 
-if (($cid = get_input_value('_cid', RCUBE_INPUT_GPC)) && ($record = $CONTACTS->get_record($cid, true)))
-    $OUTPUT->set_env('cid', $record['ID']);
+    // Initialize addressbook
+    $CONTACTS = rcmail_contact_source($source, true);
 
-// adding not allowed here
-if ($CONTACTS->readonly) {
-    $OUTPUT->show_message('sourceisreadonly');
-    rcmail_overwrite_action('show');
-    return;
+    // Contact edit
+    if ($cid && ($record = $CONTACTS->get_record($cid, true))) {
+        $OUTPUT->set_env('cid', $record['ID']);
+    }
+
+    // adding not allowed here
+    if ($CONTACTS->readonly) {
+        $OUTPUT->show_message('sourceisreadonly');
+        rcmail_overwrite_action('show');
+        return;
+    }
+}
+else {
+    $source = get_input_value('_source', RCUBE_INPUT_GPC);
+
+    $CONTACTS = $RCMAIL->get_address_book($source);
+
+    // find writable addressbook
+    if (!$CONTACTS || $CONTACTS->readonly)
+        $source = rcmail_default_source(true);
+
+    // Initialize addressbook
+    $CONTACTS = rcmail_contact_source($source, true);
 }
 
 
@@ -45,7 +68,7 @@
          $RCMAIL->output->show_message('contactnotfound');
          return false;
      }
-     
+
      return $record;
 }
 
@@ -90,7 +113,7 @@
 
     // add some labels to client
     $RCMAIL->output->add_label('noemailwarning', 'nonamewarning');
-    
+
     // copy (parsed) address template to client
     if (preg_match_all('/\{([a-z0-9]+)\}([^{]*)/i', $RCMAIL->config->get('address_template', ''), $templ, PREG_SET_ORDER))
       $RCMAIL->output->set_env('address_template', $templ);
@@ -123,7 +146,7 @@
             ),
         ),
     );
-    
+
     if (isset($CONTACT_COLTYPES['notes'])) {
         $form['notes'] = array(
             'name'    => rcube_label('notes'),
@@ -158,11 +181,11 @@
   if ($max_postsize && $max_postsize < $max_filesize)
     $max_filesize = $max_postsize;
   $max_filesize = show_bytes($max_filesize);
-  
+
   $hidden = new html_hiddenfield(array('name' => '_cid', 'value' => $GLOBALS['cid']));
   $input = new html_inputfield(array('type' => 'file', 'name' => '_photo', 'size' => $attrib['size']));
   $button = new html_inputfield(array('type' => 'button'));
-  
+
   $out = html::div($attrib,
     $OUTPUT->form_tag(array('name' => 'uploadform', 'method' => 'post', 'enctype' => 'multipart/form-data'),
       $hidden->show() .
@@ -174,7 +197,7 @@
       )
     )
   );
-  
+
   $OUTPUT->add_label('addphoto','replacephoto');
   $OUTPUT->add_gui_object('uploadbox', $attrib['id']);
   return $out;
@@ -211,12 +234,14 @@
 }
 
 
-$OUTPUT->add_handler('contactedithead', 'rcmail_contact_edithead');
-$OUTPUT->add_handler('contacteditform', 'rcmail_contact_editform');
-$OUTPUT->add_handler('contactphoto',    'rcmail_contact_photo');
-$OUTPUT->add_handler('photouploadform', 'rcmail_upload_photo_form');
+$OUTPUT->add_handlers(array(
+    'contactedithead' => 'rcmail_contact_edithead',
+    'contacteditform' => 'rcmail_contact_editform',
+    'contactphoto'    => 'rcmail_contact_photo',
+    'photouploadform' => 'rcmail_upload_photo_form',
+));
 
-if (!$CONTACTS->get_result() && $OUTPUT->template_exists('contactadd'))
+if ($RCMAIL->action == 'add' && $OUTPUT->template_exists('contactadd'))
     $OUTPUT->send('contactadd');
 
 // this will be executed if no template for addcontact exists
diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc
index bfe8e99..04b98a3 100644
--- a/program/steps/addressbook/export.inc
+++ b/program/steps/addressbook/export.inc
@@ -6,6 +6,7 @@
  |                                                                       |
  | This file is part of the Roundcube Webmail client                     |
  | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
+ | Copyright (C) 2011, Kolab Systems AG                                  |
  | Licensed under the GNU GPL                                            |
  |                                                                       |
  | PURPOSE:                                                              |
@@ -13,16 +14,56 @@
  |                                                                       |
  +-----------------------------------------------------------------------+
  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ | Author: Aleksander Machniak <machniak@kolabsys.com>                   |
  +-----------------------------------------------------------------------+
 
- $Id:  $
+ $Id$
 
 */
 
-// get contacts for this user
-$CONTACTS->set_page(1);
-$CONTACTS->set_pagesize(99999);
-$result = $CONTACTS->list_records(null, 0, true);
+// Use search result
+if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']]))
+{
+    $search  = (array)$_SESSION['search'][$_REQUEST['_search']];
+    $records = array();
+
+    // Get records from all sources
+    foreach ($search as $s => $set) {
+        $source = $RCMAIL->get_address_book($s);
+
+        // reset page
+        $source->set_page(1);
+        $source->set_pagesize(99999);
+        $source->set_search_set($set);
+
+        // get records
+        $result = $source->list_records();
+
+        while ($row = $result->next()) {
+            $row['sourceid'] = $s;
+            $key = $row['name'] . ':' . $row['sourceid'];
+            $records[$key] = $row;
+        }
+        unset($result);
+    }
+
+    // sort the records
+    ksort($records, SORT_LOCALE_STRING);
+
+    // create resultset object
+    $count  = count($records);
+    $result = new rcube_result_set($count);
+    $result->records = array_values($records);
+}
+// selected directory/group
+else {
+    $CONTACTS = rcmail_contact_source(null, true);
+
+    // get contacts for this user
+    $CONTACTS->set_page(1);
+    $CONTACTS->set_pagesize(99999);
+    $result = $CONTACTS->list_records(null, 0, true);
+}
 
 // send downlaod headers
 send_nocacheing_headers();
@@ -30,25 +71,25 @@
 header('Content-Disposition: attachment; filename="rcube_contacts.vcf"');
 
 while ($result && ($row = $result->next())) {
-  // we already have a vcard record
-  if ($row['vcard'] && $row['name']) {
-    echo rcube_vcard::rfc2425_fold($row['vcard']) . "\n";
-  }
-  // copy values into vcard object
-  else {
-    $vcard = new rcube_vcard($row['vcard']);
-    $vcard->reset();
-    foreach ($row as $key => $values) {
-      list($field, $section) = explode(':', $key);
-      foreach ((array)$values as $value) {
-        if (is_array($value) || strlen($value))
-          $vcard->set($field, $value, strtoupper($section));
-      }
+    // we already have a vcard record
+    if ($row['vcard'] && $row['name']) {
+        echo rcube_vcard::rfc2425_fold($row['vcard']) . "\n";
     }
+    // copy values into vcard object
+    else {
+        $vcard = new rcube_vcard($row['vcard']);
+        $vcard->reset();
 
-    echo $vcard->export(true) . "\n";
-  }
+        foreach ($row as $key => $values) {
+            list($field, $section) = explode(':', $key);
+            foreach ((array)$values as $value) {
+                if (is_array($value) || strlen($value))
+                    $vcard->set($field, $value, strtoupper($section));
+            }
+        }
+
+        echo $vcard->export(true) . "\n";
+    }
 }
 
 exit;
-
diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc
index 8b7e641..9731d9f 100644
--- a/program/steps/addressbook/func.inc
+++ b/program/steps/addressbook/func.inc
@@ -21,46 +21,6 @@
 
 $SEARCH_MODS_DEFAULT = array('name'=>1, 'firstname'=>1, 'surname'=>1, 'email'=>1, '*'=>1);
 
-// select source
-$source = get_input_value('_source', RCUBE_INPUT_GPC);
-
-if (!$RCMAIL->action && !$OUTPUT->ajax_call) {
-    // add list of address sources to client env
-    $js_list = $RCMAIL->get_address_sources();
-
-    // if source is not set use first directory
-    if (empty($source))
-        $source = $js_list[key($js_list)]['id'];
-
-    $search_mods = $RCMAIL->config->get('addressbook_search_mods', $SEARCH_MODS_DEFAULT);
-    $OUTPUT->set_env('search_mods', $search_mods);
-    $OUTPUT->set_env('address_sources', $js_list);
-}
-
-// instantiate a contacts object according to the given source
-$CONTACTS = $RCMAIL->get_address_book($source);
-
-$CONTACTS->set_pagesize($CONFIG['pagesize']);
-
-// set list properties and session vars
-if (!empty($_GET['_page']))
-    $CONTACTS->set_page(($_SESSION['page'] = intval($_GET['_page'])));
-else
-    $CONTACTS->set_page(isset($_SESSION['page']) ?$_SESSION['page'] : 1);
-
-if (!empty($_REQUEST['_gid']))
-    $CONTACTS->set_group(get_input_value('_gid', RCUBE_INPUT_GPC));
-
-// set data source env
-$OUTPUT->set_env('source', $source ? $source : '0');
-$OUTPUT->set_env('readonly', $CONTACTS->readonly);
-if (!$OUTPUT->ajax_call) {
-    $search_mods = $RCMAIL->config->get('addressbook_search_mods', $SEARCH_MODS_DEFAULT);
-    $OUTPUT->set_env('search_mods', $search_mods);
-    $OUTPUT->set_pagetitle(rcube_label('addressbook'));
-}
-
-
 // general definition of contact coltypes
 $CONTACT_COLTYPES = array(
   'name'         => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('name'), 'category' => 'main'),
@@ -96,19 +56,93 @@
   // TODO: define fields for vcards like GEO, KEY
 );
 
-// reduce/extend $CONTACT_COLTYPES with specification from the current $CONTACT object
-if (is_array($CONTACTS->coltypes)) {
-    // remove cols not listed by the backend class
-    $contact_cols = $CONTACTS->coltypes[0] ? array_flip($CONTACTS->coltypes) : $CONTACTS->coltypes;
-    $CONTACT_COLTYPES = array_intersect_key($CONTACT_COLTYPES, $contact_cols);
-    // add associative coltypes definition
-    if (!$CONTACTS->coltypes[0]) {
-        foreach ($CONTACTS->coltypes as $col => $colprop)
-            $CONTACT_COLTYPES[$col] = $CONTACT_COLTYPES[$col] ? array_merge($CONTACT_COLTYPES[$col], $colprop) : $colprop;
+
+// Addressbook UI
+if (!$RCMAIL->action && !$OUTPUT->ajax_call) {
+    // add list of address sources to client env
+    $js_list = $RCMAIL->get_address_sources();
+
+    // use first directory by default
+    $source = $js_list[key($js_list)]['id'];
+
+    // find writeable source
+    foreach ($js_list as $s) {
+        if (!$s['readonly']) {
+            $OUTPUT->set_env('writable_source', $s['id']);
+            break;
+        }
     }
+
+    $search_mods = $RCMAIL->config->get('addressbook_search_mods', $SEARCH_MODS_DEFAULT);
+    $OUTPUT->set_env('search_mods', $search_mods);
+    $OUTPUT->set_env('address_sources', $js_list);
+
+    $OUTPUT->set_pagetitle(rcube_label('addressbook'));
+
+    $CONTACTS = rcmail_contact_source($source, true);
 }
 
-$OUTPUT->set_env('photocol', is_array($CONTACT_COLTYPES['photo']));
+
+// instantiate a contacts object according to the given source
+function rcmail_contact_source($source=null, $init_env=false)
+{
+    global $RCMAIL, $OUTPUT, $CONFIG, $CONTACT_COLTYPES;
+
+    if (!strlen($source)) {
+        $source = get_input_value('_source', RCUBE_INPUT_GPC);
+    }
+
+    if (!strlen($source)) {
+        return null;
+    }
+
+    // Get object
+    $CONTACTS = $RCMAIL->get_address_book($source);
+    $CONTACTS->set_pagesize($CONFIG['pagesize']);
+
+    // set list properties and session vars
+    if (!empty($_GET['_page']))
+        $CONTACTS->set_page(($_SESSION['page'] = intval($_GET['_page'])));
+    else
+        $CONTACTS->set_page(isset($_SESSION['page']) ? $_SESSION['page'] : 1);
+
+    if (!empty($_REQUEST['_gid']))
+        $CONTACTS->set_group(get_input_value('_gid', RCUBE_INPUT_GPC));
+
+    if (!$init_env)
+        return $CONTACTS;
+
+    $OUTPUT->set_env('readonly', $CONTACTS->readonly);
+    $OUTPUT->set_env('source', $source);
+
+    // reduce/extend $CONTACT_COLTYPES with specification from the current $CONTACT object
+    if (is_array($CONTACTS->coltypes)) {
+        // remove cols not listed by the backend class
+        $contact_cols = $CONTACTS->coltypes[0] ? array_flip($CONTACTS->coltypes) : $CONTACTS->coltypes;
+        $CONTACT_COLTYPES = array_intersect_key($CONTACT_COLTYPES, $contact_cols);
+        // add associative coltypes definition
+        if (!$CONTACTS->coltypes[0]) {
+            foreach ($CONTACTS->coltypes as $col => $colprop)
+                $CONTACT_COLTYPES[$col] = $CONTACT_COLTYPES[$col] ? array_merge($CONTACT_COLTYPES[$col], $colprop) : $colprop;
+        }
+    }
+
+    $OUTPUT->set_env('photocol', is_array($CONTACT_COLTYPES['photo']));
+
+    return $CONTACTS;
+}
+
+
+function rcmail_default_source($writable=false)
+{
+    global $RCMAIL;
+
+    // get list of address sources
+    $list = $RCMAIL->get_address_sources($writable);
+
+    // use first directory by default
+    return $list[key($list)]['id'];
+}
 
 
 function rcmail_directory_list($attrib)
@@ -230,6 +264,11 @@
     while ($row = $result->next()) {
         $a_row_cols = array();
 
+        // build contact ID with source ID
+        if (isset($row['sourceid'])) {
+            $row['ID'] = $row['ID'].'-'.$row['sourceid'];
+        }
+
         // format each col
         foreach ($a_show_cols as $col)
             $a_row_cols[$col] = Q($row[$col]);
@@ -246,7 +285,7 @@
 
     if (!$attrib['id'])
         $attrib['id'] = 'rcmcontactframe';
-    
+
     $attrib['name'] = $attrib['id'];
 
     $OUTPUT->set_env('contentframe', $attrib['name']);
@@ -269,12 +308,14 @@
 }
 
 
-function rcmail_get_rowcount_text()
+function rcmail_get_rowcount_text($result=null)
 {
-    global $CONTACTS;
-  
+    global $CONTACTS, $CONFIG;
+
     // read nr of contacts
-    $result = $CONTACTS->get_result();
+    if (!$result) {
+        $result = $CONTACTS->get_result();
+    }
     if (!$result) {
         $result = $CONTACTS->count();
     }
@@ -286,7 +327,7 @@
             'name'  => 'contactsfromto',
             'vars'  => array(
             'from'  => $result->first + 1,
-            'to'    => min($result->count, $result->first + $CONTACTS->page_size),
+            'to'    => min($result->count, $result->first + $CONFIG['pagesize']),
             'count' => $result->count)
         ));
 
@@ -303,7 +344,7 @@
             && ($label = preg_replace('/(\d+)$/', '', $label))
             && rcube_label_exists($label))
         return rcube_label($label) . ' ' . $m[1];
-    
+
     return ucfirst($type);
 }
 
@@ -322,11 +363,11 @@
     $del_button = $attrib['deleteicon'] ? html::img(array('src' => $CONFIG['skin_path'] . $attrib['deleteicon'], 'alt' => rcube_label('delete'))) : rcube_label('delete');
     unset($attrib['deleteicon']);
     $out = '';
-    
+
     // get default coltypes
     $coltypes = $GLOBALS['CONTACT_COLTYPES'];
     $coltype_labels = array();
-    
+
     foreach ($coltypes as $col => $prop) {
         if ($prop['subtypes']) {
             $subtype_names = array_map('rcmail_get_type_label', $prop['subtypes']);
@@ -369,7 +410,7 @@
                     // skip cols unknown to the backend
                     if (!$coltypes[$col])
                         continue;
-                    
+
                     // only string values are expected here
                     if (is_array($record[$col]))
                         $record[$col] = join(' ', $record[$col]);
@@ -390,7 +431,7 @@
                 }
                 $content .= html::div($blockname, $fields);
             }
-            
+
             if ($edit_mode)
                 $content .= html::p('addfield', $select_add->show(null));
 
@@ -522,13 +563,13 @@
                     else   // row without label
                         $rows .= html::div('row', html::div('contactfield', $val));
                 }
-                
+
                 // add option to the add-field menu
                 if (!$colprop['limit'] || $coltypes[$field]['count'] < $colprop['limit']) {
                     $select_add->add($colprop['label'], $col);
                     $select_add->_count++;
                 }
-                
+
                 // wrap rows in fieldgroup container
                 $content .= html::tag('fieldset', array('class' => 'contactfieldgroup ' . ($colprop['subtypes'] ? 'contactfieldgroupmulti ' : '') . 'contactcontroller' . $col, 'style' => ($rows ? null : 'display:none')),
                   ($colprop['subtypes'] ? html::tag('legend', null, Q($colprop['label'])) : ' ') .
@@ -599,6 +640,48 @@
 }
 
 
+/**
+ * Returns contact ID(s) and source(s) from GET/POST data
+ *
+ * @return array List of contact IDs per-source
+ */
+function rcmail_get_cids()
+{
+    // contact ID (or comma-separated list of IDs) is provided in two
+    // forms. If _source is an empty string then the ID is a string
+    // containing contact ID and source name in form: <ID>-<SOURCE>
+
+    $cid    = get_input_value('_cid', RCUBE_INPUT_GPC);
+    $source = get_input_value('_source', RCUBE_INPUT_GPC);
+
+    if (!preg_match('/^[a-zA-Z0-9\+\/=_-]+(,[a-zA-Z0-9\+\/=_-]+)*$/', $cid)) {
+        return array();
+    }
+
+    $cid        = explode(',', $cid);
+    $got_source = strlen($source);
+    $result     = array();
+
+    // create per-source contact IDs array
+    foreach ($cid as $id) {
+        // if _source is not specified we'll find it from decoded ID
+        if (!$got_source) {
+            list ($c, $s) = explode('-', $id, 2);
+            if (strlen($s)) {
+                $result[$s][] = $c;
+            }
+            else if (strlen($source)) {
+                $result[$source][] = $c;
+            }
+        }
+        else {
+            $result[$source][] = $id;
+        }
+    }
+
+    return $result;
+}
+
 // register UI objects
 $OUTPUT->add_handlers(array(
     'directorylist' => 'rcmail_directory_list',
diff --git a/program/steps/addressbook/groups.inc b/program/steps/addressbook/groups.inc
index 2517873..208df24 100644
--- a/program/steps/addressbook/groups.inc
+++ b/program/steps/addressbook/groups.inc
@@ -19,12 +19,13 @@
 
 */
 
+$source = get_input_value('_source', RCUBE_INPUT_GPC);
+$CONTACTS = rcmail_contact_source($source, true);
+
 if ($CONTACTS->readonly || !$CONTACTS->groups) {
   $OUTPUT->show_message('sourceisreadonly', 'warning');
   $OUTPUT->send();
 }
-
-$source = get_input_value('_source', RCUBE_INPUT_GPC);
 
 if ($RCMAIL->action == 'group-addmembers') {
   if (($gid = get_input_value('_gid', RCUBE_INPUT_POST)) && ($ids = get_input_value('_cid', RCUBE_INPUT_POST))) {
diff --git a/program/steps/addressbook/import.inc b/program/steps/addressbook/import.inc
index 4583be5..fdac962 100644
--- a/program/steps/addressbook/import.inc
+++ b/program/steps/addressbook/import.inc
@@ -13,9 +13,10 @@
  |                                                                       |
  +-----------------------------------------------------------------------+
  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ | Author: Aleksander Machniak <machniak@kolabsys.com>                   |
  +-----------------------------------------------------------------------+
 
- $Id: $
+ $Id$
 
 */
 
@@ -26,30 +27,44 @@
 {
   global $RCMAIL, $OUTPUT;
   $target = get_input_value('_target', RCUBE_INPUT_GPC);
-  
+
   $attrib += array('id' => "rcmImportForm");
-  
-  $abook = new html_hiddenfield(array('name' => '_target', 'value' => $target));
-  $form = $abook->show();
+
+  $writable_books = $RCMAIL->get_address_sources(true);
 
   $upload = new html_inputfield(array('type' => 'file', 'name' => '_file', 'id' => 'rcmimportfile', 'size' => 40));
-  $form .= html::p(null, html::label('rcmimportfile', rcube_label('importfromfile')) . html::br() . $upload->show());
-  
+  $form = html::p(null, html::label('rcmimportfile', rcube_label('importfromfile')) . $upload->show());
+
+  // addressbook selector
+  if (count($writable_books) > 1) {
+    $select = new html_select(array('name' => '_target', 'id' => 'rcmimporttarget'));
+
+    foreach ($writable_books as $book)
+        $select->add($book['name'], $book['id']);
+
+    $form .= html::p(null, html::label('rcmimporttarget', rcube_label('importtarget'))
+        . $select->show($target));
+  }
+  else {
+    $abook = new html_hiddenfield(array('name' => '_target', 'value' => key($writable_books)));
+    $form .= $abook->show();
+  }
+
   $check_replace = new html_checkbox(array('name' => '_replace', 'value' => 1, 'id' => 'rcmimportreplace'));
   $form .= html::p(null, $check_replace->show(get_input_value('_replace', RCUBE_INPUT_GPC)) .
     html::label('rcmimportreplace', rcube_label('importreplace')));
-  
+
   $OUTPUT->add_label('selectimportfile','importwait');
   $OUTPUT->add_gui_object('importform', $attrib['id']);
-  
+
   $out = html::p(null, Q(rcube_label('importtext'), 'show'));
-  
+
   $out .= $OUTPUT->form_tag(array(
       'action' => $RCMAIL->url('import'),
       'method' => 'post',
       'enctype' => 'multipart/form-data') + $attrib,
     $form);
-  
+
   return $out;
 }
 
@@ -60,19 +75,19 @@
 function rcmail_import_confirm($attrib)
 {
   global $IMPORT_STATS;
-  
+
   $vars = get_object_vars($IMPORT_STATS);
   $vars['names'] = $vars['skipped_names'] = '';
-  
+
   $content = html::p(null, rcube_label(array(
       'name' => 'importconfirm',
       'nr' => $IMORT_STATS->inserted,
       'vars' => $vars,
     )) . ($IMPORT_STATS->names ? ':' : '.'));
-    
+
   if ($IMPORT_STATS->names)
     $content .= html::p('em', join(', ', array_map('Q', $IMPORT_STATS->names)));
-  
+
   if ($IMPORT_STATS->skipped) {
       $content .= html::p(null, rcube_label(array(
           'name' => 'importconfirmskipped',
@@ -81,7 +96,7 @@
         )) . ':');
       $content .= html::p('em', join(', ', array_map('Q', $IMPORT_STATS->skipped_names)));
   }
-  
+
   return html::div($attrib, $content);
 }
 
@@ -93,10 +108,10 @@
 {
   global $IMPORT_STATS, $OUTPUT;
   $target = get_input_value('_target', RCUBE_INPUT_GPC);
-  
+
   $attrib += array('type' => 'input');
   unset($attrib['name']);
-  
+
   if (is_object($IMPORT_STATS)) {
     $attrib['class'] = trim($attrib['class'] . ' mainaction');
     $out = $OUTPUT->button(array('command' => 'list', 'prop' => $target, 'label' => 'done') + $attrib);
@@ -107,7 +122,7 @@
     $attrib['class'] = trim($attrib['class'] . ' mainaction');
     $out .= $OUTPUT->button(array('command' => 'import', 'label' => 'import') + $attrib);
   }
-  
+
   return $out;
 }
 
@@ -137,13 +152,13 @@
     $IMPORT_STATS->skipped_names = array();
     $IMPORT_STATS->count = count($vcards);
     $IMPORT_STATS->inserted = $IMPORT_STATS->skipped = $IMPORT_STATS->nomail = $IMPORT_STATS->errors = 0;
-    
+
     if ($replace)
       $CONTACTS->delete_all();
-    
+
     foreach ($vcards as $vcard) {
       $email = $vcard->email[0];
-      
+
       // skip entries without an e-mail address
       if (empty($email)) {
         $IMPORT_STATS->nomail++;
@@ -152,7 +167,7 @@
 
       // We're using UTF8 internally
       $email = rcube_idn_to_utf8($email);
-      
+
       if (!$replace && $email) {
         // compare e-mail address
         $existing = $CONTACTS->search('email', $email, false, false);
@@ -165,10 +180,10 @@
           continue;
         }
       }
-      
+
       $a_record = $vcard->get_assoc();
       $a_record['vcard'] = $vcard->export();
-      
+
       $plugin = $RCMAIL->plugins->exec_hook('contact_create', array('record' => $a_record, 'source' => null));
       $a_record = $plugin['record'];
 
diff --git a/program/steps/addressbook/list.inc b/program/steps/addressbook/list.inc
index 864ad93..dbc86e2 100644
--- a/program/steps/addressbook/list.inc
+++ b/program/steps/addressbook/list.inc
@@ -19,20 +19,68 @@
 
 */
 
-// set message set for search result
+// Use search result
 if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']]))
-    $CONTACTS->set_search_set($_SESSION['search'][$_REQUEST['_search']]);
+{
+    $search  = (array)$_SESSION['search'][$_REQUEST['_search']];
+    $records = array();
 
-// get contacts for this user
-$result = $CONTACTS->list_records(array('name'));
+    if (!empty($_GET['_page']))
+        $page = intval($_GET['_page']);
+    else
+        $page = isset($_SESSION['page']) ? $_SESSION['page'] : 1;
+
+    $_SESSION['page'] = $page;
+
+    // Get records from all sources
+    foreach ($search as $s => $set) {
+        $source = $RCMAIL->get_address_book($s);
+
+        // reset page
+        $source->set_page(1);
+        $source->set_pagesize(9999);
+        $source->set_search_set($set);
+
+        // get records
+        $result = $source->list_records(array('name', 'email'));
+
+        while ($row = $result->next()) {
+            $row['sourceid'] = $s;
+            $key = $row['name'] . ':' . $row['sourceid'];
+            $records[$key] = $row;
+        }
+        unset($result);
+    }
+
+    // sort the records
+    ksort($records, SORT_LOCALE_STRING);
+
+    // create resultset object
+    $count  = count($records);
+    $first  = ($page-1) * $CONFIG['pagesize'];
+    $result = new rcube_result_set($count, $first);
+
+    // we need only records for current page
+    if ($CONFIG['pagesize'] < $count) {
+        $records = array_slice($records, $first, $CONFIG['pagesize']);
+    }
+
+    $result->records = array_values($records);
+}
+// List selected directory
+else {
+    $CONTACTS = rcmail_contact_source(null, true);
+
+    // get contacts for this user
+    $result = $CONTACTS->list_records(array('name'));
+}
 
 // update message count display
-$OUTPUT->set_env('pagecount', ceil($result->count / $CONTACTS->page_size));
-$OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($rowcount));
+$OUTPUT->set_env('pagecount', ceil($result->count / $CONFIG['pagesize']));
+$OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result));
 
 // create javascript list
 rcmail_js_contacts_list($result);
 
 // send response
 $OUTPUT->send();
-
diff --git a/program/steps/addressbook/mailto.inc b/program/steps/addressbook/mailto.inc
index e4f2801..5996b9d 100644
--- a/program/steps/addressbook/mailto.inc
+++ b/program/steps/addressbook/mailto.inc
@@ -19,33 +19,36 @@
 
 */
 
-$cid = get_input_value('_cid', RCUBE_INPUT_POST);
-$recipients = null;
+$cids   = rcmail_get_cids();
 $mailto = array();
 
-if ($cid && preg_match('/^[a-z0-9\+\/=_-]+(,[a-z0-9\+\/=_-]+)*$/i', $cid) && $CONTACTS->ready)
+foreach ($cids as $source => $cid)
 {
-  $CONTACTS->set_page(1);
-  $CONTACTS->set_pagesize(substr_count($cid, ',')+2); // +2 to skip counting query
-  $recipients = $CONTACTS->search($CONTACTS->primary_key, $cid);
+    $CONTACTS = $RCMAIL->get_address_book($source);
 
-  while (is_object($recipients) && ($rec = $recipients->iterate())) {
-    $emails = $CONTACTS->get_col_values('email', $rec, true);
-    $mailto[] = format_email_recipient($emails[0], $rec['name']);
-  }
+    if ($CONTACTS->ready)
+    {
+        $CONTACTS->set_page(1);
+        $CONTACTS->set_pagesize(count($cid) + 2); // +2 to skip counting query
+        $recipients = $CONTACTS->search($CONTACTS->primary_key, $cid, false, true, true, 'email');
+
+        while (is_object($recipients) && ($rec = $recipients->iterate())) {
+            $emails = $CONTACTS->get_col_values('email', $rec, true);
+            $mailto[] = format_email_recipient($emails[0], $rec['name']);
+        }
+    }
 }
 
 if (!empty($mailto))
 {
-  $mailto_str = join(', ', $mailto);
-  $mailto_id = substr(md5($mailto_str), 0, 16);
-  $_SESSION['mailto'][$mailto_id] = urlencode($mailto_str);
-  $OUTPUT->redirect(array('task' => 'mail', '_action' => 'compose', '_mailto' => $mailto_id));
+    $mailto_str = join(', ', $mailto);
+    $mailto_id = substr(md5($mailto_str), 0, 16);
+    $_SESSION['mailto'][$mailto_id] = urlencode($mailto_str);
+    $OUTPUT->redirect(array('task' => 'mail', '_action' => 'compose', '_mailto' => $mailto_id));
 }
-else
-  $OUTPUT->show_message('nocontactsfound', 'warning');
-
+else {
+    $OUTPUT->show_message('nocontactsfound', 'warning');
+}
 
 // send response
 $OUTPUT->send();
-
diff --git a/program/steps/addressbook/save.inc b/program/steps/addressbook/save.inc
index 0092eb1..19d8f8c 100644
--- a/program/steps/addressbook/save.inc
+++ b/program/steps/addressbook/save.inc
@@ -19,8 +19,10 @@
 
 */
 
-$cid = get_input_value('_cid', RCUBE_INPUT_POST);
+$CONTACTS = rcmail_contact_source(null, true);
+$cid      = get_input_value('_cid', RCUBE_INPUT_POST);
 $return_action = empty($cid) ? 'add' : 'edit';
+
 
 // cannot edit record
 if ($CONTACTS->readonly) {
@@ -38,19 +40,19 @@
     if ($filepath = $_FILES['_photo']['tmp_name']) {
         // check file type and resize image
         $imageprop = rcmail::imageprops($_FILES['_photo']['tmp_name']);
-        
+
         if ($imageprop['width'] && $imageprop['height']) {
             $maxsize = intval($RCMAIL->config->get('contact_photo_size', 160));
             $tmpfname = tempnam($RCMAIL->config->get('temp_dir'), 'rcmImgConvert');
             $save_hook = 'attachment_upload';
-            
+
             // scale image to a maximum size
             if (($imageprop['width'] > $maxsize || $imageprop['height'] > $maxsize) &&
                   (rcmail::imageconvert(array('in' => $filepath, 'out' => $tmpfname, 'size' => $maxsize.'x'.$maxsize, 'type' => $imageprop['type'])) !== false)) {
                 $filepath = $tmpfname;
                 $save_hook = 'attachment_save';
             }
-            
+
             // save uploaded file in storage backend
             $attachment = $RCMAIL->plugins->exec_hook($save_hook, array(
                 'path' => $filepath,
@@ -87,10 +89,10 @@
             $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array('size' => show_bytes(parse_bytes($maxsize)))));
         else
             $msg = rcube_label('fileuploaderror');
-            
+
         $OUTPUT->command('display_message', $msg, 'error');
     }
-    
+
     $OUTPUT->command('photo_upload_end');
     $OUTPUT->send('iframe');
 }
@@ -155,7 +157,7 @@
     }
     else
         unset($a_record['photo']);
-    
+
     // cleanup session data
     $RCMAIL->plugins->exec_hook('attachments_cleanup', array('group' => 'contact'));
     $RCMAIL->session->remove('contacts');
@@ -240,7 +242,7 @@
         $CONTACTS->add_to_group($gid, $plugin['ids']);
       }
     }
-    
+
     // add contact row or jump to the page where it should appear
     $CONTACTS->reset();
     $result = $CONTACTS->search($CONTACTS->primary_key, $insert_id);
diff --git a/program/steps/addressbook/search.inc b/program/steps/addressbook/search.inc
index 498e9b2..a78478f 100644
--- a/program/steps/addressbook/search.inc
+++ b/program/steps/addressbook/search.inc
@@ -31,17 +31,17 @@
 
 function rcmail_contact_search()
 {
-    global $RCMAIL, $OUTPUT, $CONTACTS, $CONTACT_COLTYPES, $SEARCH_MODS_DEFAULT;
+    global $RCMAIL, $OUTPUT, $CONFIG, $SEARCH_MODS_DEFAULT;
 
     $adv = isset($_POST['_adv']);
 
     // get fields/values from advanced search form
     if ($adv) {
-        foreach ($CONTACT_COLTYPES as $col => $colprop) {
-            $s = trim(get_input_value('_'.$col, RCUBE_INPUT_POST, true));
-            if (strlen($s)) {
+        foreach (array_keys($_POST) as $key) {
+            $s = trim(get_input_value($key, RCUBE_INPUT_POST, true));
+            if (strlen($s) && preg_match('/^_search_([a-zA-Z0-9_-]+)$/', $key, $m)) {
                 $search[] = $s;
-                $fields[] = $col;
+                $fields[] = $m[1];
             }
         }
 
@@ -71,20 +71,77 @@
         }
     }
 
+    // get sources list
+    $sources    = $RCMAIL->get_address_sources();
+    $search_set = array();
+    $records    = array();
+
+    foreach ($sources as $s) {
+        $source = $RCMAIL->get_address_book($s['id']);
+
+        // check if all search fields are supported....
+        if (is_array($fields)) {
+            $cols = $source->coltypes[0] ? array_flip($source->coltypes) : $source->coltypes;
+            $supported = 0;
+
+            foreach ($fields as $f) {
+                if (array_key_exists($f, $cols)) {
+                    $supported ++;
+                }
+            }
+
+            // ...if not, we can skip this source
+            if ($supported < count($fields)) {
+                continue;
+            }
+        }
+
+        // reset page
+        $source->set_page(1);
+        $source->set_pagesize(9999);
+
+        // get contacts count
+        $result = $source->search($fields, $search, false, false);
+
+        if (!$result->count) {
+            continue;
+        }
+
+        // get records
+        $result = $source->list_records(array('name', 'email'));
+
+        while ($row = $result->next()) {
+            $row['sourceid'] = $s['id'];
+            $key = $row['name'] . ':' . $row['sourceid'];
+            $records[$key] = $row;
+        }
+
+        unset($result);
+        $search_set[$s['id']] = $source->get_search_set();
+    }
+
+    // sort the records
+    ksort($records, SORT_LOCALE_STRING);
+
+    // create resultset object
+    $count  = count($records);
+    $result = new rcube_result_set($count);
+
+    // cut first-page records
+    if ($CONFIG['pagesize'] < $count) {
+        $records = array_slice($records, 0, $CONFIG['pagesize']);
+    }
+
+    $result->records = array_values($records);
+
     // search request ID
     $search_request = md5('addr'
         .(is_array($fields) ? implode($fields, ',') : $fields)
         .(is_array($search) ? implode($search, ',') : $search));
 
-    // reset page
-    $CONTACTS->set_page(1);
-    $_SESSION['page'] = 1;
-
-    // get contacts for this user
-    $result = $CONTACTS->search($fields, $search);
-
     // save search settings in session
-    $_SESSION['search'][$search_request] = $CONTACTS->get_search_set();
+    $_SESSION['search'][$search_request] = $search_set;
+    $_SESSION['page'] = 1;
 
     if ($adv)
         $OUTPUT->command('list_contacts_clear');
@@ -99,8 +156,11 @@
 
     // update message count display
     $OUTPUT->command('set_env', 'search_request', $search_request);
-    $OUTPUT->command('set_env', 'pagecount', ceil($result->count / $CONTACTS->page_size));
-    $OUTPUT->command('set_rowcount', rcmail_get_rowcount_text());
+    $OUTPUT->command('set_env', 'pagecount', ceil($result->count / $CONFIG['pagesize']));
+    $OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result));
+
+    // unselect currently selected directory/group
+    $OUTPUT->command('unselect_directory');
 
     // send response
     $OUTPUT->send($adv ? 'iframe' : null);
@@ -108,7 +168,7 @@
 
 function rcmail_contact_search_form($attrib)
 {
-    global $RCMAIL, $CONTACTS, $CONTACT_COLTYPES;
+    global $RCMAIL, $CONTACT_COLTYPES;
 
     $i_size = !empty($attrib['size']) ? $attrib['size'] : 30;
 
@@ -130,7 +190,26 @@
         ),
     );
 
-    foreach ($CONTACT_COLTYPES as $col => $colprop)
+    // get supported coltypes from all address sources
+    $sources  = $RCMAIL->get_address_sources();
+    $coltypes = array();
+
+    foreach ($sources as $s) {
+        $CONTACTS = $RCMAIL->get_address_book($s['id']);
+
+        if (is_array($CONTACTS->coltypes)) {
+            $contact_cols = $CONTACTS->coltypes[0] ? array_flip($CONTACTS->coltypes) : $CONTACTS->coltypes;
+            $coltypes = array_merge($coltypes, $contact_cols);
+        }
+    }
+
+    // merge supported coltypes with $CONTACT_COLTYPES
+    foreach ($coltypes as $col => $colprop) {
+        $coltypes[$col] = $CONTACT_COLTYPES[$col] ? array_merge($CONTACT_COLTYPES[$col], (array)$colprop) : (array)$colprop;
+    }
+
+    // build form fields list
+    foreach ($coltypes as $col => $colprop)
     {
         if ($colprop['type'] != 'image' && !$colprop['nosearch'])
         {
@@ -142,15 +221,13 @@
                 $colprop['size'] = $i_size;
 
             $content  = html::div('row', html::div('contactfieldlabel label', Q($label))
-                . html::div('contactfieldcontent', rcmail_get_edit_field($col, '', $colprop, $ftype)));
+                . html::div('contactfieldcontent', rcmail_get_edit_field('search_'.$col, '', $colprop, $ftype)));
 
             $form[$category]['content'][] = $content;
         }
     }
 
-    $hiddenfields = new html_hiddenfield(array(
-        'name' => '_source', 'value' => get_input_value('_source', RCUBE_INPUT_GPC)));
-    $hiddenfields->add(array('name' => '_gid', 'value' => $CONTACTS->group_id));
+    $hiddenfields = new html_hiddenfield();
     $hiddenfields->add(array('name' => '_adv', 'value' => 1));
 
     $out = $RCMAIL->output->request_form(array(
diff --git a/program/steps/addressbook/show.inc b/program/steps/addressbook/show.inc
index f62bad3..998dee1 100644
--- a/program/steps/addressbook/show.inc
+++ b/program/steps/addressbook/show.inc
@@ -19,9 +19,16 @@
 
 */
 
+// Get contact ID and source ID from request
+$cids   = rcmail_get_cids();
+$source = key($cids);
+$cid    = array_shift($cids[$source]);
+
+// Initialize addressbook source
+$CONTACTS = rcmail_contact_source($source, true);
 
 // read contact record
-if (($cid = get_input_value('_cid', RCUBE_INPUT_GPC)) && ($record = $CONTACTS->get_record($cid, true))) {
+if ($cid && ($record = $CONTACTS->get_record($cid, true))) {
     $OUTPUT->set_env('cid', $record['ID']);
 }
 
@@ -41,7 +48,7 @@
         if (!preg_match('![^a-z0-9/=+-]!i', $data))
             $data = base64_decode($data, true);
     }
-    
+
     header('Content-Type: ' . rc_image_content_type($data));
     echo $data ? $data : file_get_contents('program/blank.gif');
     exit;
@@ -190,14 +197,15 @@
     $form_end = '</form>';
 
     $RCMAIL->output->add_gui_object('editform', 'form');
-  
+
     return $form_start . $table->show() . $form_end;
 }
 
 
-//$OUTPUT->framed = $_framed;
-$OUTPUT->add_handler('contacthead', 'rcmail_contact_head');
-$OUTPUT->add_handler('contactdetails', 'rcmail_contact_details');
-$OUTPUT->add_handler('contactphoto', 'rcmail_contact_photo');
+$OUTPUT->add_handlers(array(
+    'contacthead'    => 'rcmail_contact_head',
+    'contactdetails' => 'rcmail_contact_details',
+    'contactphoto'   => 'rcmail_contact_photo',
+));
 
 $OUTPUT->send('contact');

--
Gitblit v1.9.1