* Calendar: new REST API to read, create, modify and delete events, see https://github.com/EGroupware/egroupware/blob/master/doc/REST-CalDAV-CardDAV/Calendar.md

This commit is contained in:
ralf 2023-07-24 17:08:05 +02:00
parent f04b25089a
commit b013f75eef
7 changed files with 1083 additions and 538 deletions

View File

@ -33,16 +33,17 @@ class JsCalendar
*
* @param int|array $event
* @param bool|"pretty" $encode=true true: JSON encode, "pretty": JSON encode with pretty-print, false: return raw data e.g. from listing
* @param ?array $exceptions=null
* @return string|array
* @throws Api\Exception\NotFound
*/
public static function getJsEvent($event, $encode=true)
public static function JsEvent($event, $encode=true, array $exceptions=[])
{
if (is_scalar($event) && !($event = self::getCalendar()->read($event)))
{
throw new Api\Exception\NotFound();
}
$data = array_filter([
$data = [
self::AT_TYPE => self::TYPE_EVENT,
'prodId' => 'EGroupware Calendar '.$GLOBALS['egw_info']['apps']['api']['version'],
'uid' => self::uid($event['uid']),
@ -54,16 +55,25 @@ class JsCalendar
'timeZone' => $event['tzid'],
'showWithoutTime' => $event['whole_day'],
'duration' => self::Duration($event['start'], $event['end'], $event['whole_day']),
'recurrenceRules' => null,
'recurrenceOverrides' => null,
'freeBusyStatus' => $event['non_blocking'] ? 'free' : null, // default is busy
'description' => $event['description'],
'participants' => self::Participants($event),
'alerts' => self::Alerts($event['alarm']),
'status' => empty($event['deleted']) ? 'confirmed' : 'cancelled', // we have no "tentative" event-status (only participants)!
'priority' => self::Priority($event['priority']),
'categories' => self::categories($event['category']),
'privacy' => $event['public'] ? 'public' : 'private',
'alerts' => self::Alerts($event['alarms']),
'egroupware.org:customfields' => self::customfields($event),
]+self::Locations($event)+self::Recurrence($event));
] + self::Locations($event);
if (!empty($event['recur_type']))
{
$data = array_merge($data, self::Recurrence($event, $data, $exceptions));
}
$data = array_filter($data);
if ($encode)
{
return Api\CalDAV::json_encode($data, $encode === "pretty");
@ -149,7 +159,13 @@ class JsCalendar
break;
case 'alerts':
$event['alarms'] = self::parseAlerts($value);
throw new \Exception('Creating or modifying alerts is NOT (yet) implemented!');
break;
case 'recurrenceRules':
case 'recurrenceOverrides':
case 'excludedRecurrenceRules':
throw new \Exception('Creating or modifying recurring events is NOT (yet) implemented!');
break;
case 'categories':
@ -174,6 +190,13 @@ class JsCalendar
catch (\Throwable $e) {
self::handleExceptions($e, 'JsCalendar Event', $name, $value);
}
// if no participant given add current user as CHAIR to the event
if (empty($event['participants']))
{
$event['participants'][$GLOBALS['egw_info']['user']['account_id']] = 'ACHAIR';
}
return $event;
}
@ -538,13 +561,15 @@ class JsCalendar
$info = self::getCalendar()->resource_info($uid) ?: [];
switch($info['type'] ?? $info['app'])
{
case 'user':
case 'e': // email
case 'c': // contact
case 'u': // user
$info['kind'] = 'individual';
break;
case 'group':
case 'g':
$info['kind'] = 'group';
break;
case 'resources':
case 'r':
$info['kind'] = Api\CalDAV\Principals::resource_is_location($user_id) ? 'location' : 'resource';
break;
}
@ -552,7 +577,7 @@ class JsCalendar
catch (\Exception $e) {
$info = [];
}
$participant = [
$participant = array_filter([
self::AT_TYPE => self::TYPE_PARTICIPANT,
'name' => $info['name'] ?? null,
'email' => $info['email'] ?? null,
@ -565,7 +590,7 @@ class JsCalendar
'informational' => $role === 'NON-PARTICIPANT',
]),
'participationStatus' => $status2jscal[$status],
];
]);
$participants[$uid] = $participant;
}
@ -612,22 +637,122 @@ class JsCalendar
return $priority_egw2jscal[$priority];
}
const TYPE_RECURRENCE_RULE = 'RecurrenceRule';
const TYPE_NDAY = 'NDay';
/**
* Return recurrence properties: recurrenceId, recurrenceRules, recurrenceOverrides, ...
*
* @TODO
* EGroupware only supports a subset of iCal recurrence rules (e.g. only byDay and byMonthDay, no other by-types)!
*
* @param array $event
* @param array $data JSCalendar representation of event to calculate overrides
* @param array $exceptions exceptions
* @return array
*/
protected static function Recurrence(array $event)
protected static function Recurrence(array $event, array $data, array $exceptions=[])
{
return [];
if (empty($event['recur_type']))
{
return []; // non-recurring event
}
$rriter = \calendar_rrule::event2rrule($event, false);
$rrule = $rriter->generate_rrule('2.0');
$rule = array_filter([
self::AT_TYPE => self::TYPE_RECURRENCE_RULE,
'frequency' => strtolower($rrule['FREQ']),
'interval' => $rrule['INTERVAL'] ?? null,
'until' => empty($rrule['UNTIL']) ? null : self::DateTime($rrule['UNTIL'], $event['tzid']),
]);
if (!empty($GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts']) &&
$GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts'] !== 'Monday')
{
$rule['firstDayOfWeek'] = strtolower(substr($GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts'], 0, 2));
}
if (!empty($rrule['BYDAY']))
{
$rule['byDay'] = array_filter([
self::AT_TYPE => self::TYPE_NDAY,
'day' => strtolower(substr($rrule['BYDAY'], $rriter->monthly_byday_num ? strlen((string)$rriter->monthly_byday_num) : 0)),
'nthOfPeriod' => $rriter->monthly_byday_num,
]);
}
elseif (!empty($rrule['BYMONTHDAY']))
{
$rule['byMonthDay'] = [$rrule['BYMONTHDAY']]; // EGroupware supports only a single day!
}
$overrides = [];
// adding excludes to the overrides
if (!empty($event['recur_exception']))
{
foreach ($event['recur_exception'] as $timestamp)
{
$ex_date = new Api\DateTime($timestamp, Api\DateTime::$server_timezone);
if (!empty($event['whole_day']))
{
$ex_date->setTime(0, 0, 0);
}
$overrides[self::DateTime($ex_date, $event['tzid'])] = [
'excluded' => true,
];
}
}
// adding exceptions to the overrides
foreach($exceptions as $exception)
{
$overrides[self::DateTime($exception['recurrence'], $event['tzid'])] = self::getPatch(self::JsEvent($exception, false), $data);
}
return array_filter([
'recurrenceRules' => [$rule],
'recurrenceOverrides' => $overrides,
]);
}
/**
* Get patch from an event / recurrence compared to the master event
*
* @param array $event
* @param array $master
* @return array with modified attributes
*/
public static function getPatch(array $event, array $master=null)
{
if (!$master)
{
return $event;
}
// array_diff_assoc only reports values changed or set in $event for scalar values
$patch = array_diff_assoc($event, $master);
// we need to report unset / removed values
foreach($master as $name => $value)
{
if (isset($value) && !isset($event[$name]))
{
$patch[$name] = null;
}
}
// for non-scalar values, we have to call ourselves recursive
foreach($event as $name => $value)
{
if (is_array($value) && ($diff = self::getPatch($event[$name], $master[$name])))
{
$patch[$name] = $diff;
}
}
return $patch;
}
const TYPE_ALERT = 'Alert';
const TYPE_OFFSET_TRIGGER = 'OffsetTrigger';
/**
* Return alerts object
*
* @TODO
* @param array|null $alarms
* @return array
*/
@ -636,7 +761,19 @@ class JsCalendar
$alerts = [];
foreach($alarms ?? [] as $alarm)
{
if (!isset($alarm['offset']) || empty($alarm['all']) && $alarm['owner'] != $GLOBALS['egw_info']['user']['account_id'])
{
continue; // do NOT show other users alarms
}
$alerts[$alarm['uid']] = array_filter([
self::AT_TYPE => self::TYPE_ALERT,
'trigger' => [
self::AT_TYPE => self::TYPE_OFFSET_TRIGGER,
'offset' => $alarm['offset'],
],
'acknowledged' => empty($alarm['attrs']['ACKNOWLEDGED']['value']) ? null :
self::UTCDateTime(new Api\DateTime($alarm['attrs']['ACKNOWLEDGED']['value'])),
]);
}
return $alerts;
}

View File

@ -1128,7 +1128,7 @@ class calendar_bo
}
/**
* Inserts all repetions of $event in the timespan between $start and $end into $events
* Inserts all repetitions of $event in the timespan between $start and $end into $events
*
* The new entries are just appended to $events, so $events is no longer sorted by startdate !!!
*
@ -2191,9 +2191,9 @@ class calendar_bo
$entry = $this->read($id, $recur_date, true, 'server');
}
$etag = $schedule_tag = $entry['id'].':'.$entry['etag'];
$etag .= ':'.$entry['modified'];
$etag .= ':'.Api\DateTime::user2server($entry['modified'], 'ts');
//error_log(__METHOD__ . "($entry[id],$client_share_uid_excpetions) entry=".array2string($entry)." --> etag=$etag");
//error_log(__METHOD__ . "($entry[id],$client_share_uid_exceptions) entry=".array2string($entry)." --> etag=$etag");
return $etag;
}

View File

@ -383,10 +383,11 @@ class calendar_groupdav extends Api\CalDAV\Handler
$events = null;
$is_jscalendar = Api\CalDAV::isJSON();
for($chunk=0; (!$chunk || count($events) === self::CHUNK_SIZE) && // stop after we have not got a full chunk
($events =& $this->bo->search($filter+[
($events =& $this->bo->search([
'offset' => $chunk*self::CHUNK_SIZE,
'num_rows' => self::CHUNK_SIZE,
])); ++$chunk)
'date_format' => $is_jscalendar ? 'object' : 'ts',
]+$filter)); ++$chunk)
{
foreach($events as $event)
{
@ -441,11 +442,10 @@ class calendar_groupdav extends Api\CalDAV\Handler
//error_log(__FILE__ . __METHOD__ . "Calendar Data : $calendar_data");
if ($calendar_data)
{
$content = $is_jscalendar ? Api\CalDAV\JsCalendar::getJsEvent($event, false) :
$this->iCal($event, $filter['users'],
$content = $this->iCal($event, $filter['users'],
strpos($path, '/inbox/') !== false ? 'REQUEST' : null,
!isset($calendar_data['children']['expand']) ? false :
($calendar_data['children']['expand']['attrs'] ?: true), $exceptions);
($calendar_data['children']['expand']['attrs'] ?: true), $exceptions, $is_jscalendar ? false : null);
$props['getcontentlength'] = bytes($is_jscalendar ? json_encode($content) : $content);
$props['calendar-data'] = Api\CalDAV::mkprop(Api\CalDAV::CALDAV,'calendar-data',$content);
}
@ -578,10 +578,12 @@ class calendar_groupdav extends Api\CalDAV\Handler
}
if ($time)
{
$props['dtstamp'] = Api\CalDAV::mkprop(Api\CalDAV::CALENDARSERVER, 'dtstamp', gmdate('Ymd\\This\\Z', $time));
$dtstamp = Api\DateTime::to($time, 'object');
$dtstamp->setTimezone(new DateTimeZone('utc'));
$props['dtstamp'] = Api\CalDAV::mkprop(Api\CalDAV::CALENDARSERVER, 'dtstamp', $dtstamp->format('Ymd\\This\\Z'));
}
//error_log(__METHOD__."($user, $time) returning ".array2string($props));
return $props ? $props : '';
return $props ?: '';
}
/**
@ -763,12 +765,12 @@ class calendar_groupdav extends Api\CalDAV\Handler
// jsEvent or iCal
if (($type=Api\CalDAV::isJSON()))
{
$options['data'] = Api\CalDAV\JsCalendar::getJsEvent($event, $type);
$options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null, false, null, $type);
$options['mimetype'] = Api\CalDAV\JsCalendar::MIME_TYPE_JSEVENT.';charset=utf-8';
}
else
{
$options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null);
$options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null, false, null);
$options['mimetype'] = 'text/calendar; charset=utf-8';
}
header('Content-Encoding: identity');
@ -784,16 +786,17 @@ class calendar_groupdav extends Api\CalDAV\Handler
/**
* Generate an iCal for the given event
*
* Taking into account virtual an real exceptions for recuring events
* Taking into account virtual and real exceptions for recurring events
*
* @param array $event
* @param int $user =null account_id of calendar to display
* @param string $method =null eg. 'PUBLISH' for inbox, nothing anywhere else
* @param string $method =null e.g. 'PUBLISH' for inbox, nothing anywhere else
* @param boolean|array $expand =false true or array with values for 'start', 'end' to expand recurrences
* @param array|null $events as returned by get_series() for !$expand (to not read them again)
* @return string
* @param bool|"pretty"|null $json null: iCal, false: return array with JSCalendar data, true+"pretty": return json-serialized JSCalendar
* @return string|array array if $json === false
*/
private function iCal(array $event,$user=null, $method=null, $expand=false, array $events=null)
private function iCal(array $event,$user=null, $method=null, $expand=false, array $events=null, $json=null)
{
static $handler = null;
if (is_null($handler)) $handler = $this->_get_handler();
@ -843,7 +846,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
// pass in original event as master, as it has correct start-date even if first recurrence is an exception
if ($expand || !isset($events))
{
$events =& self::get_series($event['uid'], $this->bo, $expand, $user, $event);
$events =& self::get_series($event['uid'], $this->bo, $expand, $user, $event, isset($json) ? 'object' : 'server');
}
// as alarm is now only on next recurrence, set alarm from original event on master
if ($event['alarm']) $events[0]['alarm'] = $event['alarm'];
@ -857,6 +860,10 @@ class calendar_groupdav extends Api\CalDAV\Handler
$events[0]['uid'] .= '-'.$event['id']; // force a different uid
}
}
if (isset($json))
{
return Api\CalDAV\JsCalendar::JsEvent($events[0], $json, array_slice($events, 1));
}
return $handler->exportVCal($events, '2.0', $method);
}
@ -872,7 +879,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
* @param array $master =null use provided event as master to fix wrong start-date if first recurrence is an exception
* @return array
*/
private static function &get_series($uid,calendar_bo $bo=null, $expand=false, $user=null, $master=null)
private static function &get_series($uid,calendar_bo $bo=null, $expand=false, $user=null, $master=null, $date_format='server')
{
if (is_null($bo)) $bo = new calendar_boupdate();
@ -880,7 +887,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
'query' => array('cal_uid' => $uid),
'filter' => 'owner', // return all possible entries
'daywise' => false,
'date_format' => 'server',
'date_format' => $date_format,
'cfs' => array(), // read cfs as we use them to store X- attributes
);
if (is_array($expand)) $params += $expand;
@ -891,7 +898,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
return $events;
}
// find master, which is not always first event, eg. when first event is an exception
// find master, which is not always first event, e.g. when first event is an exception
$exceptions = array();
foreach($events as $k => &$recurrence)
{
@ -904,7 +911,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
}
}
// if recurring event starts in future behind horizont, nothing will be returned by bo::search()
if (!isset($master)) $master = $bo->read($uid);
if (!isset($master)) $master = $bo->read($uid, null, false, $date_format);
foreach($events as $k => &$recurrence)
{
@ -912,7 +919,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
if ($master && $recurrence['reference'] && $recurrence['reference'] != $master['id'])
{
unset($events[$k]);
continue; // same uid, but references a different event or is own master
continue; // same uid, but references a different event or its own master
}
if (!$master || $recurrence['id'] != $master['id']) // real exception
{
@ -929,7 +936,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
$recurrence['participants'][$user] = 'R';
}
//error_log('real exception: '.array2string($recurrence));
// remove from masters recur_exception, as exception is include
// remove from masters recur_exception, as exception is included
// at least Lightning "understands" EXDATE as exception from what's included
// in the whole resource / VCALENDAR component
// not removing it causes Lightning to remove the exception itself
@ -1620,12 +1627,13 @@ class calendar_groupdav extends Api\CalDAV\Handler
{
if (strpos($column=self::$path_attr,'_') === false) $column = 'cal_'.$column;
$event = $this->bo->read(array($column => $id, 'cal_deleted IS NULL', 'cal_reference=0'), null, true, 'server');
$event = $this->bo->read(array($column => $id, 'cal_deleted IS NULL', 'cal_reference=0'), null, true,
$date_format = Api\CalDAV::isJSON() ? 'object' : 'server');
if ($event) $event = array_shift($event); // read with array as 1. param, returns an array of events!
if (!($retval = $this->bo->check_perms(calendar_bo::ACL_FREEBUSY,$event, 0, 'server')) &&
// above can be true, if current user is not in master but just a recurrence
(!$event['recur_type'] || !($events = self::get_series($event['uid'], $this->bo))))
(!$event['recur_type'] || !($events = self::get_series($event['uid'], $this->bo, false, null, null, $date_format))))
{
if ($this->debug > 0) error_log(__METHOD__."($id) no READ or FREEBUSY rights returning ".array2string($retval));
return $retval;

View File

@ -538,9 +538,9 @@ class calendar_rrule implements Iterator
$previous = clone $this->current;
$this->next_no_exception();
}
// if enddate is now before next acurrence, but not on same day, we use previous recurrence
// if enddate is now before next occurrence, but not on same day, we use previous recurrence
// this can happen if client gives an enddate which is NOT a recurrence date
// eg. for a on Monday recurring weekly event a Tuesday as enddate
// e.g. for an on Monday recurring weekly event a Tuesday as enddate
if ($this->enddate < $this->current && $this->current->format('Ymd') != $this->enddate->format('Ymd'))
{
$last = $previous;

View File

@ -0,0 +1,504 @@
# EGroupware REST API for Addressbook
Authentication is via Basic Auth with username and a password, or a token valid for:
- either just the given user or all users
- CalDAV/CardDAV Sync (REST API)
- Calendar application
Following RFCs / drafts used/planned for JSON encoding of resources
* [draft-ietf-jmap-jscontact: JSContact: A JSON Representation of Contact Data](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact)
([* see at end of document](#implemented-changes-from-jscontact-draft-08))
* [draft-ietf-jmap-jscontact-vcard: JSContact: Converting from and to vCard](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-vcard/)
* [rfc8984: JSCalendar: A JSON Representation of Calendar Data](https://datatracker.ietf.org/doc/html/rfc8984)
### Supported request methods and examples
* **GET** to collections with an ```Accept: application/json``` header return all resources (similar to WebDAV PROPFIND)
<details>
<summary>Example: Getting all entries of a given users addessbook</summary>
```
curl https://example.org/egroupware/groupdav.php/<username>/addressbook/ -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/<username>/addressbook/1833": {
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
],
"fullName": { "value": "Default Tester" },
"organizations": {
"org": {
"@type": "Organization",
"name": "default.org",
"units": {
"org_unit": "department.default.org"
}
}
},
"emails": {
"work": { "@type": "EmailAddress", "email": "test@test.com", "contexts": { "work": true }, "pref": 1 }
},
"phones": {
"tel_work": { "@type": "Phone", "phone": "+49 123 4567890", "pref": 1, "features": { "voice": true }, "contexts": { "work": true } },
"tel_cell": { "@type": "Phone", "phone": "012 3723567", "features": { "cell": true }, "contexts": { "work": true } }
},
"online": {
"url": { "@type": "Resource", "resource": "https://www.test.com/", "type": "uri", "contexts": { "work": true } }
},
"notes": [
"Test test TEST\n\\server\\share\n\\\nother\nblah"
],
},
"/<username>/addressbook/list-36": {
"uid": "urn:uuid:dfa5cac5-987b-448b-85d7-6c8b529a835c",
"name": "Example distribution list",
"card": {
"uid": "urn:uuid:dfa5cac5-987b-448b-85d7-6c8b529a835c",
"prodId": "EGroupware Addressbook 21.1.001",
"updated": "2018-04-11T14:46:43Z",
"fullName": { "value": "Example distribution list" }
},
"members": {
"5638-8623c4830472a8ede9f9f8b30d435ea4": true
}
}
}
}
```
</details>
following GET parameters are supported to customize the returned properties:
- props[]=<DAV-prop-name> eg. props[]=getetag to return only the ETAG (multiple DAV properties can be specified)
Default for addressbook collections is to only return address-data (JsContact), other collections return all props.
- sync-token=<token> to only request change since last sync-token, like rfc6578 sync-collection REPORT
- nresults=N limit number of responses (only for sync-collection / given sync-token parameter!)
this will return a "more-results"=true attribute and a new "sync-token" attribute to query for the next chunk
<details>
<summary>Example: Getting just ETAGs and displayname of all contacts in a given AB</summary>
```
curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/?props[]=getetag&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/addressbook/1833": {
"displayname": "Default Tester",
"getetag": "\"1833:24\""
},
"/addressbook/1838": {
"displayname": "Test Tester",
"getetag": "\"1838:19\""
}
}
}
```
</details>
<details>
<summary>Example: Start using a sync-token to get only changed entries since last sync</summary>
#### Initial request with empty sync-token and only requesting 10 entries per chunk:
```
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/addressbook/2050": "Frau Margot Test-Notifikation",
"/addressbook/2384": "Test Tester",
"/addressbook/5462": "Margot Testgedöns",
"/addressbook/2380": "Frau Test Defaulterin",
"/addressbook/5474": "Noch ein Neuer",
"/addressbook/5575": "Mr New Name",
"/addressbook/5461": "Herr Hugo Kurt Müller Senior",
"/addressbook/5601": "Steve Jobs",
"/addressbook/5603": "Ralf Becker",
"/addressbook/1838": "Test Tester"
},
"more-results": true,
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1400867824"
}
```
#### Requesting next chunk:
```
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=https://example.org/egroupware/groupdav.php/addressbook/1400867824&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/addressbook/1833": "Default Tester",
"/addressbook/5597": "Neuer Testschnuffi",
"/addressbook/5593": "Muster Max",
"/addressbook/5628": "2. Test Contact",
"/addressbook/5629": "Testen Tester",
"/addressbook/5630": "Testen Tester",
"/addressbook/5633": "Testen Tester",
"/addressbook/5635": "Test4 Tester",
"/addressbook/5638": "Test Kontakt",
"/addressbook/5636": "Test Default"
},
"more-results": true,
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1427103057"
}
```
</details>
<details>
<summary>Example: Requesting only changes since last sync</summary>
#### ```sync-token``` from last sync need to be specified (note the null for a deleted resource!)
```
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=https://example.org/egroupware/groupdav.php/addressbook/1400867824' -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/addressbook/5597": null,
"/addressbook/5593": {
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
],
"fullName": "Default Tester",
....
}
},
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1427103057"
}
```
</details>
* **GET** requests with an ```Accept: application/json``` header can be used to retrieve single resources / JsContact or JsCalendar schema
<details>
<summary>Example: GET request for a single resource showcasing available fieldes</summary>
```
curl 'https://example.org/egroupware/groupdav.php/addressbook/6502' -H "Accept: application/pretty+json" --user <username>
{
"uid": "addressbook-6502-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.003",
"created": "2022-12-14T13:35:02Z",
"updated": "2022-12-14T13:39:14Z",
"kind": "individual",
"name": [
{ "@type": "NameComponent", "type": "prefix", "value": "Prefix/Title" },
{ "@type": "NameComponent", "type": "personal", "value": "Frist" },
{ "@type": "NameComponent", "type": "additional", "value": "Middle" },
{ "@type": "NameComponent", "type": "surname", "value": "Last" },
{ "@type": "NameComponent", "type": "suffix", "value": "Postfix" }
],
"fullName": "Prefix/Title Frist Middle Last Postfix",
"organizations": {
"org": {
"@type": "Organization",
"name": "Organisation",
"units": { "org_unit": "Department" }
}
},
"titles": {
"title": {
"@type": "Title",
"title": "Postion",
"organization": "org"
},
"role": {
"@type": "Title",
"title": "Occupation",
"organization": "org"
}
},
"emails": {
"work": {
"@type": "EmailAddress",
"email": "email@example.org",
"contexts": { "work": true },
"pref": 1
},
"private": {
"@type": "EmailAddress",
"email": "private.email@example.org",
"contexts": { "private": true }
}
},
"phones": {
"tel_work": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "voice": true },
"contexts": { "work": true }
},
"tel_cell": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "cell": true },
"contexts": { "work": true }
},
"tel_fax": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "fax": true },
"contexts": { "work": true }
},
"tel_assistent": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "voice": true },
"contexts": { "assistant": true }
},
"tel_car": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "voice": true },
"contexts": { "car": true }
},
"tel_pager": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "pager": true },
"contexts": { "work": true }
},
"tel_home": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "voice": true },
"contexts": { "private": true }
},
"tel_fax_home": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "fax": true },
"contexts": { "private": true }
},
"tel_cell_private": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "cell": true },
"contexts": { "private": true }
},
"tel_other": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "voice": true },
"contexts": { "work": true }
}
},
"online": {
"url": {
"@type": "Resource",
"resource": "https://example.org",
"type": "uri",
"contexts": { "work": true }
},
"url_home": {
"@type": "Resource",
"resource": "https://private.example.org",
"type": "uri",
"contexts": { "private": true }
}
},
"addresses": {
"work": {
"@type": "Address",
"locality": "City",
"region": "Rheinland-Pfalz",
"country": "DEUTSCHLAND",
"postcode": "12345",
"countryCode": "DE",
"street": [
{ "@type": "StreetComponent", "type": "name", "value": "Street" },
{ "@type": "StreetComponent", "type": "separator", "value": "\n" },
{ "@type": "StreetComponent", "type": "name", "value": "Street2" ],
"contexts": { "work": true },
"pref": 1
},
"home": {
"@type": "Address",
"locality": "PrivateCity",
"country": "DEUTSCHLAND",
"postcode": "12345",
"countryCode": "DE",
"street": [
{ "@type": "StreetComponent", "type": "name", "value": "PrivateStreet" },
{ "@type": "StreetComponent", "type": "separator", "value": "\n" },
{ "@type": "StreetComponent", "type": "name", "value": "PrivateStreet2" }
],
"contexts": { "home": true }
}
},
"photos": {
"photo": {
"@type": "File",
"href": "https://boulder.egroupware.org/egroupware/api/avatar.php?contact_id=6502&lavatar=1&etag=0",
"mediaType": "image/jpeg"
}
},
"anniversaries": {
"bday": {
"@type": "Anniversary",
"type": "birth",
"date": "2022-12-14"
}
},
"categories": {
"Kategorie": true,
"My Contacts": true
},
"egroupware.org:assistant": "Assistent"
}
```
</details>
* **POST** requests to collection with a ```Content-Type: application/json``` header add new entries in addressbook or calendar collections
(Location header in response gives URL of new resource)
<details>
<summary>Example: POST request to create a new resource</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/' -X POST -d @- -H "Content-Type: application/json" --user <username>
{
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
],
"fullName": { "value": "Default Tester" },
....
}
EOF
HTTP/1.1 201 Created
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
```
</details>
<details>
<summary>Example: POST request to create a new resource using flat attributes (JSON patch syntax) eg. for a simple Wordpress contact-form</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/' -X POST -d @- -H "Content-Type: application/json" --user <username>
{
"fullName": "First Tester",
"name/personal": "First",
"name/surname": "Tester",
"organizations/org/name": "Test Organization",
"emails/work": "test.user@test-user.org",
"addresses/work/locality": "Test-Town",
"addresses/work/postcode": "12345",
"addresses/work/street": "Teststr. 123",
"addresses/work/country": "Germany",
"addresses/work/countryCode": "DE",
"phones/tel_work": "+49 123 4567890",
"online/url": "https://www.example.org/",
"notes/note": "This is a note.",
"egroupware.org:customfields/Test": "Content for Test"
}
EOF
HTTP/1.1 201 Created
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
```
</details>
* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!)
<details>
<summary>Example: PUT request to update a resource</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PUT -d @- -H "Content-Type: application/json" --user <username>
{
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
],
"fullName": { "value": "Default Tester" },
....
}
EOF
HTTP/1.1 204 No Content
```
</details>
<details>
<summary>Example: PUT request with UID to update an existing resource or create it, if not exists</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/5638-8623c4830472a8ede9f9f8b30d435ea4' -X PUT -d @- -H "Content-Type: application/json" --user <username>
{
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
],
"fullName": { "value": "Default Tester" },
....
}
EOF
```
Update of an existing one:
```
HTTP/1.1 204 No Content
```
New contact:
```
HTTP/1.1 201 Created
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
```
</details>
* **PATCH** request with a ```Content-Type: application/json``` header allow to modify a single resource by only specifying changed attributes as a [PatchObject](https://www.rfc-editor.org/rfc/rfc8984.html#type-PatchObject)
<details>
<summary>Example: PATCH request to modify a contact with partial data</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username>
{
"name": [
{
"@type": "NameComponent",
"type": "personal",
"value": "Testfirst"
},
{
"@type": "NameComponent",
"type": "surname",
"value": "Username"
}
],
"fullName": "Testfirst Username",
"organizations/org/name": "Test-User.org",
"emails/work/email": "test.user@test-user.org"
}
EOF
HTTP/1.1 204 No content
```
</details>
* **DELETE** requests delete single resources
* one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON eg. for debugging and exploring the API
#### Implemented [changes from JsContact draft 08](https://github.com/rsto/draft-stepanek-jscontact/compare/draft-ietf-jmap-jscontact-08):
* localizedString type / object is removed in favor or regular String type and a [localizations object like in JsCalendar](https://datatracker.ietf.org/doc/html/rfc8984#section-4.6.1)
* [Vendor-specific Property Extensions and Values](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-1.3)
use ```<domain-name>:<name>``` like in JsCalendar
* top-level objects need a ```@type``` attribute with one of the following values:
```NameComponent```, ```Organization```, ```Title```, ```Phone```, ```Resource```, ```File```, ```ContactLanguage```,
```Address```, ```StreetComponent```, ```Anniversary```, ```PersonalInformation```

View File

@ -0,0 +1,383 @@
# EGroupware REST API for Calendar
Authentication is via Basic Auth with username and a password, or a token valid for:
- either just the given user or all users
- CalDAV/CardDAV Sync (REST API)
- Calendar application
Following RFCs / drafts used/planned for JSON encoding of resources
* [rfc8984: JSCalendar: A JSON Representation of Calendar Data](https://datatracker.ietf.org/doc/html/rfc8984)
### Supported request methods and examples
* **GET** to collections with an ```Accept: application/json``` header return all resources (similar to WebDAV PROPFIND)
<details>
<summary>Example: Getting all entries of a given users calendar</summary>
```
curl https://example.org/egroupware/groupdav.php/<username>/calendar/ -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/<username>/calendar/5695": {
"@type": "Event",
"prodId": "EGroupware Calendar 23.1.002",
"uid": "calendar-5695-34b52fc11cfa7e9acea5732210a53f48",
"sequence": "1",
"created": "2023-07-14T06:05:53Z",
"updated": "2023-07-14T08:00:04Z",
"title": "Test",
"start": "2023-07-14T10:00:00",
"timeZone": "Europe/Berlin",
"duration": "PT1H",
"participants": {
"5": {
"@type": "Participant",
"name": "Ralf Becker",
"email": "ralf@boulder.egroupware.org",
"kind": "individual",
"roles": {
"owner": true,
"chair": true
},
"participationStatus": "accepted"
}
},
"status": "confirmed",
"priority": 5,
"privacy": "public"
},
"/<username>/calendar/5699": {
"@type": "Event",
"prodId": "EGroupware Calendar 23.1.002",
"uid": "calendar-5699-34b52fc11cfa7e9acea5732210a53f48",
"sequence": "5",
"created": "2023-07-24T10:06:23Z",
"updated": "2023-07-24T13:11:49Z",
"title": "Monday and Wednesday 13h",
"start": "2023-07-24T13:00:00",
"timeZone": "Europe/Berlin",
"duration": "PT1H",
"recurrenceRules": [
{
"@type": "RecurrenceRule",
"frequency": "weekly",
"until": "2023-08-30T13:00:00",
"byDay": {
"@type": "NDay",
"day": "mo,we"
}
}
],
"recurrenceOverrides": {
"2023-07-31T13:00:00": { "excluded": true },
"2023-07-26T13:00:00": {
"sequence": "1",
"updated": "2023-07-24T11:39:44Z",
"title": "Monday und Wednesday 13h, but 26th 14h",
"start": "2023-07-26T14:00:00",
"description": "sdfasdf",
"showWithoutTime": null,
"categories": null,
"alerts": {
"64be4dc0-a044-4b27-b450-0026ac120002": {
"@type": "Alert",
"trigger": {
"@type": "OffsetTrigger",
"offset": 0
}
},
"64be4d1f-c958-4fbd-afc3-0026ac120002": null
}
}
},
"participants": {
"5": {
"@type": "Participant",
"name": "Ralf Becker",
"email": "ralf@boulder.egroupware.org",
"kind": "individual",
"roles": {
"owner": true,
"chair": true
},
"participationStatus": "accepted"
}
},
"alerts": {
"64be4d1f-c958-4fbd-afc3-0026ac120002": {
"@type": "Alert",
"trigger": {
"@type": "OffsetTrigger",
"offset": 0
}
}
},
"status": "confirmed",
"priority": 5,
"privacy": "public"
},
"/<username>/calendar/5701": {
"@type": "Event",
"prodId": "EGroupware Calendar 23.1.002",
"uid": "calendar-5701-34b52fc11cfa7e9acea5732210a53f48",
"sequence": "1",
"created": "2023-07-24T12:31:58Z",
"updated": "2023-07-24T12:41:54Z",
"title": "Di und Do den ganzen Tag",
"start": "2023-07-25T00:00:00",
"timeZone": "Europe/Berlin",
"showWithoutTime": true,
"duration": "P1D",
"recurrenceRules": [
{
"@type": "RecurrenceRule",
"frequency": "weekly",
"until": "2023-08-03T00:00:00",
"byDay": {
"@type": "NDay",
"day": "tu,th"
}
}
],
"recurrenceOverrides": {
"2023-07-27T00:00:00": {
"title": "Di und Do den ganzen Tag: AUSNAHME",
"start": "2023-07-27T00:00:00",
"description": "adsfads",
"sequence": null,
"categories": null,
"participants": {
"44": {
"@type": "Participant",
"name": "Birgit Becker",
"email": "birgit@boulder.egroupware.org",
"kind": "individual",
"roles": { "attendee": true },
"participationStatus": "needs-action"
}
},
"alerts": {
"64be7192-d4e4-4609-8c7a-004dac120002": {
"@type": "Alert",
"trigger": {
"@type": "OffsetTrigger",
"offset": 0
}
},
"64be6f3e-bc8c-4e78-9f96-004bac120002": null
}
}
},
"freeBusyStatus": "free",
"participants": {
"5": {
"@type": "Participant",
"name": "Ralf Becker",
"email": "ralf@boulder.egroupware.org",
"kind": "individual",
"roles": {
"owner": true,
"chair": true
},
"participationStatus": "accepted"
}
},
"alerts": {
"64be6f3e-bc8c-4e78-9f96-004bac120002": {
"@type": "Alert",
"trigger": {
"@type": "OffsetTrigger",
"offset": 0
}
}
},
"status": "confirmed",
"priority": 5,
"privacy": "public"
}
}
}
```
</details>
following GET parameters are supported to customize the returned properties:
- props[]=<DAV-prop-name> eg. props[]=getetag to return only the ETAG (multiple DAV properties can be specified)
Default for addressbook collections is to only return address-data (JsContact), other collections return all props.
- sync-token=<token> to only request change since last sync-token, like rfc6578 sync-collection REPORT
- nresults=N limit number of responses (only for sync-collection / given sync-token parameter!)
this will return a "more-results"=true attribute and a new "sync-token" attribute to query for the next chunk
Examples: see addressbook
* **GET** requests with an ```Accept: application/json``` header can be used to retrieve single resources / JsContact or JsCalendar schema
<details>
<summary>Example: GET request for a single resource</summary>
```
curl 'https://example.org/egroupware/groupdav.php/addressbook/6502' -H "Accept: application/pretty+json" --user <username>
{
"@type": "Event",
"prodId": "EGroupware Calendar 23.1.002",
"uid": "calendar-5695-34b52fc11cfa7e9acea5732210a53f48",
"sequence": "1",
"created": "2023-07-14T06:05:53Z",
"updated": "2023-07-14T08:00:04Z",
"title": "Test",
"start": "2023-07-14T10:00:00",
"timeZone": "Europe/Berlin",
"duration": "PT1H",
"participants": {
"5": {
"@type": "Participant",
"name": "Ralf Becker",
"email": "ralf@boulder.egroupware.org",
"kind": "individual",
"roles": {
"owner": true,
"chair": true
},
"participationStatus": "accepted"
}
},
"status": "confirmed",
"priority": 5,
"privacy": "public"
}
```
</details>
* **POST** requests to collection with a ```Content-Type: application/json``` header add new entries in addressbook or calendar collections
(Location header in response gives URL of new resource)
<details>
<summary>Example: POST request to create a new resource and use "Prefer: return=representation" to get it fully expanded back</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/calendar/' -X POST -d @- -H "Content-Type: application/json" -H "Prefer: return=representation" --user <username>
{
"title": "Test 25th",
"start": "2023-07-25T10:00:00",
"timeZone": "Europe/Berlin",
"duration": "PT1H"
}
EOF
HTTP/1.1 201 Created
Content-Type: application/jscalendar+json;type=event;charset=utf-8
Location: /egroupware/groupdav.php/ralf/calendar/5704
ETag: "5704:0:1690209221"
Schedule-Tag: "5704:0"
X-WebDAV-Status: 201 Created
{
"@type":"Event",
"prodId":"EGroupware Calendar 23.1.002",
"uid":"urn:uuid:e2b7278b-d91a-47d1-85ee-19dd1fb9b315",
"created":"2023-07-24T14:33:41Z",
"updated":"2023-07-24T14:33:41Z",
"title":"Test 25th",
"start":"2023-07-25T10:00:00",
"timeZone":"Europe/Berlin",
"duration":"PT1H",
"participants":{
"5":{
"@type":"Participant",
"name":"Ralf Becker",
"email":"ralf@boulder.egroupware.org",
"kind":"individual",
"roles":{
"owner":true,
"chair":true
},
"participationStatus":"accepted"
}
}
"status":"confirmed",
"priority":5,
"privacy":"public"
}
```
</details>
* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!)
<details>
<summary>Example: PUT request to update a resource</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PUT -d @- -H "Content-Type: application/json" --user <username>
{
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
],
"fullName": { "value": "Default Tester" },
....
}
EOF
HTTP/1.1 204 No Content
```
</details>
<details>
<summary>Example: PUT request with UID to update an existing resource or create it, if not exists</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/calendar/5638-8623c4830472a8ede9f9f8b30d435ea4' -X PUT -d @- -H "Content-Type: application/json" --user <username>
{
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"title": "Testevent",
"start": "2023-07-24T12:00:00",
"timeZone": "Europe/Berlin",
"duration": "PT2H",
....
}
EOF
```
Update of an existing one:
```
HTTP/1.1 204 No Content
```
New contact:
```
HTTP/1.1 201 Created
Location: https://example.org/egroupware/groupdav.php/<username>/calendar/1234
```
</details>
* **PATCH** request with a ```Content-Type: application/json``` header allow to modify a single resource by only specifying changed attributes as a [PatchObject](https://www.rfc-editor.org/rfc/rfc8984.html#type-PatchObject)
<details>
<summary>Example: PATCH request to modify an event with partial data</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/calendar/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username>
{
"title": "New title"
}
EOF
HTTP/1.1 204 No content
```
</details>
* **DELETE** requests delete single resources
<details>
<summary>Example: Delete an existing event</summary>
> Please note: the "Accept: application/json" header is required, as the CalDAV server would return 404 NotFound as the url does NOT end with .ics
```
curl -i 'https://example.org/egroupware/groupdav.php/<username>/calendar/1234' -X DELETE -H "Accept: application/json" --user <username>
HTTP/1.1 204 No Content
```
</details>
* one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON e.g. for debugging and exploring the API

View File

@ -36,513 +36,26 @@ One can use the following URLs relative (!) to https://example.org/egroupware/gr
Shared addressbooks or calendars are only shown in the users home-set, if he subscribed to it via his CalDAV preferences!
Calling one of the above collections with a GET request / regular browser generates an automatic index
from the data of an allprop PROPFIND, allow browsing CalDAV/CardDAV tree with a regular browser.
from the data of an ```allprop``` PROPFIND, allow browsing CalDAV/CardDAV tree with a regular browser.
## REST API: using EGroupware CalDAV/CardDAV server with JSON
> currently implemented only for contacts and calendar (without recurring events)!
- [Addressbook](Addressbook.md)
- [Calendar](Calendar.md) (currently recurring events are readonly, they are returned but can not be created or modified)
- [Mail](Mail.md) (currently only sending mails or opening interactive compose windows)
Following RFCs / drafts used/planned for JSON encoding of ressources
Following RFCs / drafts used/planned for JSON encoding of resources
* [draft-ietf-jmap-jscontact: JSContact: A JSON Representation of Contact Data](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact)
([* see at end of document](#implemented-changes-from-jscontact-draft-08))
* [draft-ietf-jmap-jscontact-vcard: JSContact: Converting from and to vCard](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-vcard/)
* [rfc8984: JSCalendar: A JSON Representation of Calendar Data](https://datatracker.ietf.org/doc/html/rfc8984)
### Supported request methods and examples
* **GET** to collections with an ```Accept: application/json``` header return all resources (similar to WebDAV PROPFIND)
<details>
<summary>Example: Getting all entries of a given users addessbook</summary>
```
curl https://example.org/egroupware/groupdav.php/<username>/addressbook/ -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/<username>/addressbook/1833": {
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
],
"fullName": { "value": "Default Tester" },
"organizations": {
"org": {
"@type": "Organization",
"name": "default.org",
"units": {
"org_unit": "department.default.org"
}
}
},
"emails": {
"work": { "@type": "EmailAddress", "email": "test@test.com", "contexts": { "work": true }, "pref": 1 }
},
"phones": {
"tel_work": { "@type": "Phone", "phone": "+49 123 4567890", "pref": 1, "features": { "voice": true }, "contexts": { "work": true } },
"tel_cell": { "@type": "Phone", "phone": "012 3723567", "features": { "cell": true }, "contexts": { "work": true } }
},
"online": {
"url": { "@type": "Resource", "resource": "https://www.test.com/", "type": "uri", "contexts": { "work": true } }
},
"notes": [
"Test test TEST\n\\server\\share\n\\\nother\nblah"
],
},
"/<username>/addressbook/list-36": {
"uid": "urn:uuid:dfa5cac5-987b-448b-85d7-6c8b529a835c",
"name": "Example distribution list",
"card": {
"uid": "urn:uuid:dfa5cac5-987b-448b-85d7-6c8b529a835c",
"prodId": "EGroupware Addressbook 21.1.001",
"updated": "2018-04-11T14:46:43Z",
"fullName": { "value": "Example distribution list" }
},
"members": {
"5638-8623c4830472a8ede9f9f8b30d435ea4": true
}
}
}
}
```
</details>
following GET parameters are supported to customize the returned properties:
- props[]=<DAV-prop-name> eg. props[]=getetag to return only the ETAG (multiple DAV properties can be specified)
Default for addressbook collections is to only return address-data (JsContact), other collections return all props.
- sync-token=<token> to only request change since last sync-token, like rfc6578 sync-collection REPORT
- nresults=N limit number of responses (only for sync-collection / given sync-token parameter!)
this will return a "more-results"=true attribute and a new "sync-token" attribute to query for the next chunk
<details>
<summary>Example: Getting just ETAGs and displayname of all contacts in a given AB</summary>
```
curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/?props[]=getetag&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/addressbook/1833": {
"displayname": "Default Tester",
"getetag": "\"1833:24\""
},
"/addressbook/1838": {
"displayname": "Test Tester",
"getetag": "\"1838:19\""
}
}
}
```
</details>
<details>
<summary>Example: Start using a sync-token to get only changed entries since last sync</summary>
#### Initial request with empty sync-token and only requesting 10 entries per chunk:
```
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/addressbook/2050": "Frau Margot Test-Notifikation",
"/addressbook/2384": "Test Tester",
"/addressbook/5462": "Margot Testgedöns",
"/addressbook/2380": "Frau Test Defaulterin",
"/addressbook/5474": "Noch ein Neuer",
"/addressbook/5575": "Mr New Name",
"/addressbook/5461": "Herr Hugo Kurt Müller Senior",
"/addressbook/5601": "Steve Jobs",
"/addressbook/5603": "Ralf Becker",
"/addressbook/1838": "Test Tester"
},
"more-results": true,
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1400867824"
}
```
#### Requesting next chunk:
```
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=https://example.org/egroupware/groupdav.php/addressbook/1400867824&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/addressbook/1833": "Default Tester",
"/addressbook/5597": "Neuer Testschnuffi",
"/addressbook/5593": "Muster Max",
"/addressbook/5628": "2. Test Contact",
"/addressbook/5629": "Testen Tester",
"/addressbook/5630": "Testen Tester",
"/addressbook/5633": "Testen Tester",
"/addressbook/5635": "Test4 Tester",
"/addressbook/5638": "Test Kontakt",
"/addressbook/5636": "Test Default"
},
"more-results": true,
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1427103057"
}
```
</details>
<details>
<summary>Example: Requesting only changes since last sync</summary>
#### ```sync-token``` from last sync need to be specified (note the null for a deleted resource!)
```
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=https://example.org/egroupware/groupdav.php/addressbook/1400867824' -H "Accept: application/pretty+json" --user <username>
{
"responses": {
"/addressbook/5597": null,
"/addressbook/5593": {
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
],
"fullName": "Default Tester",
....
}
},
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1427103057"
}
```
</details>
* **GET** requests with an ```Accept: application/json``` header can be used to retrieve single resources / JsContact or JsCalendar schema
<details>
<summary>Example: GET request for a single resource showcasing available fieldes</summary>
```
curl 'https://example.org/egroupware/groupdav.php/addressbook/6502' -H "Accept: application/pretty+json" --user <username>
{
"uid": "addressbook-6502-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.003",
"created": "2022-12-14T13:35:02Z",
"updated": "2022-12-14T13:39:14Z",
"kind": "individual",
"name": [
{ "@type": "NameComponent", "type": "prefix", "value": "Prefix/Title" },
{ "@type": "NameComponent", "type": "personal", "value": "Frist" },
{ "@type": "NameComponent", "type": "additional", "value": "Middle" },
{ "@type": "NameComponent", "type": "surname", "value": "Last" },
{ "@type": "NameComponent", "type": "suffix", "value": "Postfix" }
],
"fullName": "Prefix/Title Frist Middle Last Postfix",
"organizations": {
"org": {
"@type": "Organization",
"name": "Organisation",
"units": { "org_unit": "Department" }
}
},
"titles": {
"title": {
"@type": "Title",
"title": "Postion",
"organization": "org"
},
"role": {
"@type": "Title",
"title": "Occupation",
"organization": "org"
}
},
"emails": {
"work": {
"@type": "EmailAddress",
"email": "email@example.org",
"contexts": { "work": true },
"pref": 1
},
"private": {
"@type": "EmailAddress",
"email": "private.email@example.org",
"contexts": { "private": true }
}
},
"phones": {
"tel_work": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "voice": true },
"contexts": { "work": true }
},
"tel_cell": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "cell": true },
"contexts": { "work": true }
},
"tel_fax": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "fax": true },
"contexts": { "work": true }
},
"tel_assistent": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "voice": true },
"contexts": { "assistant": true }
},
"tel_car": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "voice": true },
"contexts": { "car": true }
},
"tel_pager": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "pager": true },
"contexts": { "work": true }
},
"tel_home": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "voice": true },
"contexts": { "private": true }
},
"tel_fax_home": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "fax": true },
"contexts": { "private": true }
},
"tel_cell_private": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "cell": true },
"contexts": { "private": true }
},
"tel_other": {
"@type": "Phone",
"phone": "+1(234)5678901",
"features": { "voice": true },
"contexts": { "work": true }
}
},
"online": {
"url": {
"@type": "Resource",
"resource": "https://example.org",
"type": "uri",
"contexts": { "work": true }
},
"url_home": {
"@type": "Resource",
"resource": "https://private.example.org",
"type": "uri",
"contexts": { "private": true }
}
},
"addresses": {
"work": {
"@type": "Address",
"locality": "City",
"region": "Rheinland-Pfalz",
"country": "DEUTSCHLAND",
"postcode": "12345",
"countryCode": "DE",
"street": [
{ "@type": "StreetComponent", "type": "name", "value": "Street" },
{ "@type": "StreetComponent", "type": "separator", "value": "\n" },
{ "@type": "StreetComponent", "type": "name", "value": "Street2" ],
"contexts": { "work": true },
"pref": 1
},
"home": {
"@type": "Address",
"locality": "PrivateCity",
"country": "DEUTSCHLAND",
"postcode": "12345",
"countryCode": "DE",
"street": [
{ "@type": "StreetComponent", "type": "name", "value": "PrivateStreet" },
{ "@type": "StreetComponent", "type": "separator", "value": "\n" },
{ "@type": "StreetComponent", "type": "name", "value": "PrivateStreet2" }
],
"contexts": { "home": true }
}
},
"photos": {
"photo": {
"@type": "File",
"href": "https://boulder.egroupware.org/egroupware/api/avatar.php?contact_id=6502&lavatar=1&etag=0",
"mediaType": "image/jpeg"
}
},
"anniversaries": {
"bday": {
"@type": "Anniversary",
"type": "birth",
"date": "2022-12-14"
}
},
"categories": {
"Kategorie": true,
"My Contacts": true
},
"egroupware.org:assistant": "Assistent"
}
```
</details>
* **POST** requests to collection with a ```Content-Type: application/json``` header add new entries in addressbook or calendar collections
(Location header in response gives URL of new resource)
<details>
<summary>Example: POST request to create a new resource</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/' -X POST -d @- -H "Content-Type: application/json" --user <username>
{
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
],
"fullName": { "value": "Default Tester" },
....
}
EOF
HTTP/1.1 201 Created
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
```
</details>
<details>
<summary>Example: POST request to create a new resource using flat attributes (JSON patch syntax) eg. for a simple Wordpress contact-form</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/' -X POST -d @- -H "Content-Type: application/json" --user <username>
{
"fullName": "First Tester",
"name/personal": "First",
"name/surname": "Tester",
"organizations/org/name": "Test Organization",
"emails/work": "test.user@test-user.org",
"addresses/work/locality": "Test-Town",
"addresses/work/postcode": "12345",
"addresses/work/street": "Teststr. 123",
"addresses/work/country": "Germany",
"addresses/work/countryCode": "DE",
"phones/tel_work": "+49 123 4567890",
"online/url": "https://www.example.org/",
"notes/note": "This is a note.",
"egroupware.org:customfields/Test": "Content for Test"
}
EOF
HTTP/1.1 201 Created
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
```
</details>
* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!)
<details>
<summary>Example: PUT request to update a resource</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PUT -d @- -H "Content-Type: application/json" --user <username>
{
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
],
"fullName": { "value": "Default Tester" },
....
}
EOF
HTTP/1.1 204 No Content
```
</details>
<details>
<summary>Example: PUT request with UID to update an existing resource or create it, if not exists</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/5638-8623c4830472a8ede9f9f8b30d435ea4' -X PUT -d @- -H "Content-Type: application/json" --user <username>
{
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
"prodId": "EGroupware Addressbook 21.1.001",
"created": "2010-10-21T09:55:42Z",
"updated": "2014-06-02T14:45:24Z",
"name": [
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
],
"fullName": { "value": "Default Tester" },
....
}
EOF
```
Update of an existing one:
```
HTTP/1.1 204 No Content
```
New contact:
```
HTTP/1.1 201 Created
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
```
</details>
* **PATCH** request with a ```Content-Type: application/json``` header allow to modify a single resource by only specifying changed attributes as a [PatchObject](https://www.rfc-editor.org/rfc/rfc8984.html#type-PatchObject)
<details>
<summary>Example: PATCH request to modify a contact with partial data</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username>
{
"name": [
{
"@type": "NameComponent",
"type": "personal",
"value": "Testfirst"
},
{
"@type": "NameComponent",
"type": "surname",
"value": "Username"
}
],
"fullName": "Testfirst Username",
"organizations/org/name": "Test-User.org",
"emails/work/email": "test.user@test-user.org"
}
EOF
HTTP/1.1 204 No content
```
</details>
* **DELETE** requests delete single resources
* one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON eg. for debugging and exploring the API
#### Implemented [changes from JsContact draft 08](https://github.com/rsto/draft-stepanek-jscontact/compare/draft-ietf-jmap-jscontact-08):
* localizedString type / object is removed in favor or regular String type and a [localizations object like in JsCalendar](https://datatracker.ietf.org/doc/html/rfc8984#section-4.6.1)
* [Vendor-specific Property Extensions and Values](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-1.3)
use ```<domain-name>:<name>``` like in JsCalendar
* top-level objects need a ```@type``` attribute with one of the following values:
```NameComponent```, ```Organization```, ```Title```, ```Phone```, ```Resource```, ```File```, ```ContactLanguage```,
```Address```, ```StreetComponent```, ```Anniversary```, ```PersonalInformation```
### ToDos
- [x] Addressbook
- [ ] update of photos, keys, attachments
- [ ] InfoLog
- [X] Calendar
- [X] Calendar (recurring events and alarms are readonly)
- [ ] support creating and modifying recurring events and alarms
- [X] Mail
- [ ] querying received mails
- [ ] relatedTo / links
- [ ] storing not native supported attributes eg. localization