From 41eaebde793ceed545b04ab468142e9f7486f78e Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 31 Jan 2012 09:57:59 +0000 Subject: [PATCH] first try to export distribution lists via CardDAV as vCard with "X-CALENDARSERVER-KIND:group", we might need a user-agent whitelist, as not all clients will understand that --- addressbook/inc/class.addressbook_bo.inc.php | 26 +-- .../inc/class.addressbook_groupdav.inc.php | 114 +++++++++++++- addressbook/inc/class.addressbook_so.inc.php | 44 ++++-- addressbook/inc/class.addressbook_sql.inc.php | 149 ++++++++++++------ .../inc/class.addressbook_vcal.inc.php | 57 ++++++- 5 files changed, 312 insertions(+), 78 deletions(-) diff --git a/addressbook/inc/class.addressbook_bo.inc.php b/addressbook/inc/class.addressbook_bo.inc.php index 8de0544f38..fe0865eefa 100755 --- a/addressbook/inc/class.addressbook_bo.inc.php +++ b/addressbook/inc/class.addressbook_bo.inc.php @@ -7,7 +7,7 @@ * @author Ralf Becker * @author Joerg Lehrke * @package addressbook - * @copyright (c) 2005-11 by Ralf Becker + * @copyright (c) 2005-12 by Ralf Becker * @copyright (c) 2005/6 by Cornelius Weiss * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ @@ -1727,38 +1727,40 @@ class addressbook_bo extends addressbook_so } /** - * Adds a distribution list + * Adds / updates a distribution list * - * @param string $name list-name + * @param string|array $keys list-name or array with column-name => value pairs to specify the list * @param int $owner user- or group-id - * @param array $contacts=array() contacts to add - * @return list_id or false on error + * @param array $contacts=array() contacts to add (only for not yet existing lists!) + * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name' + * @return int|boolean integer list_id or false on error */ - function add_list($name,$owner,$contacts=array()) + function add_list($keys,$owner,$contacts=array(),array &$data=array()) { if (!$this->check_list(null,EGW_ACL_ADD,$owner)) return false; - return parent::add_list($name,$owner,$contacts); + return parent::add_list($name,$owner,$contacts,$data); } /** - * Adds one contact to a distribution list + * Adds contacts to a distribution list * - * @param int $contact contact_id + * @param int|array $contact contact_id(s) * @param int $list list-id + * @param array $existing=null array of existing contact-id(s) of list, to not reread it, eg. array() * @return false on error */ - function add2list($contact,$list) + function add2list($contact,$list,array $existing=null) { if (!$this->check_list($list,EGW_ACL_EDIT)) return false; - return parent::add2list($contact,$list); + return parent::add2list($contact,$list,$existing); } /** * Removes one contact from distribution list(s) * - * @param int $contact contact_id + * @param int|array $contact contact_id(s) * @param int $list list-id * @return false on error */ diff --git a/addressbook/inc/class.addressbook_groupdav.inc.php b/addressbook/inc/class.addressbook_groupdav.inc.php index f89a1b9671..10e16588f9 100644 --- a/addressbook/inc/class.addressbook_groupdav.inc.php +++ b/addressbook/inc/class.addressbook_groupdav.inc.php @@ -42,7 +42,7 @@ class addressbook_groupdav extends groupdav_handler 'ADR;HOME' => array('','adr_two_street2','adr_two_street','adr_two_locality','adr_two_region', 'adr_two_postalcode','adr_two_countryname'), 'BDAY' => array('bday'), - //'CLASS' => array('private'), + //'CLASS' => array('private'), //'CATEGORIES' => array('cat_id'), 'EMAIL;WORK' => array('email'), 'EMAIL;HOME' => array('email_home'), @@ -70,6 +70,7 @@ class addressbook_groupdav extends groupdav_handler 'X-ASSISTANT' => array('assistent'), 'X-ASSISTANT-TEL' => array('tel_assistent'), 'UID' => array('uid'), + 'REV' => array('modified'), ); /** @@ -203,6 +204,31 @@ class addressbook_groupdav extends groupdav_handler $files[] = $this->add_resource($path, $contact, $props); } } + // add groups after contacts + if (!$start || count($contacts) < $start[1]) + { + if (($lists = $this->bo->read_lists(array('list_owner' => $filter['contact_owner']?$filter['contact_owner']:array_keys($this->bo->grants))))) + { + //_debug_array($lists); + foreach($lists as $list) + { + $list['carddav_name'] = $list['list_carddav_name']; + $props = array( + 'getcontenttype' => HTTP_WebDAV_Server::mkprop('getcontenttype', 'text/vcard'), + 'getlastmodified' => egw_time::to($list['list_modified'],'ts'), + 'displayname' => $list['list_name'], + 'getetag' => '"'.$list['list_id'].':'.$list['list_etag'].'"', + ); + if ($address_data) + { + $content = $handler->getGroupVCard($list); + $props['getcontentlength'] = bytes($content); + $props[] = HTTP_WebDAV_Server::mkprop(groupdav::CARDDAV,'address-data',$content,true); + } + $files[] = $this->add_resource($path, $list, $props); + } + } + } if ($this->debug) error_log(__METHOD__."($path,".array2string($filter).','.array2string($start).") took ".(microtime(true) - $starttime).' to return '.count($files).' items'); return $files; } @@ -386,7 +412,8 @@ class addressbook_groupdav extends groupdav_handler return $contact; } $handler = self::_get_handler(); - $options['data'] = $handler->getVCard($contact['id'],$this->charset,false); + $options['data'] = $contact['list_id'] ? $handler->getGroupVCard($contact) : + $handler->getVCard($contact['id'],$this->charset,false); // e.g. Evolution does not understand 'text/vcard' $options['mimetype'] = 'text/x-vcard; charset='.$this->charset; header('Content-Encoding: identity'); @@ -450,8 +477,13 @@ class addressbook_groupdav extends groupdav_handler $contactId = -1; $retval = '201 Created'; } + $is_group = $contact['##X-CALENDARSERVER-KIND'] == 'group'; + if ($oldContact && $is_group !== isset($oldContact['list_id'])) + { + throw new egw_exception_assertion_failed(__METHOD__."(,'$id',$user,'$prefix') can contact into group or visa-versa!"); + } - if (is_array($contact['cat_id'])) + if (!$is_group && is_array($contact['cat_id'])) { $contact['cat_id'] = implode(',',$this->bo->find_or_add_categories($contact['cat_id'], $contactId)); } @@ -486,7 +518,7 @@ class addressbook_groupdav extends groupdav_handler } if ($this->http_if_match) $contact['etag'] = self::etag2value($this->http_if_match); - if (!($save_ok = $this->bo->save($contact))) + if (!($save_ok = $is_group ? $this->save_group($contact) : $this->bo->save($contact))) { if ($this->debug) error_log(__METHOD__."(,$id) save(".array2string($contact).") failed, Ok=$save_ok"); if ($save_ok === 0) @@ -514,6 +546,69 @@ class addressbook_groupdav extends groupdav_handler return $retval; } + /** + * Save distribition-list / group + * + * @param array $contact + * @param array|false $oldContact + * @param int|boolean $list_id or false on error + */ + function save_group(array $contact, $oldContact=null) + { + $data = array('list_name' => $contact['n_fn']); + foreach(array('id','carddav_name','uid') as $name) + { + if ($name != self::$path_attr) $data['list_'.$name] = $contact[$name]; + } + if (($list_id=$this->bo->add_list(array('list_'.self::$path_attr => $contact[self::$path_attr]), + $contact['owner'], null, $data))) + { + // update members given in $contact['##X-CALENDARSERVER-MEMBER'] + $new_members = $contact['##X-CALENDARSERVER-MEMBER']; + if ($new_members[1] == ':' && ($n = unserialize($new_members))) + { + $new_members = $n['values']; + } + else + { + $new_members = array($new_members); + } + foreach($new_members as &$uid) $uid = substr($uid,9); // cut off "urn:uuid:" prefix + + if ($oldContact) + { + $to_add = array_diff($oldContact['members'],$new_members); + $to_delete = array_diff($new_members,$oldContact['members']); + } + else + { + $to_add = $new_members; + } + if ($to_add || $to_delete) + { + $to_add_ids = $to_delete_ids = array(); + $filter = array('uid' => $to_delete ? array_merge($to_add, $to_delete) : $to_add); + if (($contacts =& $this->bo->search(array(),'id,uid','','','',False,'AND',false,$filter))) + { + foreach($contacts as $contact) + { + if ($to_delete && in_array($contact['uid'], $to_delete)) + { + $to_delete_ids[] = $contact['id']; + } + else + { + $to_add_ids[] = $contact['id']; + } + } + } + if ($to_add_ids) $this->bo->add2list($to_add_ids, $list_id, array()); + if ($to_delete_ids) $this->bo->remove_from_list($to_delete_ids, $list_id); + } + } + return $list_id; + } + /** * Query ctag for addressbook * @@ -661,6 +756,17 @@ class addressbook_groupdav extends groupdav_handler } $contact = $this->bo->read(array(self::$path_attr => $id, 'tid' => $non_deleted_tids)); + // see if we have a distribution-list / group with that id + if (!$contact && ($contact = $this->bo->read_lists(array('list_'.self::$path_attr => $id)))) + { + $contact = array_shift($contact); + $contact['n_fn'] = $contact['n_family'] = $contact['list_name']; + foreach(array('owner','id','carddav_name','modified','modifier','created','creator') as $name) + { + $contact[$name] = $contact['list_'.$name]; + } + } + if ($contact && $contact['tid'] == addressbook_so::DELETED_TYPE) { $contact = null; // handle deleted events, as not existing (404 Not Found) diff --git a/addressbook/inc/class.addressbook_so.inc.php b/addressbook/inc/class.addressbook_so.inc.php index f3ba3be8be..bf45db6909 100755 --- a/addressbook/inc/class.addressbook_so.inc.php +++ b/addressbook/inc/class.addressbook_so.inc.php @@ -6,7 +6,7 @@ * @author Cornelius Weiss * @author Ralf Becker * @package addressbook - * @copyright (c) 2005-11 by Ralf Becker + * @copyright (c) 2005-12 by Ralf Becker * @copyright (c) 2005/6 by Cornelius Weiss * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ @@ -961,38 +961,54 @@ class addressbook_so } /** - * Adds a distribution list + * Get the availible distribution lists for givens users and groups * - * @param string $name list-name - * @param int $owner user- or group-id - * @param array $contacts=array() contacts to add - * @return list_id or false on error + * @param array $keys column-name => value(s) pairs, eg. array('list_uid'=>$uid) + * @param string $member_attr='contact_uid' null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute + * @return array with list_id => array(list_id,list_name,list_owner,...,'members') pairs */ - function add_list($name,$owner,$contacts=array()) + function read_lists(array $keys,$member_attr='contact_uid') { - if (!method_exists($this->somain,'add_list')) return false; + if (!method_exists($this->somain,'get_lists')) return false; - return $this->somain->add_list($name,$owner,$contacts); + return $this->somain->get_lists($keys,null,$member_attr); } /** - * Adds one contact to a distribution list + * Adds / updates a distribution list * - * @param int $contact contact_id + * @param string|array $keys list-name or array with column-name => value pairs to specify the list + * @param int $owner user- or group-id + * @param array $contacts=array() contacts to add (only for not yet existing lists!) + * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name' + * @return int|boolean integer list_id or false on error + */ + function add_list($keys,$owner,$contacts=array(),array &$data=array()) + { + if (!method_exists($this->somain,'add_list')) return false; + + return $this->somain->add_list($name,$owner,$contacts,$data); + } + + /** + * Adds contact(s) to a distribution list + * + * @param int|array $contact contact_id(s) * @param int $list list-id + * @param array $existing=null array of existing contact-id(s) of list, to not reread it, eg. array() * @return false on error */ - function add2list($contact,$list) + function add2list($contact,$list,array $existing=null) { if (!method_exists($this->somain,'add2list')) return false; - return $this->somain->add2list($contact,$list); + return $this->somain->add2list($contact,$list,$existing); } /** * Removes one contact from distribution list(s) * - * @param int $contact contact_id + * @param int|array $contact contact_id(s) * @param int $list=null list-id or null to remove from all lists * @return false on error */ diff --git a/addressbook/inc/class.addressbook_sql.inc.php b/addressbook/inc/class.addressbook_sql.inc.php index 6b8b779548..2af110c15d 100644 --- a/addressbook/inc/class.addressbook_sql.inc.php +++ b/addressbook/inc/class.addressbook_sql.inc.php @@ -5,7 +5,7 @@ * @link http://www.egroupware.org * @author Ralf Becker * @package addressbook - * @copyright (c) 2006-10 by Ralf Becker + * @copyright (c) 2006-12 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ @@ -458,101 +458,162 @@ class addressbook_sql extends so_sql_cf /** * Get the availible distribution lists for givens users and groups * - * @param array $uids user or group id's + * @param array $uids array of user or group id's for $uid_column='list_owners', or values for $uid_column, + * or whole where array: column-name => value(s) pairs + * @param string $uid_column='list_owner' column-name or null to use $uids as where array + * @param string $member_attr=null null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute * @return array with list_id => array(list_id,list_name,list_owner,...) pairs */ - function get_lists($uids) + function get_lists($uids,$uid_column='list_owner',$member_attr=null) { $user = $GLOBALS['egw_info']['user']['account_id']; $lists = array(); - foreach($this->db->select($this->lists_table,'*',array('list_owner'=>$uids),__LINE__,__FILE__, + foreach($this->db->select($this->lists_table,'*',$uid_column?array($uid_column=>$uids):$uids,__LINE__,__FILE__, false,'ORDER BY list_owner<>'.(int)$GLOBALS['egw_info']['user']['account_id'].',list_name') as $row) { $lists[$row['list_id']] = $row; } - //echo "

socontacts_sql::get_lists(".print_r($uids,true).")

\n"; _debug_array($lists); + if ($lists && $member_attr && in_array($member_attr,array('contact_id','contact_uid','caldav_name'))) + { + foreach($this->db->select($this->ab2list_table,"list_id,$member_attr",array('list_id'=>array_keys($lists)), + __LINE__,__FILE__,false,$member_attr=='contact_id' ? '' : + '',false,0,"JOIN $this->table_name ON $this->ab2list_table.contact_id=$this->table_name.contact_id") as $row) + { + $lists[$row['list_id']]['members'][] = $row[$member_attr]; + } + } + error_log(__METHOD__.'('.array2string($uids).", '$uid_column', '$member_attr') returning ".array2string($lists)); return $lists; } /** - * Adds a distribution list + * Adds / updates a distribution list * - * @param string $name list-name + * @param string|array $keys list-name or array with column-name => value pairs to specify the list * @param int $owner user- or group-id - * @param array $contacts=array() contacts to add - * @return int/boolean integer list_id, true if the list already exists or false on error + * @param array $contacts=array() contacts to add (only for not yet existing lists!) + * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name' + * @return int|boolean integer list_id or false on error */ - function add_list($name,$owner,$contacts=array()) + function add_list($keys,$owner,$contacts=array(),array &$data=array()) { - if (!$name || !(int)$owner) return false; + if (!$keys || !(int)$owner) return false; - if ($this->db->select($this->lists_table,'list_id',array( - 'list_name' => $name, - 'list_owner' => $owner, - ),__LINE__,__FILE__)->fetchColumn()) + if (!is_array($keys)) $keys = array('list_name' => $keys); + $keys['list_owner'] = $owner; + + if (!($list_id = $this->select($this->lists_table,'list_id',$keys)->fetchColumn())) { - return true; // return existing list-id + $data['list_created'] = time(); + $data['list_creator'] = $GLOBALS['egw_info']['user']['account_id']; } - if (!$this->db->insert($this->lists_table,array( - 'list_name' => $name, - 'list_owner' => $owner, - 'list_created' => time(), - 'list_creator' => $GLOBALS['egw_info']['user']['account_id'], - ),array(),__LINE__,__FILE__)) return false; - - if ((int)($list_id = $this->db->get_last_insert_id($this->lists_table,'list_id')) && $contacts) + else { - foreach($contacts as $contact) + $data[] = 'list_etag=list_etag+1'; + } + $data['list_modified'] = time(); + $data['list_modifier'] = $GLOBALS['egw_info']['user']['account_id']; + + if (!$this->db->insert($this->lists_table,$data,$keys,__LINE__,__FILE__)) return false; + + if (!$list_id && ($list_id = $this->db->get_last_insert_id($this->lists_table,'list_id')) && + (!isset($data['list_uid']) || !isset($data['list_carddav_name']))) + { + $update = array(); + if (!isset($data['list_uid'])) { - $this->add2list($list_id,$contact); + $update['list_uid'] = $data['list_uid'] = common::generate_uid('addresbook-lists', $list_idD); } + if (!isset($data['list_carddav_name'])) + { + $update['list_carddav_name'] = $data['list_carddav_name'] = $data['list_uid'].'.vcf'; + } + $this->db->update($this->lists_table,$update,array('list_id'=>$list_id)); + + $this->add2list($list_id,$contacts,array()); } + $data += $keys; + return $list_id; } /** - * Adds one contact to a distribution list + * Adds contact(s) to a distribution list * - * @param int $contact contact_id + * @param int|array $contact contact_id(s) * @param int $list list-id + * @param array $existing=null array of existing contact-id(s) of list, to not reread it, eg. array() * @return false on error */ - function add2list($contact,$list) + function add2list($contact,$list,array $existing=null) { - if (!(int)$list || !(int)$contact) return false; + if (!(int)$list || !is_array($contact) && !(int)$contact) return false; - if ($this->db->select($this->ab2list_table,'list_id',array( - 'contact_id' => $contact, - 'list_id' => $list, - ),__LINE__,__FILE__)->fetchColumn()) + if (!is_array($existing)) $existing = $this->read_list($list); + if (!($to_add = array_diff((array)$contact,$existing))) { return true; // no need to insert it, would give sql error } - return $this->db->insert($this->ab2list_table,array( - 'contact_id' => $contact, + foreach($to_add as $contact) + { + $this->db->insert($this->ab2list_table,array( + 'contact_id' => $contact, + 'list_id' => $list, + 'list_added' => time(), + 'list_added_by' => $GLOBALS['egw_info']['user']['account_id'], + ),array(),__LINE__,__FILE__); + } + // update etag + return $this->db->update($this->list_table,array( + 'list_etag=list_etag+1', + 'list_modified' => time(), + 'list_modifier' => $GLOBALS['egw_info']['user']['account_id'], + ),array( 'list_id' => $list, - 'list_added' => time(), - 'list_added_by' => $GLOBALS['egw_info']['user']['account_id'], - ),array(),__LINE__,__FILE__); + ),__LINE__,__FILE__); } /** * Removes one contact from distribution list(s) * - * @param int $contact contact_id + * @param int|array $contact contact_id(s) * @param int $list=null list-id or null to remove from all lists * @return false on error */ function remove_from_list($contact,$list=null) { - if (!(int)$list && !is_null($list) || !(int)$contact) return false; + if (!(int)$list && !is_null($list) || !is_array($contact) && !(int)$contact) return false; $where = array( 'contact_id' => $contact, ); - if (!is_null($list)) $where['list_id'] = $list; - - return $this->db->delete($this->ab2list_table,$where,__LINE__,__FILE__); + if (!is_null($list)) + { + $where['list_id'] = $list; + } + else + { + $list = array(); + foreach($this->db->select($this->ab2list_table,'list_id',$where,__LINE__,__FILE__) as $row) + { + $list[] = $row['list_id']; + } + } + if (!$this->db->delete($this->ab2list_table,$where,__LINE__,__FILE__)) + { + return false; + } + foreach((array)$list as $list_id) + { + $this->db->update($this->list_table,array( + 'list_etag=list_etag+1', + 'list_modified' => time(), + 'list_modifier' => $GLOBALS['egw_info']['user']['account_id'], + ),array( + 'list_id' => $list_id, + ),__LINE__,__FILE__); + } + return true; } /** diff --git a/addressbook/inc/class.addressbook_vcal.inc.php b/addressbook/inc/class.addressbook_vcal.inc.php index 511561a00e..001a888f0d 100644 --- a/addressbook/inc/class.addressbook_vcal.inc.php +++ b/addressbook/inc/class.addressbook_vcal.inc.php @@ -72,6 +72,7 @@ class addressbook_vcal extends addressbook_bo 'X-ASSISTANT' => array('assistent'), 'X-ASSISTANT-TEL' => array('tel_assistent'), 'UID' => array('uid'), + 'REV' => array('modified'), ); /** @@ -206,7 +207,7 @@ class addressbook_vcal extends addressbook_bo $vCard->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['addressbook']['version'].'//'. strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang'])); - $sysCharSet = $GLOBALS['egw']->translation->charset(); + $sysCharSet = translation::charset(); // KAddressbook and Funambol4BlackBerry always requires non-ascii chars to be qprint encoded. if ($this->productName == 'kde' || @@ -280,6 +281,10 @@ class addressbook_vcal extends addressbook_bo switch ($databaseField) { + case 'modified': + $value = gmdate("Y-m-d\TH:i:s\Z",egw_time::user2server($value)); + break; + case 'private': $value = $value ? 'PRIVATE' : 'PUBLIC'; $hasdata++; @@ -326,7 +331,7 @@ class addressbook_vcal extends addressbook_bo case 'cat_id': if (!empty($value) && ($values = $this->get_categories($value))) { - $values = (array) $GLOBALS['egw']->translation->convert($values, $sysCharSet, $_charset); + $values = (array) translation::convert($values, $sysCharSet, $_charset); $value = implode(',', $values); // just for the CHARSET recognition if (($size > 0) && strlen($value) > $size) { @@ -410,7 +415,7 @@ class addressbook_vcal extends addressbook_bo || in_array($vcardField,array('FN','ORG','N')) || ($size >= 0 && !$noTruncate)) { - $value = $GLOBALS['egw']->translation->convert(trim($value), $sysCharSet, $_charset); + $value = translation::convert(trim($value), $sysCharSet, $_charset); $values[] = $value; if (preg_match('/[^\x20-\x7F]/', $value)) { @@ -965,6 +970,21 @@ class addressbook_vcal extends addressbook_bo } } } + // add unsupported attributes as with '##' prefix + else + { + $attribute = $vcardValues[$vcardKey]; + // for attributes with multiple values in multiple lines, merge the values + if (isset($contact['##'.$attribute['name']])) + { + error_log(__METHOD__."() taskData['##$attribute[name]'] = ".array2string($contact['##'.$attribute['name']])); + $attribute['values'] = array_merge( + is_array($contact['##'.$attribute['name']]) ? $contact['##'.$attribute['name']]['values'] : (array)$contact['##'.$attribute['name']], + $attribute['values']); + } + $contact['##'.$attribute['name']] = $attribute['params'] || count($attribute['values']) > 1 ? + serialize($attribute) : $attribute['value']; + } } $this->fixup_contact($contact); @@ -1011,8 +1031,37 @@ class addressbook_vcal extends addressbook_bo if (!$file) { - $GLOBALS['egw']->common->egw_exit(); + common::egw_exit(); } return true; } + + /** + * return a groupVCard + * + * @param array $list values for 'list_uid', 'list_name', 'list_modified', 'members' + * @param string $version='3.0' vcard version + * @return string containing the vcard + */ + function getGroupVCard(array $list,$version='3.0') + { + require_once(EGW_SERVER_ROOT.'/phpgwapi/inc/horde/Horde/iCalendar/vcard.php'); + + $vCard = new Horde_iCalendar_vcard($version); + $vCard->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['addressbook']['version'].'//'. + strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang'])); + + $vCard->setAttribute('N',$list['list_name']); + $vCard->setAttribute('FN',$list['list_name']); + + $vCard->setAttribute('X-ADRESSBOOKSERVER-KIND','group'); + foreach($list['members'] as $uid) + { + $vCard->setAttribute('X-ADRESSBOOKSERVER-MEMBER','urn:uuid:'.$uid); + } + $vCard->setAttribute('REV',egw_time::to($list['list_modified'],'Y-m-d\TH:i:s\Z')); + $vCard->setAttribute('UID',$list['list_uid']); + + return $vCard->exportvCalendar(); + } }