summaryrefslogtreecommitdiff
path: root/plugins/kolab_addressbook
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/kolab_addressbook')
-rw-r--r--plugins/kolab_addressbook/kolab_addressbook.php153
-rw-r--r--plugins/kolab_addressbook/localization/en_US.inc12
-rw-r--r--plugins/kolab_addressbook/rcube_kolab_contacts.php876
3 files changed, 1041 insertions, 0 deletions
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
new file mode 100644
index 000000000..68179c387
--- /dev/null
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -0,0 +1,153 @@
+<?php
+
+require_once(dirname(__FILE__) . '/rcube_kolab_contacts.php');
+
+/**
+ * Kolab address book
+ *
+ * Sample plugin to add a new address book source with data from Kolab storage
+ * This is work-in-progress for the Roundcube+Kolab integration.
+ *
+ * @author Thomas Bruederli <roundcube@gmail.com>
+ *
+ */
+class kolab_addressbook extends rcube_plugin
+{
+ private $folders;
+ private $sources;
+
+ /**
+ * Required startup method of a Roundcube plugin
+ */
+ public function init()
+ {
+ // load required plugin
+ $this->require_plugin('kolab_core');
+
+ $this->add_texts('localization');
+
+ // register hooks
+ $this->add_hook('addressbooks_list', array($this, 'address_sources'));
+ $this->add_hook('addressbook_get', array($this, 'get_address_book'));
+ $this->add_hook('contact_form', array($this, 'contact_form'));
+
+ // extend list of address sources to be used for autocompletion
+ $rcmail = rcmail::get_instance();
+ if ($rcmail->action == 'autocomplete' || $rcmail->action == 'group-expand') {
+ $sources = (array) $rcmail->config->get('autocomplete_addressbooks', array());
+ foreach ($this->_list_sources() as $abook_id => $abook) {
+ if (!in_array($abook_id, $sources))
+ $sources[] = $abook_id;
+ }
+ $rcmail->config->set('autocomplete_addressbooks', $sources);
+ }
+ }
+
+ /**
+ * Handler for the addressbooks_list hook.
+ *
+ * This will add all instances of available Kolab-based address books
+ * to the list of address sources of Roundcube.
+ *
+ * @param array Hash array with hook parameters
+ * @return array Hash array with modified hook parameters
+ */
+ public function address_sources($p)
+ {
+ foreach ($this->_list_sources() as $abook_id => $abook) {
+ // register this address source
+ $p['sources'][$abook_id] = array(
+ 'id' => $abook_id,
+ 'name' => $abook->get_name(),
+ 'readonly' => $abook->readonly,
+ 'groups' => $abook->groups,
+ );
+ }
+
+ return $p;
+ }
+
+
+ /**
+ * Getter for the rcube_addressbook instance
+ */
+ public function get_address_book($p)
+ {
+ if ($this->sources[$p['id']]) {
+ $p['instance'] = $this->sources[$p['id']];
+ }
+
+ return $p;
+ }
+
+
+ private function _list_sources()
+ {
+ // already read sources
+ if (isset($this->sources))
+ return $this->sources;
+
+ // get all folders that have "contact" type
+ $this->folders = rcube_kolab::get_folders('contact');
+ $this->sources = array();
+
+ if (PEAR::isError($this->folders)) {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed to list contact folders from Kolab server:" . $this->folders->getMessage()),
+ true, false);
+ }
+ else {
+ foreach ($this->folders as $c_folder) {
+ // create instance of rcube_contacts
+ $abook_id = strtolower(asciiwords(strtr($c_folder->name, '/.', '--')));
+ $abook = new rcube_kolab_contacts($c_folder->name);
+ $this->sources[$abook_id] = $abook;
+ }
+ }
+
+ return $this->sources;
+ }
+
+
+ /**
+ * Plugin hook called before rendering the contact form or detail view
+ */
+ public function contact_form($p)
+ {
+ // none of our business
+ if (!is_a($GLOBALS['CONTACTS'], 'rcube_kolab_contacts'))
+ return $p;
+
+ // extend the list of contact fields to be displayed in the 'info' section
+ if (is_array($p['form']['info'])) {
+ $p['form']['info']['content']['initials'] = array('size' => 6);
+ $p['form']['info']['content']['officelocation'] = array('size' => 40);
+ $p['form']['info']['content']['profession'] = array('size' => 40);
+ $p['form']['info']['content']['children'] = array('size' => 40);
+
+ // re-order fields according to the coltypes list
+ $block = array();
+ $contacts = reset($this->sources);
+ foreach ($contacts->coltypes as $col => $prop) {
+ if (isset($p['form']['info']['content'][$col]))
+ $block[$col] = $p['form']['info']['content'][$col];
+ }
+
+ $p['form']['info']['content'] = $block;
+
+ // define a separate section 'settings'
+ $p['form']['settings'] = array(
+ 'name' => rcube_label('kolab_addressbook.settings'),
+ 'content' => array(
+ 'pgppublickey' => array('size' => 40, 'visible' => true),
+ 'freebusyurl' => array('size' => 40, 'visible' => true),
+ )
+ );
+ }
+
+ return $p;
+ }
+
+}
diff --git a/plugins/kolab_addressbook/localization/en_US.inc b/plugins/kolab_addressbook/localization/en_US.inc
new file mode 100644
index 000000000..968d9e691
--- /dev/null
+++ b/plugins/kolab_addressbook/localization/en_US.inc
@@ -0,0 +1,12 @@
+<?php
+
+$labels = array();
+$labels['initials'] = 'Initials';
+$labels['profession'] = 'Profession';
+$labels['officelocation'] = 'Office location';
+$labels['children'] = 'Children';
+$labels['pgppublickey'] = 'PGP publickey';
+$labels['freebusyurl'] = 'Free-busy URL';
+$labels['settings'] = 'Settings';
+
+?> \ No newline at end of file
diff --git a/plugins/kolab_addressbook/rcube_kolab_contacts.php b/plugins/kolab_addressbook/rcube_kolab_contacts.php
new file mode 100644
index 000000000..8eb1f7856
--- /dev/null
+++ b/plugins/kolab_addressbook/rcube_kolab_contacts.php
@@ -0,0 +1,876 @@
+<?php
+
+
+/**
+ * Backend class for a custom address book
+ *
+ * This part of the Roundcube+Kolab integration and connects the
+ * rcube_addressbook interface with the rcube_kolab wrapper for Kolab_Storage
+ *
+ * @author Thomas Bruederli
+ * @see rcube_addressbook
+ */
+class rcube_kolab_contacts extends rcube_addressbook
+{
+ public $primary_key = 'ID';
+ public $readonly = false;
+ public $groups = true;
+ public $coltypes = array(
+ 'name' => array('limit' => 1),
+ 'firstname' => array('limit' => 1),
+ 'surname' => array('limit' => 1),
+ 'middlename' => array('limit' => 1),
+ 'prefix' => array('limit' => 1),
+ 'suffix' => array('limit' => 1),
+ 'nickname' => array('limit' => 1),
+ 'jobtitle' => array('limit' => 1),
+ 'organization' => array('limit' => 1),
+ 'department' => array('limit' => 1),
+ 'gender' => array('limit' => 1),
+ 'initials' => array('type' => 'text', 'size' => 6, 'limit' => 1, 'label' => 'kolab_addressbook.initials'),
+ 'email' => array('subtypes' => null),
+ 'phone' => array(),
+ 'im' => array('limit' => 1, 'subtypes' => null),
+ 'website' => array('limit' => 1, 'subtypes' => null),
+ 'address' => array('limit' => 2, 'subtypes' => array('home','business')),
+ 'birthday' => array('limit' => 1),
+ 'anniversary' => array('limit' => 1),
+ 'officelocation' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => 'kolab_addressbook.officelocation'),
+ 'profession' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => 'kolab_addressbook.profession'),
+ 'manager' => array('limit' => 1),
+ 'assistant' => array('limit' => 1),
+ 'spouse' => array('limit' => 1),
+ 'children' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => 'kolab_addressbook.children'),
+ 'pgppublickey' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => 'kolab_addressbook.pgppublickey'),
+ 'freebusyurl' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => 'kolab_addressbook.freebusyurl'),
+ 'notes' => array(),
+ 'photo' => array(),
+ // TODO: define more Kolab-specific fields such as: language, latitude, longitude
+ );
+
+ private $gid;
+ private $imap;
+ private $kolab;
+ private $folder;
+ private $contactstorage;
+ private $liststorage;
+ private $contacts;
+ private $distlists;
+ private $groupmembers;
+ private $id2uid;
+ private $filter;
+ private $result;
+ private $imap_folder = 'INBOX/Contacts';
+ private $gender_map = array(0 => 'male', 1 => 'female');
+ private $phonetypemap = array('home' => 'home1', 'work' => 'business1', 'work2' => 'business2', 'workfax' => 'businessfax');
+ private $addresstypemap = array('work' => 'business');
+ private $fieldmap = array(
+ // kolab => roundcube
+ 'full-name' => 'name',
+ 'given-name' => 'firstname',
+ 'middle-names' => 'middlename',
+ 'last-name' => 'surname',
+ 'prefix' => 'prefix',
+ 'suffix' => 'suffix',
+ 'nick-name' => 'nickname',
+ 'organization' => 'organization',
+ 'department' => 'department',
+ 'job-title' => 'jobtitle',
+ 'initials' => 'initials',
+ 'birthday' => 'birthday',
+ 'anniversary' => 'anniversary',
+ 'im-address' => 'im',
+ 'web-page' => 'website',
+ 'office-location' => 'officelocation',
+ 'profession' => 'profession',
+ 'manager-name' => 'manager',
+ 'assistant' => 'assistant',
+ 'spouse-name' => 'spouse',
+ 'children' => 'children',
+ 'body' => 'notes',
+ 'pgp-publickey' => 'pgppublickey',
+ 'free-busy-url' => 'freebusyurl',
+ );
+
+
+ public function __construct($imap_folder = null)
+ {
+ if ($imap_folder)
+ $this->imap_folder = $imap_folder;
+
+ // extend coltypes configuration
+ $format = rcube_kolab::get_format('contact');
+ $this->coltypes['phone']['subtypes'] = $format->_phone_types;
+ $this->coltypes['address']['subtypes'] = $format->_address_types;
+
+ // set localized labels for proprietary cols
+ foreach ($this->coltypes as $col => $prop) {
+ if (is_string($prop['label']))
+ $this->coltypes[$col]['label'] = rcube_label($prop['label']);
+ }
+
+ // fetch objects from the given IMAP folder
+ $this->contactstorage = rcube_kolab::get_storage($this->imap_folder);
+ $this->liststorage = rcube_kolab::get_storage($this->imap_folder, 'distributionlist');
+
+ $this->ready = !PEAR::isError($this->contactstorage) && !PEAR::isError($this->liststorage);
+ }
+
+
+ /**
+ * Getter for the address book name to be displayed
+ *
+ * @return string Name of this address book
+ */
+ public function get_name()
+ {
+ return strtr(preg_replace('!^(INBOX|user)/!i', '', $this->imap_folder), '/', ':');
+ }
+
+
+ /**
+ * Setter for the current group
+ */
+ public function set_group($gid)
+ {
+ $this->gid = $gid;
+ }
+
+
+ /**
+ * Save a search string for future listings
+ *
+ * @param mixed Search params to use in listing method, obtained by get_search_set()
+ */
+ public function set_search_set($filter)
+ {
+ $this->filter = $filter;
+ }
+
+
+ /**
+ * Getter for saved search properties
+ *
+ * @return mixed Search properties used by this class
+ */
+ public function get_search_set()
+ {
+ return $this->filter;
+ }
+
+
+ /**
+ * Reset saved results and search parameters
+ */
+ public function reset()
+ {
+ $this->result = null;
+ $this->filter = null;
+ }
+
+
+ /**
+ * List all active contact groups of this source
+ *
+ * @param string Optional search string to match group name
+ * @return array Indexed list of contact groups, each a hash array
+ */
+ function list_groups($search = null)
+ {
+ $this->_fetch_groups();
+ $groups = array();
+ foreach ((array)$this->distlists as $group) {
+ if (!$search || strstr(strtolower($group['last-name']), strtolower($search)))
+ $groups[] = array('ID' => $group['ID'], 'name' => $group['last-name']);
+ }
+ return $groups;
+ }
+
+ /**
+ * List the current set of contact records
+ *
+ * @param array List of cols to show
+ * @param int Only return this number of records, use negative values for tail
+ * @return array Indexed list of contact records, each a hash array
+ */
+ public function list_records($cols=null, $subset=0)
+ {
+ $this->result = $this->count();
+
+ // list member of the selected group
+ if ($this->gid) {
+ $seen = array();
+ $this->result->count = 0;
+ foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
+ // skip member that don't match the search filter
+ if (is_array($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false)
+ continue;
+ if ($this->contacts[$member['ID']] && !$seen[$member['ID']]++)
+ $this->result->count++;
+ }
+ $ids = array_keys($seen);
+ }
+ else
+ $ids = is_array($this->filter['ids']) ? $this->filter['ids'] : array_keys($this->contacts);
+
+ // fill contact data into the current result set
+ $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
+ $last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, count($ids));
+
+ for ($i = $start_row; $i < $last_row; $i++) {
+ if ($id = $ids[$i])
+ $this->result->add($this->contacts[$id]);
+ }
+
+ return $this->result;
+ }
+
+
+ /**
+ * Search records
+ *
+ * @param array List of fields to search in
+ * @param string Search value
+ * @param boolean True if results are requested, False if count only
+ * @param boolean True to skip the count query (select only)
+ * @param array List of fields that cannot be empty
+ * @return object rcube_result_set List of contact records and 'count' value
+ */
+ public function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
+ {
+ $this->_fetch_contacts();
+
+ // search by ID
+ if ($fields == $this->primary_key) {
+ return $this->get_record($value);
+ }
+
+ $value = strtolower($value);
+ if (!is_array($fields))
+ $fields = array($fields);
+ if (!is_array($required) && !empty($required))
+ $required = array($required);
+
+ $this->filter = array('fields' => $fields, 'value' => $value, 'strict' => $strict, 'ids' => array());
+
+ // search be iterating over all records in memory
+ foreach ($this->contacts as $id => $contact) {
+ // check if current contact has required values, otherwise skip it
+ if ($required) {
+ foreach ($required as $f)
+ if (empty($contact[$f]))
+ continue 2;
+ }
+ foreach ($fields as $f) {
+ foreach ((array)$contact[$f] as $val) {
+ $val = strtolower($val);
+ if (($strict && $val == $value) || (!$strict && strstr($val, $value))) {
+ $this->filter['ids'][] = $id;
+ break 2;
+ }
+ }
+ }
+ }
+
+ // list records (now limited by $this->filter)
+ return $this->list_records();
+ }
+
+
+ /**
+ * Refresh saved search results after data has changed
+ */
+ public function refresh_search()
+ {
+ if ($this->filter)
+ $this->search($this->filter['fields'], $this->filter['value'], $this->filter['strict']);
+
+ return $this->get_search_set();
+ }
+
+
+ /**
+ * Count number of available contacts in database
+ *
+ * @return rcube_result_set Result set with values for 'count' and 'first'
+ */
+ public function count()
+ {
+ $this->_fetch_contacts();
+ $this->_fetch_groups();
+ $count = $this->gid ? count($this->distlists[$this->gid]['member']) : (is_array($this->filter['ids']) ? count($this->filter['ids']) : count($this->contacts));
+ return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
+ }
+
+
+ /**
+ * Return the last result set
+ *
+ * @return rcube_result_set Current result set or NULL if nothing selected yet
+ */
+ public function get_result()
+ {
+ return $this->result;
+ }
+
+ /**
+ * Get a specific contact record
+ *
+ * @param mixed record identifier(s)
+ * @param boolean True to return record as associative array, otherwise a result set is returned
+ * @return mixed Result object with all record fields or False if not found
+ */
+ public function get_record($id, $assoc=false)
+ {
+ $this->_fetch_contacts();
+ if ($this->contacts[$id]) {
+ $this->result = new rcube_result_set(1);
+ $this->result->add($this->contacts[$id]);
+ return $assoc ? $this->contacts[$id] : $this->result;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Get group assignments of a specific contact record
+ *
+ * @param mixed Record identifier
+ * @return array List of assigned groups as ID=>Name pairs
+ */
+ public function get_record_groups($id)
+ {
+ $out = array();
+ $this->_fetch_groups();
+
+ foreach ((array)$this->groupmembers[$id] as $gid) {
+ if ($group = $this->distlists[$gid])
+ $out[$gid] = $group['last-name'];
+ }
+
+ return $out;
+ }
+
+
+ /**
+ * Create a new contact record
+ *
+ * @param array Assoziative array with save data
+ * Keys: Field name with optional section in the form FIELD:SECTION
+ * Values: Field value. Can be either a string or an array of strings for multiple values
+ * @param boolean True to check for duplicates first
+ * @return mixed The created record ID on success, False on error
+ */
+ public function insert($save_data, $check=false)
+ {
+ if (!is_array($save_data))
+ return false;
+
+ $insert_id = $existing = false;
+
+ // check for existing records by e-mail comparison
+ if ($check) {
+ foreach ($this->get_col_values('email', $save_data, true) as $email) {
+ if (($res = $this->search('email', $email, true, false)) && $res->count) {
+ $existing = true;
+ break;
+ }
+ }
+ }
+
+ if (!$existing) {
+ // generate new Kolab contact item
+ $object = $this->_from_rcube_contact($save_data);
+ $object['uid'] = $this->contactstorage->generateUID();
+
+ $saved = $this->contactstorage->save($object);
+
+ if (PEAR::isError($saved)) {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
+ true, false);
+ }
+ else {
+ $contact = $this->_to_rcube_contact($object);
+ $id = $contact['ID'];
+ $this->contacts[$id] = $contact;
+ $this->id2uid[$id] = $object['uid'];
+ $insert_id = $id;
+ }
+ }
+
+ return $insert_id;
+ }
+
+
+ /**
+ * Update a specific contact record
+ *
+ * @param mixed Record identifier
+ * @param array Assoziative array with save data
+ * Keys: Field name with optional section in the form FIELD:SECTION
+ * Values: Field value. Can be either a string or an array of strings for multiple values
+ * @return boolean True on success, False on error
+ */
+ public function update($id, $save_data)
+ {
+ $updated = false;
+ $this->_fetch_contacts();
+ if ($this->contacts[$id] && ($uid = $this->id2uid[$id])) {
+ $old = $this->contactstorage->getObject($uid);
+ $object = array_merge($old, $this->_from_rcube_contact($save_data));
+
+ $saved = $this->contactstorage->save($object, $uid);
+ if (PEAR::isError($saved)) {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
+ true, false);
+ }
+ else {
+ $this->contacts[$id] = $this->_to_rcube_contact($object);
+ $updated = true;
+ }
+ }
+
+ return $updated;
+ }
+
+ /**
+ * Mark one or more contact records as deleted
+ *
+ * @param array Record identifiers
+ */
+ public function delete($ids)
+ {
+ $this->_fetch_contacts();
+ $this->_fetch_groups();
+
+ if (!is_array($ids))
+ $ids = explode(',', $ids);
+
+ $count = 0;
+ foreach ($ids as $id) {
+ if ($uid = $this->id2uid[$id]) {
+ $deleted = $this->contactstorage->delete($uid);
+
+ if (PEAR::isError($deleted)) {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error deleting a contact object from the Kolab server:" . $deleted->getMessage()),
+ true, false);
+ }
+ else {
+ // remove from distribution lists
+ foreach ((array)$this->groupmembers[$id] as $gid)
+ $this->remove_from_group($gid, $id);
+
+ // clear internal cache
+ unset($this->contacts[$id], $this->id2uid[$id], $this->groupmembers[$id]);
+ $count++;
+ }
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * Remove all records from the database
+ */
+ public function delete_all()
+ {
+ if (!PEAR::isError($this->contactstorage->deleteAll())) {
+ $this->contacts = array();
+ $this->id2uid = array();
+ $this->result = null;
+ }
+ }
+
+
+ /**
+ * Close connection to source
+ * Called on script shutdown
+ */
+ public function close()
+ {
+ rcube_kolab::shutdown();
+ }
+
+
+ /**
+ * Create a contact group with the given name
+ *
+ * @param string The group name
+ * @return mixed False on error, array with record props in success
+ */
+ function create_group($name)
+ {
+ $this->_fetch_groups();
+ $result = false;
+
+ $list = array(
+ 'uid' => $this->liststorage->generateUID(),
+ 'last-name' => $name,
+ 'member' => array(),
+ );
+ $saved = $this->liststorage->save($list);
+
+ if (PEAR::isError($saved)) {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
+ true, false);
+ return false;
+ }
+ else {
+ $id = md5($list['uid']);
+ $this->distlists[$record['ID']] = $list;
+ $result = array('id' => $id, 'name' => $name);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Delete the given group and all linked group members
+ *
+ * @param string Group identifier
+ * @return boolean True on success, false if no data was changed
+ */
+ function delete_group($gid)
+ {
+ $this->_fetch_groups();
+ $result = false;
+
+ if ($list = $this->distlists[$gid])
+ $deleted = $this->liststorage->delete($list['uid']);
+
+ if (PEAR::isError($deleted)) {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error deleting distribution-list object from the Kolab server:" . $deleted->getMessage()),
+ true, false);
+ }
+ else
+ $result = true;
+
+ return $result;
+ }
+
+ /**
+ * Rename a specific contact group
+ *
+ * @param string Group identifier
+ * @param string New name to set for this group
+ * @return boolean New name on success, false if no data was changed
+ */
+ function rename_group($gid, $newname)
+ {
+ $this->_fetch_groups();
+ $list = $this->distlists[$gid];
+
+ if ($newname != $list['last-name']) {
+ $list['last-name'] = $newname;
+ $saved = $this->liststorage->save($list, $list['uid']);
+ }
+
+ if (PEAR::isError($saved)) {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
+ true, false);
+ return false;
+ }
+
+ return $newname;
+ }
+
+ /**
+ * Add the given contact records the a certain group
+ *
+ * @param string Group identifier
+ * @param array List of contact identifiers to be added
+ * @return int Number of contacts added
+ */
+ function add_to_group($gid, $ids)
+ {
+ if (!is_array($ids))
+ $ids = explode(',', $ids);
+
+ $added = 0;
+ $exists = array();
+
+ $this->_fetch_groups();
+ $this->_fetch_contacts();
+ $list = $this->distlists[$gid];
+
+ foreach ((array)$list['member'] as $i => $member)
+ $exists[] = $member['ID'];
+
+ // substract existing assignments from list
+ $ids = array_diff($ids, $exists);
+
+ foreach ($ids as $contact_id) {
+ if ($uid = $this->id2uid[$contact_id]) {
+ $contact = $this->contacts[$contact_id];
+ foreach ($this->get_col_values('email', $contact, true) as $email) {
+ $list['member'][] = array(
+ 'uid' => $uid,
+ 'display-name' => $contact['name'],
+ 'smtp-address' => $email,
+ );
+ }
+ $this->groupmembers[$contact_id][] = $gid;
+ $added++;
+ }
+ }
+
+ if ($added)
+ $saved = $this->liststorage->save($list, $list['uid']);
+
+ if (PEAR::isError($saved)) {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving distribution-list to Kolab server:" . $saved->getMessage()),
+ true, false);
+ $added = false;
+ }
+ else {
+ $this->distlists[$gid] = $list;
+ }
+
+ return $added;
+ }
+
+ /**
+ * Remove the given contact records from a certain group
+ *
+ * @param string Group identifier
+ * @param array List of contact identifiers to be removed
+ * @return int Number of deleted group members
+ */
+ function remove_from_group($gid, $ids)
+ {
+ if (!is_array($ids))
+ $ids = explode(',', $ids);
+
+ $this->_fetch_groups();
+ if (!($list = $this->distlists[$gid]))
+ return false;
+
+ $new_member = array();
+ foreach ((array)$list['member'] as $member) {
+ if (!in_array($member['ID'], $ids))
+ $new_member[] = $member;
+ }
+
+ // write distribution list back to server
+ $list['member'] = $new_member;
+ $saved = $this->liststorage->save($list, $list['uid']);
+
+ if (PEAR::isError($saved)) {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
+ true, false);
+ }
+ else {
+ // remove group assigments in local cache
+ foreach ($ids as $id) {
+ $j = array_search($gid, $this->groupmembers[$id]);
+ unset($this->groupmembers[$id][$j]);
+ }
+ $this->distlists[$gid] = $list;
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Simply fetch all records and store them in private member vars
+ */
+ private function _fetch_contacts()
+ {
+ if (!isset($this->contacts)) {
+ // read contacts
+ $this->contacts = $this->id2uid = array();
+ foreach ((array)$this->contactstorage->getObjects() as $record) {
+ $contact = $this->_to_rcube_contact($record);
+ $id = $contact['ID'];
+ $this->contacts[$id] = $contact;
+ $this->id2uid[$id] = $record['uid'];
+ }
+
+ // TODO: sort data arrays according to desired list sorting
+ }
+ }
+
+
+ /**
+ * Read distribution-lists AKA groups from server
+ */
+ private function _fetch_groups()
+ {
+ if (!isset($this->distlists)) {
+ $this->distlists = $this->groupmembers = array();
+ foreach ((array)$this->liststorage->getObjects() as $record) {
+ // FIXME: folders without any distribution-list objects return contacts instead ?!
+ if ($record['__type'] != 'Group')
+ continue;
+ $record['ID'] = md5($record['uid']);
+ foreach ((array)$record['member'] as $i => $member) {
+ $mid = md5($member['uid']);
+ $record['member'][$i]['ID'] = $mid;
+ $this->groupmembers[$mid][] = $record['ID'];
+ }
+ $this->distlists[$record['ID']] = $record;
+ }
+ }
+ }
+
+
+ /**
+ * Map fields from internal Kolab_Format to Roundcube contact format
+ */
+ private function _to_rcube_contact($record)
+ {
+ $out = array(
+ 'ID' => md5($record['uid']),
+ 'email' => array(),
+ 'phone' => array(),
+ );
+
+ foreach ($this->fieldmap as $kolab => $rcube) {
+ if (strlen($record[$kolab]))
+ $out[$rcube] = $record[$kolab];
+ }
+
+ if (isset($record['gender']))
+ $out['gender'] = $this->gender_map[$record['gender']];
+
+ foreach ((array)$record['email'] as $i => $email)
+ $out['email'][] = $email['smtp-address'];
+
+ if (!$record['email'] && $record['emails'])
+ $out['email'] = preg_split('/,\s*/', $record['emails']);
+
+ foreach ((array)$record['phone'] as $i => $phone)
+ $out['phone:'.$phone['type']][] = $phone['number'];
+
+ if (is_array($record['address'])) {
+ foreach ($record['address'] as $i => $adr) {
+ $key = 'address:' . $adr['type'];
+ $out[$key][] = array(
+ 'street' => $adr['street'],
+ 'locality' => $adr['locality'],
+ 'zipcode' => $adr['postal-code'],
+ 'region' => $adr['region'],
+ 'country' => $adr['country'],
+ );
+ }
+ }
+
+ // photo is stored as separate attachment
+ if ($record['picture'] && ($att = $record['_attachments'][$record['picture']])) {
+ $out['photo'] = $att['content'] ? $att['content'] : $this->contactstorage->getAttachment($att['key']);
+ }
+
+ // remove empty fields
+ return array_filter($out);
+ }
+
+ private function _from_rcube_contact($contact)
+ {
+ $object = array();
+
+ foreach (array_flip($this->fieldmap) as $rcube => $kolab) {
+ if (isset($contact[$rcube]))
+ $object[$kolab] = is_array($contact[$rcube]) ? $contact[$rcube][0] : $contact[$rcube];
+ else if ($values = $this->get_col_values($rcube, $contact, true))
+ $object[$kolab] = is_array($values) ? $values[0] : $values;
+ }
+
+ // format dates
+ if ($object['birthday'] && ($date = @strtotime($object['birthday'])))
+ $object['birthday'] = date('Y-m-d', $date);
+ if ($object['anniversary'] && ($date = @strtotime($object['anniversary'])))
+ $object['anniversary'] = date('Y-m-d', $date);
+
+ $gendermap = array_flip($this->gender_map);
+ if (isset($contact['gender']))
+ $object['gender'] = $gendermap[$contact['gender']];
+
+ $emails = $this->get_col_values('email', $contact, true);
+ $object['emails'] = join(', ', array_filter($emails));
+
+ foreach ($this->get_col_values('phone', $contact) as $type => $values) {
+ if ($this->phonetypemap[$type])
+ $type = $this->phonetypemap[$type];
+ foreach ((array)$values as $phone) {
+ if (!empty($phone)) {
+ $object['phone-' . $type] = $phone;
+ $object['phone'][] = array('number' => $phone, 'type' => $type);
+ }
+ }
+ }
+
+ foreach ($this->get_col_values('address', $contact) as $type => $values) {
+ if ($this->addresstypemap[$type])
+ $type = $this->addresstypemap[$type];
+
+ $basekey = 'addr-' . $type . '-';
+ foreach ((array)$values as $adr) {
+ // switch type if slot is already taken
+ if (isset($object[$basekey . 'type'])) {
+ $type = $type == 'home' ? 'business' : 'home';
+ $basekey = 'addr-' . $type . '-';
+ }
+
+ if (!isset($object[$basekey . 'type'])) {
+ $object[$basekey . 'type'] = $type;
+ $object[$basekey . 'street'] = $adr['street'];
+ $object[$basekey . 'locality'] = $adr['locality'];
+ $object[$basekey . 'postal-code'] = $adr['zipcode'];
+ $object[$basekey . 'region'] = $adr['region'];
+ $object[$basekey . 'country'] = $adr['country'];
+ }
+ else {
+ $object['address'][] = array(
+ 'type' => $type,
+ 'street' => $adr['street'],
+ 'locality' => $adr['locality'],
+ 'postal-code' => $adr['zipcode'],
+ 'region' => $adr['region'],
+ 'country' => $adr['country'],
+ );
+ }
+ }
+ }
+
+ // save new photo as attachment
+ if ($contact['photo']) {
+ $attkey = 'photo.attachment';
+ $object['_attachments'][$attkey] = array(
+ 'type' => rc_image_content_type($contact['photo']),
+ 'content' => preg_match('![^a-z0-9/=+-]!i', $contact['photo']) ? $contact['photo'] : base64_decode($contact['photo']),
+ );
+ $object['picture'] = $attkey;
+ }
+
+ return $object;
+ }
+
+}