first step for CalDAV scheduling

- principal reports scheduling-inbox-URL /<username>/inbox/ and scheduling-outbox-URL /<username>/outbox/
- outbox collection contains no events
- outbox correctly answers POST for freebusy information
- outbox respons to all other POST with "204 No Content", ignore client request to deliver invitations
- inbox collection contains events of unknown status (PARTSTAT=NEEDS-ACTION)
- inbox responds to DELETE with "200 Ok"
--> iCal under OS X now shows freebusy times :-)
(had to add "write-content" privilege for calendar collections user has edit rights for, to allow adding events)
This commit is contained in:
Ralf Becker 2011-09-22 15:22:52 +00:00
parent 7297e02f39
commit c26fcffda7
5 changed files with 202 additions and 128 deletions

View File

@ -142,6 +142,16 @@ class calendar_groupdav extends groupdav_handler
{
$filter['filter'] = 'owner';
}
// scheduling inbox, shows only not yet accepted or rejected events
elseif (substr($path,-7) == '/inbox/')
{
$filter['filter'] = 'unknown';
}
// ToDo: not sure what scheduling outbox is supposed to show, leave it empty for now
elseif (substr($path,-8) == '/outbox/')
{
return true;
}
else
{
$filter['filter'] = 'default'; // not rejected
@ -318,6 +328,7 @@ class calendar_groupdav extends groupdav_handler
$cal_filters['end'] = $cal_end;
}
}
// multiget or propfind on a given id
//error_log(__FILE__ . __METHOD__ . "multiget of propfind:");
if ($options['root']['name'] == 'calendar-multiget' || $id)
@ -600,30 +611,51 @@ class calendar_groupdav extends groupdav_handler
{
if ($this->debug) error_log(__METHOD__."($id, $user)".print_r($options,true));
if (preg_match('/^METHOD:(PUBLISH|REQUEST)(\r\n|\r|\n)(.*)^BEGIN:VEVENT/ism', $options['content']))
$vCalendar = htmlspecialchars_decode($options['content']);
$charset = null;
if (!empty($options['content_type']))
{
$handler = $this->_get_handler();
$vCalendar = htmlspecialchars_decode($options['content']);
$charset = null;
if (!empty($options['content_type']))
$content_type = explode(';', $options['content_type']);
if (count($content_type) > 1)
{
$content_type = explode(';', $options['content_type']);
if (count($content_type) > 1)
array_shift($content_type);
foreach ($content_type as $attribute)
{
array_shift($content_type);
foreach ($content_type as $attribute)
trim($attribute);
list($key, $value) = explode('=', $attribute);
switch (strtolower($key))
{
trim($attribute);
list($key, $value) = explode('=', $attribute);
switch (strtolower($key))
{
case 'charset':
$charset = strtoupper(substr($value,1,-1));
}
case 'charset':
$charset = strtoupper(substr($value,1,-1));
}
}
}
}
if (substr($options['path'],-8) == '/outbox/')
{
if (preg_match('/^METHOD:REQUEST(\r\n|\r|\n)(.*)^BEGIN:VFREEBUSY/ism', $vCalendar))
{
if ($user != $GLOBALS['egw_info']['user']['account_id'])
{
error_log(__METHOD__."() freebusy request only allowed to own outbox!");
return '403 Forbidden';
}
// do freebusy request
return $this->outbox_freebusy_request($vCalendar, $charset, $user, $options);
}
else
{
// POST to deliver an invitation, containing http headers:
// Originator: mailto:<organizer-email>
// Recipient: mailto:<attendee-email>
// --> currently we simply ignore these posts, as EGroupware does it's own notifications based on user preferences
return '204 No Content';
}
}
if (preg_match('/^METHOD:(PUBLISH|REQUEST)(\r\n|\r|\n)(.*)^BEGIN:VEVENT/ism', $options['content']))
{
$handler = $this->_get_handler();
if (($foundEvents = $handler->search($vCalendar, null, false, $charset)))
{
$eventId = array_shift($foundEvents);
@ -640,6 +672,85 @@ class calendar_groupdav extends groupdav_handler
return true;
}
/**
* Handle outbox freebusy request
*
* @param string $ical
* @param string $charset of ical
* @param int $user account_id of owner
* @param array &$options
* @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found')
*/
protected function outbox_freebusy_request($ical, $charset, $user, array &$options)
{
include_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php';
$vcal = new Horde_iCalendar();
if (!$vcal->parsevCalendar($ical, 'VCALENDAR', $charset))
{
return '400 Bad request';
}
$version = $vcal->getAttribute('VERSION');
//echo $ical."\n";
$handler = $this->_get_handler();
$handler->setSupportedFields('groupdav');
$handler->calendarOwner = $handler->user = 0; // to NOT default owner/organizer to something
if (!($component = $vcal->getComponent(0)) ||
!($event = $handler->vevent2egw($component, $version, $handler->supportedFields, $this->groupdav->current_user_principal, 'Horde_iCalendar_vfreebusy')))
{
return '400 Bad request';
}
if ($event['owner'] != $user)
{
error_log(__METHOD__."('$ical',,$user) ORGANIZER is NOT principal!");
return '403 Forbidden';
}
//print_r($event);
$organizer = $component->getAttribute('ORGANIZER');
$attendees = $component->getAttribute('ATTENDEE');
// X-CALENDARSERVER-MASK-UID specifies to exclude given event from busy-time
$mask_uid = $component->getAttribte('X-CALENDARSERVER-MASK-UID');
header('Content-type: text/xml; charset=UTF-8');
$xml = new XMLWriter;
$xml->openMemory();
$xml->startDocument('1.0', 'UTF-8');
$xml->startElementNs('C', 'schedule-response', groupdav::CALDAV);
foreach($event['participants'] as $uid => $status)
{
$xml->startElementNs('C', 'response', null);
$xml->startElementNs('C', 'recipient', null);
$xml->writeElementNs('D', 'href', 'DAV:', $attendee=array_shift($attendees));
$xml->endElement(); // recipient
if (is_numeric($uid))
{
$xml->writeElementNs('C', 'request-status', null, '2.0;Success');
$xml->writeElementNs('C', 'calendar-data', null, str_replace("\r", '', // CalDAV rfc example has no encoded "\r"
$handler->freebusy($uid, $event['end'], true, 'utf-8', $event['start'], 'REPLY', array(
'UID' => $event['uid'],
'ORGANIZER' => $organizer,
'ATTENDEE' => $attendee,
'X-CALENDARSERVER-MASK-UID' => $mask_uid,
))));
}
else
{
$xml->writeElementNs('C', 'request-status', null, '3.7;Invalid calendar user');
}
$xml->endElement(); // response
}
$xml->endElement(); // schedule-response
$xml->endDocument();
echo $xml->outputMemory();
return true;
}
/**
* Fix event series with exceptions, called by calendar_ical::importVCal():
* a) only series master = first event got cal_id from URL
@ -725,6 +836,10 @@ class calendar_groupdav extends groupdav_handler
*/
function delete(&$options,$id)
{
if (strpos($options['path'], '/inbox/') !== false)
{
return true; // simply ignore DELETE in inbox for now
}
$return_no_access = true; // to allow to check if current use is a participant and reject the event for him
if (!is_array($event = $this->_common_get_put_delete('DELETE',$options,$id,$return_no_access)) || !$return_no_access)
{
@ -815,35 +930,6 @@ class calendar_groupdav extends groupdav_handler
return $this->bo->check_perms($acl,$event,0,'server');
}
/**
* Add the privileges of the current user
*
* @param array $props=array() regular props by the groupdav handler
* @return array
*/
static function current_user_privilege_set(array $props=array())
{
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::DAV,'current-user-privilege-set',
array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'privilege',
array(
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'read',''),
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'read-free-busy',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'read-current-user-privilege-set',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'bind',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'unbind',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-post',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-post-vevent',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-respond',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-respond-vevent',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-deliver',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-deliver-vevent',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'write',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'write-properties',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'write-content',''),
))));
return $props;
}
/**
* Add extra properties for calendar collections
*
@ -855,45 +941,26 @@ class calendar_groupdav extends groupdav_handler
static function extra_properties(array $props=array(), $displayname, $base_uri=null)
{
// calendar description
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-description',$displayname);
/*
// BOX URLs of the current user
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'schedule-inbox-URL',
array(HTTP_WebDAV_Server::mkprop(self::DAV,'href',$base_uri.'/calendar/')));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'schedule-outbox-URL',
array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'href',$base_uri.'/calendar/')));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'schedule-default-calendar-URL',
array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'href',$base_uri.'/calendar/')));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'dropbox-home-URL',
array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'href',$base_uri.'/calendar/')));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'notifications-URL',
array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'href',$base_uri.'/calendar/')));
*/
$props['calendar-description'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-description',$displayname);
// email of the current user, see caldav-sheduling draft
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-address-set',array(
$props['calendar-user-address-set'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-address-set',array(
HTTP_WebDAV_Server::mkprop('href','MAILTO:'.$GLOBALS['egw_info']['user']['email']),
HTTP_WebDAV_Server::mkprop('href',$base_uri.'/principals/users/'.$GLOBALS['egw_info']['user']['account_lid'].'/'),
HTTP_WebDAV_Server::mkprop('href','urn:uuid:'.$GLOBALS['egw_info']['user']['account_lid'])));
//$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-address-set',array(
// HTTP_WebDAV_Server::mkprop('href','MAILTO:'.$GLOBALS['egw_info']['user']['email'])));
// supported components, currently only VEVENT
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-component-set',array(
$props['supported-calendar-component-set'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-component-set',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VCALENDAR')),
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VTIMEZONE')),
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VEVENT')),
// HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VTODO')), // not yet supported
));
$props[] = HTTP_WebDAV_Server::mkprop('supported-report-set',array(
$props['supported-report-set'] = HTTP_WebDAV_Server::mkprop('supported-report-set',array(
HTTP_WebDAV_Server::mkprop('supported-report',array(
HTTP_WebDAV_Server::mkprop('report',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-multiget','')))))));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-data',array(
$props['supported-calendar-data'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-data',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data', array('content-type' => 'text/calendar', 'version'=> '2.0')),
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data', array('content-type' => 'text/x-calendar', 'version'=> '1.0'))));
//$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'publish-url',array(
// HTTP_WebDAV_Server::mkprop('href',$base_uri.'/calendar/')));
//$props = self::current_user_privilege_set($props);
return $props;
}

View File

@ -966,12 +966,12 @@ class calendar_ical extends calendar_boupdate
{
foreach (is_array($value) && $parameters[$key]['VALUE']!='DATE' ? $value : array($value) as $valueID => $valueData)
{
$valueData = $GLOBALS['egw']->translation->convert($valueData,$GLOBALS['egw']->translation->charset(),$charset);
$paramData = (array) $GLOBALS['egw']->translation->convert(is_array($value) ?
$valueData = translation::convert($valueData,translation::charset(),$charset);
$paramData = (array) translation::convert(is_array($value) ?
$parameters[$key][$valueID] : $parameters[$key],
$GLOBALS['egw']->translation->charset(),$charset);
$valuesData = (array) $GLOBALS['egw']->translation->convert($values[$key],
$GLOBALS['egw']->translation->charset(),$charset);
translation::charset(),$charset);
$valuesData = (array) translation::convert($values[$key],
translation::charset(),$charset);
$content = $valueData . implode(';', $valuesData);
if (preg_match('/[^\x20-\x7F]/', $content) ||
@ -2233,12 +2233,13 @@ class calendar_ical extends calendar_boupdate
* @param string $version vCal version (1.0/2.0)
* @param array $supportedFields supported fields of the device
* @param string $principalURL='' Used for CalDAV imports
* @param string $check_component='Horde_iCalendar_vevent'
*
* @return array|boolean event on success, false on failure
*/
function vevent2egw(&$component, $version, $supportedFields, $principalURL='')
function vevent2egw(&$component, $version, $supportedFields, $principalURL='', $check_component='Horde_iCalendar_vevent')
{
if (!is_a($component, 'Horde_iCalendar_vevent'))
if ($check_component && !is_a($component, $check_component))
{
if ($this->log)
{
@ -3034,52 +3035,47 @@ class calendar_ical extends calendar_boupdate
* @param mixed $end=null end-date, default now+1 month
* @param boolean $utc=true if false, use severtime for dates
* @param string $charset='UTF-8' encoding of the vcalendar, default UTF-8
* @return string
* @param mixed $start=null default now
* @param string $method='PUBLISH' or eg. 'REPLY'
* @param array $extra=null extra attributes to add
* X-CALENDARSERVER-MASK-UID can be used to not include an event specified by this uid as busy
*/
function freebusy($user,$end=null,$utc=true, $charset='UTF-8')
function freebusy($user,$end=null,$utc=true, $charset='UTF-8', $start=null, $method='PUBLISH', array $extra=null)
{
if (!$end) $end = $this->now_su + 100*DAY_s; // default next 100 days
if (!$start) $start = time(); // default now
if (!$end) $end = time() + 100*DAY_s; // default next 100 days
$vcal = new Horde_iCalendar;
$vcal->setAttribute('PRODID','-//eGroupWare//NONSGML eGroupWare Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
$vcal->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
$vcal->setAttribute('VERSION','2.0');
$vcal->setAttribute('METHOD',$method);
$vfreebusy = Horde_iCalendar::newComponent('VFREEBUSY',$vcal);
$parameters = array(
'ORGANIZER' => $GLOBALS['egw']->translation->convert(
$GLOBALS['egw']->accounts->id2name($user,'account_firstname').' '.
$GLOBALS['egw']->accounts->id2name($user,'account_lastname'),
$GLOBALS['egw']->translation->charset(),$charset),
if ($uid) $vfreebusy->setAttribute('UID', $uid);
$attributes = array(
'DTSTAMP' => time(),
'DTSTART' => $this->date2ts($start,true), // true = server-time
'DTEND' => $this->date2ts($end,true), // true = server-time
);
if ($utc)
if (!$utc)
{
foreach (array(
'URL' => $this->freebusy_url($user),
'DTSTART' => $this->date2ts($this->now_su,true), // true = server-time
'DTEND' => $this->date2ts($end,true), // true = server-time
'ORGANIZER' => $GLOBALS['egw']->accounts->id2name($user,'account_email'),
'DTSTAMP' => time(),
) as $attr => $value)
foreach ($attributes as $attr => $value)
{
$vfreebusy->setAttribute($attr, $value);
$attributes[$attr] = date('Ymd\THis', $value);
}
}
else
if (is_null($extra)) $extra = array(
'URL' => $this->freebusy_url($user),
'ORGANIZER' => 'mailto:'.$GLOBALS['egw']->accounts->id2name($user,'account_email'),
);
foreach($attributes+$extra as $attr => $value)
{
foreach (array(
'URL' => $this->freebusy_url($user),
'DTSTART' => date('Ymd\THis',$this->date2ts($this->now_su,true)), // true = server-time
'DTEND' => date('Ymd\THis',$this->date2ts($end,true)), // true = server-time
'ORGANIZER' => $GLOBALS['egw']->accounts->id2name($user,'account_email'),
'DTSTAMP' => date('Ymd\THis',time()),
) as $attr => $value)
{
$vfreebusy->setAttribute($attr, $value);
}
$vfreebusy->setAttribute($attr, $value);
}
$fbdata = parent::search(array(
'start' => $this->now_su,
'start' => $start,
'end' => $end,
'users' => $user,
'date_format' => 'server',
@ -3090,6 +3086,7 @@ class calendar_ical extends calendar_boupdate
foreach ($fbdata as $event)
{
if ($event['non_blocking']) continue;
if ($event['uid'] === $extra['X-CALENDARSERVER-MASK-UID']) continue;
if ($utc)
{

View File

@ -92,16 +92,16 @@ class groupdav extends HTTP_WebDAV_Server
'resourcetype' => array(self::GROUPDAV => 'vevent-collection', self::CALDAV => 'calendar'),
'component-set' => array(self::GROUPDAV => 'VEVENT'),
),
/*'inbox' => array(
'inbox' => array(
'resourcetype' => array(self::CALDAV => 'schedule-inbox'),
'app' => 'calendar',
'no-root' => true,
'user-only' => true, // display just in user home
),
'outbox' => array(
'resourcetype' => array(self::CALDAV => 'schedule-outbox'),
'app' => 'calendar',
'no-root' => true,
),*/
'user-only' => true, // display just in user home
),
'infolog' => array(
'resourcetype' => array(self::GROUPDAV => 'vtodo-collection', self::CALDAV => 'calendar'),
'component-set' => array(self::GROUPDAV => 'VTODO'),
@ -265,7 +265,7 @@ class groupdav extends HTTP_WebDAV_Server
if (!in_array(2,$dav)) $dav[] = 2;
$dav[] = 'access-control';
$dav[] = 'calendar-access';
//$dav[] = 'calendar-auto-schedule';
$dav[] = 'calendar-auto-schedule';
$dav[] = 'calendar-proxy';
//$dav[] = 'calendar-availibility';
//$dav[] = 'calendarserver-private-events';
@ -546,7 +546,7 @@ class groupdav extends HTTP_WebDAV_Server
foreach($this->root as $app => $data)
{
if (!$GLOBALS['egw_info']['user']['apps'][$data['app'] ? $data['app'] : $app]) continue; // no rights for the given app
if ($path == '/' && !empty($data['no-root'])) continue;
if (!empty($data['user-only']) && ($path == '/' || $user < 0)) continue;
$files['files'][] = $this->add_app($app,false,$user,$path);
}
@ -593,8 +593,7 @@ class groupdav extends HTTP_WebDAV_Server
}
$account = $this->accounts->read($account_lid);
$displayname = $GLOBALS['egw']->translation->convert($account['account_fullname'],
$GLOBALS['egw']->translation->charset(),'utf-8');
$displayname = translation::convert($account['account_fullname'],translation::charset(),'utf-8');
if ($user < 0)
{
@ -655,7 +654,7 @@ class groupdav extends HTTP_WebDAV_Server
}
break;
case 'app':
case 'no-root':
case 'user-only':
break; // no props, already handled
default:
if (is_array($values))
@ -888,31 +887,30 @@ class groupdav extends HTTP_WebDAV_Server
$arr = array();
foreach($props as $prop)
{
$ns_hash = array('DAV:' => 'D');
switch($prop['ns'])
{
case 'DAV:';
$ns = 'DAV';
break;
case self::CALDAV:
$ns = 'CalDAV';
$ns = $ns_hash[$prop['ns']] = 'CalDAV';
break;
case self::CARDDAV:
$ns = 'CardDAV';
$ns = $ns_hash[$prop['ns']] = 'CardDAV';
break;
case self::GROUPDAV:
$ns = 'GroupDAV';
$ns = $ns_hash[$prop['ns']] = 'GroupDAV';
break;
default:
$ns = $prop['ns'];
}
$ns_defs = '';
$ns_hash = array($prop['ns'] => $ns, 'DAV:' => 'D');
if (is_array($prop['val']))
{
$prop['val'] = $this->_hierarchical_prop_encode($prop['val'], $prop['ns'], $ns_defs, $ns_hash);
$prop['val'] = $this->_hierarchical_prop_encode($prop['val'], $prop['ns'], $ns_defs='', $ns_hash);
// hack to show real namespaces instead of not (visibly) defined shortcuts
unset($ns_hash['DAV:']);
$value = strtr($this->prop_value($prop['val']),array_flip($ns_hash));
$value = strtr($v=$this->prop_value($prop['val']),array_flip($ns_hash));
}
else
{

View File

@ -412,6 +412,10 @@ abstract class groupdav_handler
$priviledes[] = 'bind'; // PUT for new resources
}
if (!$user || $grants[$user] & EGW_ACL_EDIT)
{
$priviledes[] = 'write-content'; // otherwise iOS calendar does not allow to add events
}
if (!$user || $grants[$user] & EGW_ACL_DELETE)
{
$priviledes[] = 'unbind'; // DELETE
}

View File

@ -368,24 +368,32 @@ class groupdav_principals extends groupdav_handler
return $this->add_principal('users/'.$account['account_lid'], array(
'getetag' => $this->get_etag($account),
'displayname' => $displayname,
'alternate-URI-set' => array(
HTTP_WebDAV_Server::mkprop('href','MAILTO:'.$account['account_email'])),
// CalDAV
'calendar-home-set' => HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-home-set',$calendars),
// CalDAV scheduling
'schedule-outbox-URL' => HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'schedule-outbox-URL',array(
HTTP_WebDAV_Server::mkprop('href',$this->base_uri.'/'.$account['account_lid'].'/outbox/'))),
'schedule-inbox-URL' => HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'schedule-inbox-URL',array(
HTTP_WebDAV_Server::mkprop('href',$this->base_uri.'/'.$account['account_lid'].'/inbox/'))),
'calendar-user-address-set' => HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-address-set',array(
HTTP_WebDAV_Server::mkprop('href','MAILTO:'.$account['account_email']),
HTTP_WebDAV_Server::mkprop('href',$this->base_uri.'/principals/users/'.$account['account_lid'].'/'),
HTTP_WebDAV_Server::mkprop('href','urn:uuid:'.$account['account_lid']))),
'schedule-outbox-URL' => HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'schedule-outbox-URL',array(
HTTP_WebDAV_Server::mkprop('href',$this->base_uri.'/calendar/'))),
'calendar-user-type' => HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-type','INDIVIDUAL'),
// Calendarserver
'email-address-set' => HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'email-address-set',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'email-address',$account['account_email']))),
'last-name' => HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'last-name',$account['account_lastname']),
'first-name' => HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'first-name',$account['account_firstname']),
'record-type' => HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'record-type','user'),
'calendar-user-type' => HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-type','INDIVIDUAL'),
'addressbook-home-set' => HTTP_WebDAV_Server::mkprop(groupdav::CARDDAV,'addressbook-home-set',$addressbooks),
// WebDAV ACL and CalDAV proxy
'group-membership' => $this->principal_set('group-membership', $this->accounts->memberships($account['account_id']),
'calendar', $account['account_id']), // add proxy-rights
'alternate-URI-set' => array(
HTTP_WebDAV_Server::mkprop('href','MAILTO:'.$account['account_email'])),
// CardDAV
'addressbook-home-set' => HTTP_WebDAV_Server::mkprop(groupdav::CARDDAV,'addressbook-home-set',$addressbooks),
// CardDAV directory
'directory-gateway' => HTTP_WebDAV_Server::mkprop(groupdav::CARDDAV, 'directory-gateway',array(
HTTP_WebDAV_Server::mkprop('href', $this->base_uri.'/addressbook/'))),
));