Example #1
0
class EventResource(ItemResource):
    fields = ItemResource.fields.copy()
    fields.update({
        'id': lambda item: item.eventid,
        'subject': lambda item: item.subject,
        'recurrence': recurrence_json,
        'start': lambda req, item: _tzdate(item.start, req),
        'end': lambda req, item: _tzdate(item.end, req),
        'location': lambda item: {'displayName': item.location, 'address': {}}, # TODO
        'importance': lambda item: item.urgency,
        'sensitivity': lambda item: sensitivity_map[item.sensitivity],
        'hasAttachments': lambda item: item.has_attachments,
        'body': lambda req, item: get_body(req, item),
        'isReminderOn': lambda item: item.reminder,
        'reminderMinutesBeforeStart': lambda item: item.reminder_minutes,
        'attendees': lambda item: attendees_json(item),
        'bodyPreview': lambda item: item.body_preview,
        'isAllDay': lambda item: item.all_day,
        'showAs': lambda item: show_as_map[item.show_as],
        'seriesMasterId': lambda item: item.item.eventid if isinstance(item, kopano.Occurrence) else None,
        'type': lambda item: event_type(item),
        'responseRequested': lambda item: item.response_requested,
        'iCalUId': lambda item: kopano.hex(kopano.bdec(item.icaluid)) if item.icaluid else None, # graph uses hex!?
        'organizer': lambda item: get_email(item.from_),
        'isOrganizer': lambda item: item.from_.email == item.sender.email,
    })

    set_fields = {
        'subject': lambda item, arg: setattr(item, 'subject', arg),
        'location': lambda item, arg: setattr(item, 'location', arg['displayName']), # TODO
        'body': set_body,
        'start': lambda item, arg: set_date(item, 'start', arg),
        'end': lambda item, arg: set_date(item, 'end', arg),
        'attendees': lambda item, arg: attendees_set(item, arg),
        'recurrence': recurrence_set,
        'isAllDay': lambda item, arg: setattr(item, 'all_day', arg),
        'isReminderOn': lambda item, arg: setattr(item, 'reminder', arg),
        'reminderMinutesBeforeStart': lambda item, arg: setattr(item, 'reminder_minutes', arg),
    }

    # TODO delta functionality seems to include expanding recurrences!? check with MSGE

    def on_get(self, req, resp, userid=None, folderid=None, eventid=None, method=None):
        server, store = _server_store(req, userid, self.options)
        folder = _folder(store, folderid or 'calendar')
        event = folder.event(eventid)

        if method == 'attachments':
            attachments = list(event.attachments(embedded=True))
            data = (attachments, DEFAULT_TOP, 0, len(attachments))
            self.respond(req, resp, data, AttachmentResource.fields)

        elif method == 'instances':
            start, end = _start_end(req)
            data = (event.occurrences(start, end), DEFAULT_TOP, 0, 0)
            self.respond(req, resp, data)

        elif method:
            raise falcon.HTTPBadRequest(None, "Unsupported segment '%s'" % method)

        else:
            self.respond(req, resp, event)

    def on_post(self, req, resp, userid=None, folderid=None, eventid=None, method=None):
        server, store = _server_store(req, userid, self.options)
        folder = _folder(store, folderid or 'calendar')
        item = folder.event(eventid)
        fields = json.loads(req.stream.read().decode('utf-8'))

        if method == 'accept':
            item.accept(comment=fields.get('comment'), respond=(fields.get('sendResponse')=='true'))
            resp.status = falcon.HTTP_202

        elif method == 'tentativelyAccept':
            item.accept(comment=fields.get('comment'), tentative=True, respond=(fields.get('sendResponse')=='true'))
            resp.status = falcon.HTTP_202

        elif method == 'decline':
            item.decline(comment=fields.get('comment'), respond=(fields.get('sendResponse')=='true'))
            resp.status = falcon.HTTP_202

        elif method == 'attachments':
            if fields['@odata.type'] == '#microsoft.graph.fileAttachment':
                att = item.create_attachment(fields['name'], base64.urlsafe_b64decode(fields['contentBytes']))
                self.respond(req, resp, att, AttachmentResource.fields)
                resp.status = falcon.HTTP_201

        elif method:
            raise falcon.HTTPBadRequest(None, "Unsupported segment '%s'" % method)

    def on_patch(self, req, resp, userid=None, folderid=None, eventid=None, method=None):
        server, store = _server_store(req, userid, self.options)
        folder = _folder(store, folderid or 'calendar')
        item = folder.event(eventid)

        fields = json.loads(req.stream.read().decode('utf-8'))

        for field, value in fields.items():
            if field in self.set_fields:
                self.set_fields[field](item, value)

        self.respond(req, resp, item, EventResource.fields)

    def on_delete(self, req, resp, userid=None, folderid=None, eventid=None):
        server, store = _server_store(req, userid, self.options)
        folder = _folder(store, folderid or 'calendar')
        event = folder.event(eventid)
        folder.delete(event)

        self.respond_204(resp)
Example #2
0
class EventResource(ItemResource):
    fields = ItemResource.fields.copy()
    fields.update({
        'id':
        lambda item: item.eventid,
        'subject':
        lambda item: item.subject,
        'recurrence':
        recurrence_json,
        'start':
        lambda req, item: _tzdate(item.start, item.tzinfo, req),
        'end':
        lambda req, item: _tzdate(item.end, item.tzinfo, req),
        'location':
        location_json,
        'importance':
        lambda item: item.urgency,
        'sensitivity':
        lambda item: item.sensitivity,
        'hasAttachments':
        lambda item: item.has_attachments,
        'body':
        lambda req, item: get_body(req, item),
        'isReminderOn':
        lambda item: item.reminder,
        'reminderMinutesBeforeStart':
        lambda item: item.reminder_minutes,
        'attendees':
        lambda item: attendees_json(item),
        'bodyPreview':
        lambda item: item.body_preview,
        'isAllDay':
        lambda item: item.all_day,
        'showAs':
        lambda item: show_as_map[item.show_as],
        'seriesMasterId':
        lambda item: item.item.eventid
        if item.recurring and isinstance(item, kopano.Occurrence) else None,
        'type':
        lambda item: event_type(item),
        'responseRequested':
        lambda item: item.response_requested,
        'iCalUId':
        lambda item: kopano.hex(kopano.bdec(item.icaluid))
        if item.icaluid else None,  # graph uses hex!?
        'organizer':
        lambda item: get_email(item.from_),
        'isOrganizer':
        lambda item: item.from_.email == item.sender.email,
        'isCancelled':
        lambda item: item.canceled,
        'responseStatus':
        lambda item: responsestatus_json(item),
        # 8.7.x does not have onlinemeetingurl attribute, so we must check if its there for compatibility
        'onlineMeetingUrl':
        lambda item: item.onlinemeetingurl
        if hasattr(item, 'onlinemeetingurl') else ''
    })

    set_fields = {
        'subject':
        lambda item, arg: setattr(item, 'subject', arg),
        'location':
        lambda item, arg: location_set(item, arg),
        'body':
        set_body,
        'start':
        lambda item, arg: set_date(item, 'start', arg),
        'end':
        lambda item, arg: set_date(item, 'end', arg),
        'attendees':
        lambda item, arg: attendees_set(item, arg),
        'recurrence':
        recurrence_set,
        'isAllDay':
        lambda item, arg: setattr(item, 'all_day', arg),
        'isReminderOn':
        lambda item, arg: setattr(item, 'reminder', arg),
        'reminderMinutesBeforeStart':
        lambda item, arg: setattr(item, 'reminder_minutes', arg),
        # 8.7.x does not have onlinemeetingurl attribute, so we must check if its there for compatibility
        'onlineMeetingUrl':
        lambda item, arg: setattr(item, 'onlinemeetingurl', arg)
        if hasattr(item, 'onlinemeetingurl') else None,
    }

    # TODO delta functionality seems to include expanding recurrences!? check with MSGE

    def get_event(self, folder, eventid):
        try:
            return folder.event(eventid)
        except binascii.Error:
            raise HTTPBadRequest('Event id is malformed')
        except kopano.errors.NotFoundError:
            raise HTTPNotFound(description='Item not found')

    @experimental
    def handle_get_attachments(self, req, resp, event):
        attachments = list(event.attachments(embedded=True))
        data = (attachments, DEFAULT_TOP, 0, len(attachments))
        self.respond(req, resp, data, AttachmentResource.fields)

    def handle_get_instances(self, req, resp, event):
        start, end = _start_end(req)

        def yielder(**kwargs):
            for occ in event.occurrences(start, end, **kwargs):
                yield occ

        data = self.generator(req, yielder)
        self.respond(req, resp, data)

    def handle_get(self, req, resp, event):
        self.respond(req, resp, event)

    def on_get(self,
               req,
               resp,
               userid=None,
               folderid=None,
               eventid=None,
               method=None):
        handler = None

        if method == 'attachments':
            handler = self.handle_get_attachments

        elif method == 'instances':
            handler = self.handle_get_instances

        elif method:
            raise HTTPBadRequest("Unsupported event segment '%s'" % method)

        else:
            handler = self.handle_get

        server, store, userid = _server_store(req, userid, self.options)
        folder = _folder(store, folderid or 'calendar')
        event = self.get_event(folder, eventid)
        handler(req, resp, event=event)

    def handle_post_accept(self, req, resp, fields, item):
        _ = req.context.i18n.gettext
        self.validate_json(mr_schema, fields)
        item.accept(comment=fields.get('comment'),
                    respond=(fields.get('sendResponse', True)),
                    subject_prefix=_("Accepted"))
        resp.status = falcon.HTTP_202

    def handle_post_tentativelyAccept(self, req, resp, fields, item):
        _ = req.context.i18n.gettext
        self.validate_json(mr_schema, fields)
        item.accept(comment=fields.get('comment'),
                    tentative=True,
                    respond=(fields.get('sendResponse', True)),
                    subject_prefix=_("Tentatively accepted"))
        resp.status = falcon.HTTP_202

    def handle_post_decline(self, req, resp, fields, item):
        _ = req.context.i18n.gettext
        self.validate_json(mr_schema, fields)
        item.decline(comment=fields.get('comment'),
                     respond=(fields.get('sendResponse', True)),
                     subject_prefix=_("Declined"))
        resp.status = falcon.HTTP_202

    @experimental
    def handle_post_attachments(self, req, resp, fields, item):
        if fields['@odata.type'] == '#microsoft.graph.fileAttachment':
            att = item.create_attachment(
                fields['name'],
                base64.urlsafe_b64decode(fields['contentBytes']))
            self.respond(req, resp, att, AttachmentResource.fields)
            resp.status = falcon.HTTP_201

    def on_post(self,
                req,
                resp,
                userid=None,
                folderid=None,
                eventid=None,
                method=None):
        handler = None

        if method == 'accept':
            handler = self.handle_post_accept

        elif method == 'tentativelyAccept':
            handler = self.handle_post_tentativelyAccept

        elif method == 'decline':
            handler = self.handle_post_decline

        elif method == 'attachments':
            handler = self.handle_post_attachments

        elif method:
            raise HTTPBadRequest("Unsupported event segment '%s'" % method)

        else:
            raise HTTPBadRequest("Unsupported in event")

        server, store, userid = _server_store(req, userid, self.options)
        folder = _folder(store, folderid or 'calendar')
        item = self.get_event(folder, eventid)
        fields = self.load_json(req)
        handler(req, resp, fields=fields, item=item)

    def handle_patch(self, req, resp, fields, item):
        for field, value in fields.items():
            if field in self.set_fields:
                self.set_fields[field](item, value)

        self.respond(req, resp, item, EventResource.fields)

    def on_patch(self,
                 req,
                 resp,
                 userid=None,
                 folderid=None,
                 eventid=None,
                 method=None):
        server, store, userid = _server_store(req, userid, self.options)
        folder = _folder(store, folderid or 'calendar')
        item = self.get_event(folder, eventid)

        fields = self.load_json(req)
        self.handle_patch(req, resp, fields=fields, item=item)

    def handle_delete(self, req, resp, folder, item):
        # If meeting is organised, sent cancellation
        if self.fields['isOrganizer'](item):
            item.cancel()
            item.send()

        folder.delete(item)
        self.respond_204(resp)

    def on_delete(self, req, resp, userid=None, folderid=None, eventid=None):
        handler = self.handle_delete

        server, store, userid = _server_store(req, userid, self.options)
        folder = _folder(store, folderid or 'calendar')
        item = self.get_event(folder, eventid)

        handler(req, resp, folder=folder, item=item)
Example #3
0
class EventResource(ItemResource):
    fields = ItemResource.fields.copy()
    fields.update({
        'id': lambda item: item.eventid,
        'subject': lambda item: item.subject,
        'recurrence': recurrence_json,
        'start': lambda req, item: _tzdate(item.start, item.tzinfo, req),
        'end': lambda req, item: _tzdate(item.end, item.tzinfo, req),
        'location': location_json,
        'importance': lambda item: item.urgency,
        'sensitivity': lambda item: item.sensitivity,
        'hasAttachments': lambda item: item.has_attachments,
        'body': lambda req, item: get_body(req, item),
        'isReminderOn': lambda item: item.reminder,
        'reminderMinutesBeforeStart': lambda item: item.reminder_minutes,
        'attendees': lambda item: attendees_json(item),
        'bodyPreview': lambda item: item.body_preview,
        'isAllDay': lambda item: item.all_day,
        'showAs': lambda item: show_as_map[item.show_as],
        'seriesMasterId': lambda item: item.item.eventid if item.recurring and isinstance(item, kopano.Occurrence) else None,
        'type': lambda item: event_type(item),
        'responseRequested': lambda item: item.response_requested,
        'iCalUId': lambda item: kopano.hex(kopano.bdec(item.icaluid)) if item.icaluid else None,  # graph uses hex!?
        'organizer': lambda item: get_email(item.from_),
        'isOrganizer': lambda item: item.from_.email == item.sender.email,
        'isCancelled': lambda item: item.canceled,
        'responseStatus': lambda item: responsestatus_json(item),
        # 8.7.x does not have onlinemeetingurl attribute, so we must check if its there for compatibility
        'onlineMeetingUrl': lambda item: item.onlinemeetingurl if hasattr(item, 'onlinemeetingurl') else ''
    })

    set_fields = {
        'subject': lambda item, arg: setattr(item, 'subject', arg),
        'location': lambda item, arg: location_set(item, arg),
        'body': set_body,
        'start': lambda item, arg: set_date(item, 'start', arg),
        'end': lambda item, arg: set_date(item, 'end', arg),
        'attendees': lambda item, arg: attendees_set(item, arg),
        'recurrence': recurrence_set,
        'isAllDay': lambda item, arg: setattr(item, 'all_day', arg),
        'isReminderOn': lambda item, arg: setattr(item, 'reminder', arg),
        'reminderMinutesBeforeStart': lambda item, arg: setattr(item, 'reminder_minutes', arg),
        # 8.7.x does not have onlinemeetingurl attribute, so we must check if its there for compatibility
        'onlineMeetingUrl': lambda item, arg: setattr(item, 'onlinemeetingurl', arg) if hasattr(item, 'onlinemeetingurl') else None,
    }

    # TODO delta functionality seems to include expanding recurrences!? check with MSGE

    # GET

    @staticmethod
    def get_event(folder, eventid):
        try:
            return folder.event(eventid)
        except (binascii.Error, kopano.errors.ArgumentError):
            raise HTTPBadRequest('Event id is malformed')
        except kopano.errors.NotFoundError:
            raise HTTPNotFound(description='Item not found')

    def _get_event_instances(self, req, resp, folderid, eventid):
        start, end = _start_end(req)

        server, store, userid = req.context.server_store
        folder = _folder(store, folderid)
        event = self.get_event(folder, eventid)

        def yielder(**kwargs):
            for occ in event.occurrences(start, end, **kwargs):
                yield occ
        data = self.generator(req, yielder)
        self.respond(req, resp, data)

    def on_get_instances_by_folderid(self, req, resp, folderid, itemid):
        self._get_event_instances(req, resp, folderid, itemid)

    def on_get_instances(self, req, resp, itemid):
        self._get_event_instances(req, resp, "calendar", itemid)

    def handle_get(self, req, resp, event):
        self.respond(req, resp, event)

    @experimental
    def on_get_events(self, req, resp):
        store = req.context.server_store[1]
        calendar = store.calendar
        data = self.generator(req, calendar.items, calendar.count)
        self.respond(req, resp, data, EventResource.fields)

    def on_get_by_folderid(self, req, resp, folderid):
        """Get events of a specific folder."""
        _, store, _ = req.context.server_store
        folder = store.folder(folderid)
        store.calendar = folder
        data = self.generator(req, store.calendar.items, store.calendar.count)
        self.respond(req, resp, data, EventResource.fields)

    def on_get_by_eventid(self, req, resp, itemid):
        store = req.context.server_store[1]
        folder = _folder(store, "calendar")
        event = self.get_event(folder, itemid)
        self.respond(req, resp, event, self.fields)

    def on_get_by_folderid_eventid(self, req, resp, folderid, itemid):
        store = req.context.server_store[1]
        folder = _folder(store, folderid)
        event = self.get_event(folder, itemid)
        self.respond(req, resp, event, self.fields)

    def on_get(self, req, resp, userid=None, folderid=None, itemid=None):
        raise HTTPBadRequest("Unsupported in event")

    # POST

    def _create_event(self, req, resp, folderid):
        """Create an events.

        Args:
            req (Request): Falcon request object.
            resp (Response): Falcon response object.
            folderid (str): folder ID which the event be created in.

        Raises:
            HTTPBadRequest: invalid argument error or request has empty payload.
        """
        if not req.content_length:
            raise HTTPBadRequest("request has empty payload")
        fields = req.context.json_data
        self.validate_json(event_schema.create_schema_validator, fields)

        store = req.context.server_store[1]
        folder = _folder(store, folderid)
        try:
            item = self.create_message(folder, fields, EventResource.set_fields)
        except kopano.errors.ArgumentError as e:
            raise HTTPBadRequest("Invalid argument error '{}'".format(e))
        if fields.get('attendees', None):
            # NOTE(longsleep): Sending can fail with NO_ACCCESS if no permission to outbox.
            item.send()
        self.respond(req, resp, item, EventResource.fields)

    @experimental
    def on_post_events(self, req, resp):
        """Handle POST request to create event in the "calendar" folder.

        Args:
            req (Request): Falcon request object.
            resp (Response): Falcon response object.
        """
        self._create_event(req, resp, "calendar")

    @experimental
    def on_post_by_folderid(self, req, resp, folderid):
        """Handle POST request to create an event in a specific folder.

        Args:
            req (Request): Falcon request object.
            resp (Response): Falcon response object.
            folderid (str): folder ID which the event be created in.
        """
        self._create_event(req, resp, folderid)

    def _accept_event(self, req, resp, folderid, eventid):
        fields = req.context.json_data
        self.validate_json(event_schema.action_schema_validator, fields)
        _ = req.context.i18n.gettext
        store = req.context.server_store[1]
        folder = _folder(store, folderid)
        item = self.get_event(folder, eventid)
        item.accept(comment=fields.get('comment'), respond=(fields.get('sendResponse', True)), subject_prefix=_("Accepted"))
        resp.status = falcon.HTTP_202

    def on_post_accept_event_by_folderid(self, req, resp, folderid, itemid):
        self._accept_event(req, resp, folderid, itemid)

    def on_post_accept_event(self, req, resp, itemid):
        self._accept_event(req, resp, "calendar", itemid)

    def handle_post_tentativelyAccept(self, req, resp, fields, item):
        _ = req.context.i18n.gettext
        self.validate_json(event_schema.action_schema_validator, fields)
        item.accept(comment=fields.get('comment'), tentative=True, respond=(fields.get('sendResponse', True)), subject_prefix=_("Tentatively accepted"))
        resp.status = falcon.HTTP_202

    def _decline_event(self, req, resp, folderid, itemid):
        fields = req.context.json_data
        self.validate_json(event_schema.action_schema_validator, fields)
        _ = req.context.i18n.gettext
        store = req.context.server_store[1]
        self.validate_json(event_schema.action_schema_validator, fields)
        folder = _folder(store, folderid)
        item = self.get_event(folder, itemid)
        item.decline(comment=fields.get('comment'), respond=(fields.get('sendResponse', True)), subject_prefix=_("Declined"))
        resp.status = falcon.HTTP_202

    def on_post_decline_event_by_folderid(self, req, resp, folderid, itemid):
        self._decline_event(req, resp, folderid, itemid)

    def on_post_decline_event(self, req, resp, itemid):
        self._decline_event(req, resp, "calendar", itemid)

    def on_post(self, req, resp, userid=None, folderid=None, itemid=None, method=None):
        handler = None

        if method == 'tentativelyAccept':
            handler = self.handle_post_tentativelyAccept

        elif method:
            raise HTTPBadRequest("Unsupported event segment '%s'" % method)

        else:
            raise HTTPBadRequest("Unsupported in event")

        server, store, userid = req.context.server_store
        folder = _folder(store, folderid or 'calendar')
        item = self.get_event(folder, itemid)
        fields = req.context.json_data
        handler(req, resp, fields=fields, item=item)

    # PATCH

    def _update_event(self, req, resp, folderid, itemid):
        """Update an event.

        Args:
            req (Request): Falcon request object.
            resp (Response): Falcon response object.
            folderid (str): folder ID which the event exists in.
            itemid (str): item/event ID which should be updated.
        """
        fields = req.context.json_data
        store = req.context.server_store[1]
        folder = _folder(store, folderid)
        item = self.get_event(folder, itemid)

        for field, value in fields.items():
            if field in self.set_fields:
                self.set_fields[field](item, value)

        self.respond(req, resp, item, self.fields)

    def on_patch_by_eventid(self, req, resp, itemid):
        """Handle PATCH request for a specific event in 'calendar' folder.

        Args:
            req (Request): Falcon request object.
            resp (Response): Falcon response object.
            itemid (str): item/event ID which should be updated.
        """
        self._update_event(req, resp, "calendar", itemid)

    def on_patch_by_folderid_eventid(self, req, resp, folderid, itemid):
        """Handle PATCH request for a specific event in a specific folder.

        Args:
            req (Request): Falcon request object.
            resp (Response): Falcon response object.
            folderid (str): folder ID which the event exists in.
            itemid (str): item/event ID which should be updated.
        """
        self._update_event(req, resp, folderid, itemid)

    # DELETE

    def _delete_event(self, req, resp, folderid, itemid):
        """Delete an event.

        Args:
            req (Request): Falcon request object.
            resp (Response): Falcon response object.
            folderid (str): folder ID which the event exists in.
            itemid (str): item/event ID which should be deleted.
        """
        server, store, userid = req.context.server_store
        folder = _folder(store, folderid)
        event = self.get_event(folder, itemid)

        # If meeting is organised, sent cancellation
        if self.fields['isOrganizer'](event):
            event.cancel()
            event.send()

        folder.delete(event)
        self.respond_204(resp)

    def on_delete_by_eventid(self, req, resp, itemid):
        """Handle DELETE request for a specific event in 'calendar' folder.

        Args:
            req (Request): Falcon request object.
            resp (Response): Falcon response object.
            itemid (str): item/event ID which should be deleted.
        """
        self._delete_event(req, resp, "calendar", itemid)

    def on_delete_by_folderid_eventid(self, req, resp, folderid, itemid):
        """Handle DELETE request for a specific event in a specific folder.

        Args:
            req (Request): Falcon request object.
            resp (Response): Falcon response object.
            folderid (str): folder ID which the event exists in.
            itemid (str): item/event ID which should be deleted.
        """
        self._delete_event(req, resp, folderid, itemid)
Example #4
0
class EventResource(ItemResource):
    fields = ItemResource.fields.copy()
    fields.update({
        'id':
        lambda item: item.eventid,
        'subject':
        lambda item: item.subject,
        'recurrence':
        recurrence_json,
        'start':
        lambda item: {
            'dateTime': _date(item.start, True),
            'timeZone': 'UTC'
        } if item.start else None,
        'end':
        lambda item: {
            'dateTime': _date(item.end, True),
            'timeZone': 'UTC'
        } if item.end else None,
        'location':
        lambda item: {
            'displayName': item.location,
            'address': {}
        },  # TODO
        'importance':
        lambda item: item.urgency,
        'sensitivity':
        lambda item: sensitivity_map[item.sensitivity],
        'hasAttachments':
        lambda item: item.has_attachments,
        'body':
        lambda req, item: get_body(req, item),
        'isReminderOn':
        lambda item: item.reminder,
        'reminderMinutesBeforeStart':
        lambda item: item.reminder_minutes,
        'attendees':
        lambda item: attendees_json(item),
        'bodyPreview':
        lambda item: item.text[:255],
        'isAllDay':
        lambda item: item.all_day,
        'showAs':
        lambda item: show_as_map[item.show_as],
        'seriesMasterId':
        lambda item: item.item.eventid
        if isinstance(item, kopano.Occurrence) else None,
        'type':
        lambda item: event_type(item),
        'responseRequested':
        lambda item: item.response_requested,
        'iCalUId':
        lambda item: kopano.hex(kopano.bdec(item.icaluid))
        if item.icaluid else None,  # graph uses hex!?
        'organizer':
        lambda item: get_email(item.from_),
        'isOrganizer':
        lambda item: item.from_.email == item.sender.email,
    })

    set_fields = {
        'subject': lambda item, arg: setattr(item, 'subject', arg),
        'start': lambda item, arg: setdate(item, 'start', arg),
        'end': lambda item, arg: setdate(item, 'end', arg),
    }

    # TODO delta functionality seems to include expanding recurrences!? check with MSGE

    def on_get(self,
               req,
               resp,
               userid=None,
               folderid=None,
               itemid=None,
               method=None):
        server, store = _server_store(req, userid)
        folder = utils._folder(store, folderid or 'calendar')
        event = folder.event(itemid)

        if method == 'attachments':
            attachments = list(event.attachments(embedded=True))
            data = (attachments, TOP, 0, len(attachments))
            self.respond(req, resp, data, AttachmentResource.fields)

        elif method == 'instances':
            start, end = _start_end(req)
            data = (event.occurrences(start, end), TOP, 0, 0)
            self.respond(req, resp, data)

        else:
            self.respond(req, resp, event)

    def on_post(self,
                req,
                resp,
                userid=None,
                folderid=None,
                itemid=None,
                method=None):
        server, store = _server_store(req, userid)
        folder = utils._folder(store, folderid or 'calendar')
        item = folder.event(itemid)

        if method == 'attachments':
            fields = json.loads(req.stream.read().decode('utf-8'))
            if fields['@odata.type'] == '#microsoft.graph.fileAttachment':
                item.create_attachment(
                    fields['name'],
                    base64.urlsafe_b64decode(fields['contentBytes']))

    def on_patch(self,
                 req,
                 resp,
                 userid=None,
                 folderid=None,
                 itemid=None,
                 method=None):
        server, store = _server_store(req, userid)
        folder = utils._folder(store, folderid or 'calendar')
        item = folder.event(itemid)

        fields = json.loads(req.stream.read().decode('utf-8'))

        for field, value in fields.items():
            if field in self.set_fields:
                self.set_fields[field](item, value)

        self.respond(req, resp, item, EventResource.fields)

    def on_delete(self, req, resp, userid=None, folderid=None, itemid=None):
        server, store = _server_store(req, userid)
        folder = utils._folder(store, folderid or 'calendar')
        event = folder.event(itemid)
        folder.delete(event)