From b013f75eef0ffa77db9c74f85c82028ce4b07f1c Mon Sep 17 00:00:00 2001 From: ralf Date: Mon, 24 Jul 2023 17:08:05 +0200 Subject: [PATCH] * 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 --- api/src/CalDAV/JsCalendar.php | 167 +++++- calendar/inc/class.calendar_bo.inc.php | 6 +- calendar/inc/class.calendar_groupdav.inc.php | 52 +- calendar/inc/class.calendar_rrule.inc.php | 4 +- doc/REST-CalDAV-CardDAV/Addressbook.md | 504 ++++++++++++++++++ doc/REST-CalDAV-CardDAV/Calendar.md | 383 ++++++++++++++ doc/REST-CalDAV-CardDAV/README.md | 505 +------------------ 7 files changed, 1083 insertions(+), 538 deletions(-) create mode 100644 doc/REST-CalDAV-CardDAV/Addressbook.md create mode 100644 doc/REST-CalDAV-CardDAV/Calendar.md diff --git a/api/src/CalDAV/JsCalendar.php b/api/src/CalDAV/JsCalendar.php index 8bf5f537c0..f7484bfc41 100644 --- a/api/src/CalDAV/JsCalendar.php +++ b/api/src/CalDAV/JsCalendar.php @@ -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; } diff --git a/calendar/inc/class.calendar_bo.inc.php b/calendar/inc/class.calendar_bo.inc.php index 917942c708..3d27c566a2 100644 --- a/calendar/inc/class.calendar_bo.inc.php +++ b/calendar/inc/class.calendar_bo.inc.php @@ -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; } diff --git a/calendar/inc/class.calendar_groupdav.inc.php b/calendar/inc/class.calendar_groupdav.inc.php index d74dfbb5f4..a0d1c9c724 100644 --- a/calendar/inc/class.calendar_groupdav.inc.php +++ b/calendar/inc/class.calendar_groupdav.inc.php @@ -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; diff --git a/calendar/inc/class.calendar_rrule.inc.php b/calendar/inc/class.calendar_rrule.inc.php index e28f6ace1e..01a03ef10d 100644 --- a/calendar/inc/class.calendar_rrule.inc.php +++ b/calendar/inc/class.calendar_rrule.inc.php @@ -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; diff --git a/doc/REST-CalDAV-CardDAV/Addressbook.md b/doc/REST-CalDAV-CardDAV/Addressbook.md new file mode 100644 index 0000000000..6fd8935f7e --- /dev/null +++ b/doc/REST-CalDAV-CardDAV/Addressbook.md @@ -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) +
+ Example: Getting all entries of a given users addessbook + +``` +curl https://example.org/egroupware/groupdav.php//addressbook/ -H "Accept: application/pretty+json" --user +{ + "responses": { + "//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" + ], + }, + "//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 + } + } + } +} +``` +
+ + following GET parameters are supported to customize the returned properties: + - props[]= 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= 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 + +
+ Example: Getting just ETAGs and displayname of all contacts in a given AB + +``` +curl -i 'https://example.org/egroupware/groupdav.php//addressbook/?props[]=getetag&props[]=displayname' -H "Accept: application/pretty+json" --user +{ + "responses": { + "/addressbook/1833": { + "displayname": "Default Tester", + "getetag": "\"1833:24\"" + }, + "/addressbook/1838": { + "displayname": "Test Tester", + "getetag": "\"1838:19\"" + } + } +} +``` +
+ +
+ Example: Start using a sync-token to get only changed entries since last sync + +#### 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 +{ + "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 +{ + "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" +} +``` +
+ +
+ Example: Requesting only changes since last sync + +#### ```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 +{ + "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" +} +``` +
+ +* **GET** requests with an ```Accept: application/json``` header can be used to retrieve single resources / JsContact or JsCalendar schema +
+ Example: GET request for a single resource showcasing available fieldes + +``` +curl 'https://example.org/egroupware/groupdav.php/addressbook/6502' -H "Accept: application/pretty+json" --user +{ + "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" +} +``` +
+ +* **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) +
+ Example: POST request to create a new resource + +``` +cat </addressbook/' -X POST -d @- -H "Content-Type: application/json" --user +{ + "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//addressbook/1234 +``` +
+ +
+ Example: POST request to create a new resource using flat attributes (JSON patch syntax) eg. for a simple Wordpress contact-form + +``` +cat </addressbook/' -X POST -d @- -H "Content-Type: application/json" --user +{ + "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//addressbook/1234 +``` +
+ +* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!) + +
+ Example: PUT request to update a resource + +``` +cat </addressbook/1234' -X PUT -d @- -H "Content-Type: application/json" --user +{ + "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 +``` +
+ +
+ Example: PUT request with UID to update an existing resource or create it, if not exists + +``` +cat </addressbook/5638-8623c4830472a8ede9f9f8b30d435ea4' -X PUT -d @- -H "Content-Type: application/json" --user +{ + "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//addressbook/1234 +``` +
+ + +* **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) + +
+ Example: PATCH request to modify a contact with partial data + +``` +cat </addressbook/1234' -X PATCH -d @- -H "Content-Type: application/json" --user +{ + "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 +``` +
+ +* **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 ```:``` 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``` \ No newline at end of file diff --git a/doc/REST-CalDAV-CardDAV/Calendar.md b/doc/REST-CalDAV-CardDAV/Calendar.md new file mode 100644 index 0000000000..53e64f3e23 --- /dev/null +++ b/doc/REST-CalDAV-CardDAV/Calendar.md @@ -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) +
+ Example: Getting all entries of a given users calendar + +``` +curl https://example.org/egroupware/groupdav.php//calendar/ -H "Accept: application/pretty+json" --user +{ + "responses": { + "//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" + }, + "//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" + }, + "//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" + } + } +} +``` +
+ +following GET parameters are supported to customize the returned properties: +- props[]= 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= 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 +
+ Example: GET request for a single resource + +``` +curl 'https://example.org/egroupware/groupdav.php/addressbook/6502' -H "Accept: application/pretty+json" --user +{ + "@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" +} +``` +
+ +* **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) +
+ Example: POST request to create a new resource and use "Prefer: return=representation" to get it fully expanded back + +``` +cat </calendar/' -X POST -d @- -H "Content-Type: application/json" -H "Prefer: return=representation" --user +{ + "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" +} +``` +
+ + +* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!) + +
+ Example: PUT request to update a resource + +``` +cat </addressbook/1234' -X PUT -d @- -H "Content-Type: application/json" --user +{ + "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 +``` +
+ +
+ Example: PUT request with UID to update an existing resource or create it, if not exists + +``` +cat </calendar/5638-8623c4830472a8ede9f9f8b30d435ea4' -X PUT -d @- -H "Content-Type: application/json" --user +{ + "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//calendar/1234 +``` +
+ + +* **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) + +
+ Example: PATCH request to modify an event with partial data + +``` +cat </calendar/1234' -X PATCH -d @- -H "Content-Type: application/json" --user +{ + "title": "New title" +} +EOF + +HTTP/1.1 204 No content +``` +
+ +* **DELETE** requests delete single resources +
+ Example: Delete an existing event +> 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//calendar/1234' -X DELETE -H "Accept: application/json" --user + +HTTP/1.1 204 No Content +``` +
+ +* one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON e.g. for debugging and exploring the API \ No newline at end of file diff --git a/doc/REST-CalDAV-CardDAV/README.md b/doc/REST-CalDAV-CardDAV/README.md index 2e78d59446..cc6824d718 100644 --- a/doc/REST-CalDAV-CardDAV/README.md +++ b/doc/REST-CalDAV-CardDAV/README.md @@ -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) -
- Example: Getting all entries of a given users addessbook - -``` -curl https://example.org/egroupware/groupdav.php//addressbook/ -H "Accept: application/pretty+json" --user -{ - "responses": { - "//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" - ], - }, - "//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 - } - } - } -} -``` -
- - following GET parameters are supported to customize the returned properties: - - props[]= 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= 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 - -
- Example: Getting just ETAGs and displayname of all contacts in a given AB - -``` -curl -i 'https://example.org/egroupware/groupdav.php//addressbook/?props[]=getetag&props[]=displayname' -H "Accept: application/pretty+json" --user -{ - "responses": { - "/addressbook/1833": { - "displayname": "Default Tester", - "getetag": "\"1833:24\"" - }, - "/addressbook/1838": { - "displayname": "Test Tester", - "getetag": "\"1838:19\"" - } - } -} -``` -
- -
- Example: Start using a sync-token to get only changed entries since last sync - -#### 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 -{ - "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 -{ - "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" -} -``` -
- -
- Example: Requesting only changes since last sync - -#### ```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 -{ - "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" -} -``` -
- -* **GET** requests with an ```Accept: application/json``` header can be used to retrieve single resources / JsContact or JsCalendar schema -
- Example: GET request for a single resource showcasing available fieldes - -``` -curl 'https://example.org/egroupware/groupdav.php/addressbook/6502' -H "Accept: application/pretty+json" --user -{ - "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" -} -``` -
- -* **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) -
- Example: POST request to create a new resource - -``` -cat </addressbook/' -X POST -d @- -H "Content-Type: application/json" --user -{ - "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//addressbook/1234 -``` -
- -
- Example: POST request to create a new resource using flat attributes (JSON patch syntax) eg. for a simple Wordpress contact-form - -``` -cat </addressbook/' -X POST -d @- -H "Content-Type: application/json" --user -{ - "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//addressbook/1234 -``` -
- -* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!) - -
- Example: PUT request to update a resource - -``` -cat </addressbook/1234' -X PUT -d @- -H "Content-Type: application/json" --user -{ - "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 -``` -
- -
- Example: PUT request with UID to update an existing resource or create it, if not exists - -``` -cat </addressbook/5638-8623c4830472a8ede9f9f8b30d435ea4' -X PUT -d @- -H "Content-Type: application/json" --user -{ - "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//addressbook/1234 -``` -
- - -* **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) - -
- Example: PATCH request to modify a contact with partial data - -``` -cat </addressbook/1234' -X PATCH -d @- -H "Content-Type: application/json" --user -{ - "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 -``` -
- -* **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 ```:``` 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 \ No newline at end of file