예제 #1
0
    def processInboxItem(
        self, root, directory, principal, request, inbox,
        inboxItem, uuid, uri
    ):
        """
        Run an individual inbox item through implicit scheduling and remove
        the inbox item.
        """

        log.debug("Processing inbox item %s" % (inboxItem,))

        txn = request._newStoreTransaction

        ownerPrincipal = principal
        cua = "urn:x-uid:%s" % (uuid,)
        owner = LocalCalendarUser(
            cua, ownerPrincipal,
            inbox, ownerPrincipal.scheduleInboxURL()
        )

        calendar = yield inboxItem.componentForUser()
        if calendar.mainType() is not None:
            try:
                method = calendar.propertyValue("METHOD")
                log.info("Inbox item method is %s" % (method,))
            except ValueError:
                returnValue(None)

            if method == "REPLY":
                # originator is attendee sending reply
                originator = calendar.getAttendees()[0]
            else:
                # originator is the organizer
                originator = calendar.getOrganizer()

            principalCollection = directory.principalCollection
            originatorPrincipal = yield principalCollection.principalForCalendarUserAddress(originator)
            originator = LocalCalendarUser(originator, originatorPrincipal)
            recipients = (owner,)

            scheduler = DirectScheduler(request, inboxItem)
            # Process inbox item
            yield scheduler.doSchedulingViaPUT(
                originator, recipients, calendar,
                internal_request=False, noAttendeeRefresh=True
            )
        else:
            log.warn("Removing invalid inbox item: %s" % (uri,))

        #
        # Remove item
        #
        yield inboxItem.storeRemove(request, True, uri)
        yield txn.commit()
        returnValue(True)
예제 #2
0
    def test_no_freebusy(self):

        data = """BEGIN:VCALENDAR
VERSION:2.0
METHOD:REQUEST
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VFREEBUSY
UID:12345-67890
DTSTART:20080601T120000Z
DTEND:20080601T130000Z
ORGANIZER;CN="User 01":mailto:[email protected]
ATTENDEE:mailto:[email protected]
ATTENDEE:mailto:[email protected]
END:VFREEBUSY
END:VCALENDAR
"""

        scheduler = iMIPProcessing.FakeSchedule(
            LocalCalendarUser("mailto:[email protected]", None),
            Component.fromString(data)
        )
        recipients = (RemoteCalendarUser("mailto:[email protected]"),)
        responses = ScheduleResponseQueue("REQUEST", responsecode.OK)

        delivery = ScheduleViaIMip(scheduler, recipients, responses, True)
        yield delivery.generateSchedulingResponses()

        self.assertEqual(len(responses.responses), 1)
        self.assertEqual(str(responses.responses[0].reqstatus), iTIPRequestStatus.SERVICE_UNAVAILABLE)
예제 #3
0
def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy):
    """
    Generate a free-busy REPORT.
    (CalDAV-access-09, section 7.8)
    """
    if not self.isCollection():
        log.error("freebusy report is only allowed on collection resources {s!r}", s=self)
        raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not a calendar collection"))

    if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"):
        raise ValueError("{CalDAV:}free-busy-query expected as root element, not %s." % (freebusy.sname(),))

    timerange = freebusy.timerange
    if not timerange.valid():
        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified"))

    fbset = []

    accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes())
    if accepted_type is None:
        raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))

    def getCalendarList(calresource, uri):  # @UnusedVariable
        """
        Store the calendars that match the query in L{fbset} which will then be used with the
        freebusy query.

        @param calresource: the L{CalDAVResource} for a calendar collection.
        @param uri: the uri for the calendar collection resource.
        """

        fbset.append(calresource._newStoreObject)
        return succeed(True)

    # Run report taking depth into account
    depth = request.headers.getHeader("depth", "0")
    yield report_common.applyToCalendarCollections(self, request, request.uri, depth, getCalendarList, (caldavxml.ReadFreeBusy(),))

    # Do the actual freebusy query against the set of matched calendars
    principal = yield self.resourceOwnerPrincipal(request)
    organizer = recipient = LocalCalendarUser(principal.canonicalCalendarUserAddress(), principal.record)
    timerange = Period(timerange.start, timerange.end)
    try:
        fbresult = yield FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange).generateAttendeeFreeBusyResponse(fbset=fbset, method=None)
    except NumberOfMatchesWithinLimits:
        log.error("Too many matching components in free-busy report")
        raise HTTPError(ErrorResponse(
            responsecode.FORBIDDEN,
            davxml.NumberOfMatchesWithinLimits(),
            "Too many components"
        ))
    except TimeRangeLowerLimit, e:
        raise HTTPError(ErrorResponse(
            responsecode.FORBIDDEN,
            caldavxml.MinDateTime(),
            "Time-range value too far in the past. Must be on or after %s." % (str(e.limit),)
        ))
예제 #4
0
    def test_iMIP_delivery(self):

        data = """BEGIN:VCALENDAR
VERSION:2.0
METHOD:REQUEST
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
UID:12345-67890
DTSTART:20080601T120000Z
DTEND:20080601T130000Z
ORGANIZER;CN="User 01":mailto:[email protected]
ATTENDEE:mailto:[email protected]
ATTENDEE:mailto:[email protected]
END:VEVENT
END:VCALENDAR
"""

        results = []

        class FakeSender(object):

            def outbound(self, txn, fromAddr, toAddr, calendar):
                results.append((fromAddr, toAddr))
                return succeed(None)
        self.patch(IMIPInvitationWork, "mailSender", FakeSender())

        scheduler = iMIPProcessing.FakeSchedule(
            LocalCalendarUser("mailto:[email protected]", None),
            Component.fromString(data)
        )
        scheduler.txn = self.transactionUnderTest()
        recipients = (RemoteCalendarUser("mailto:[email protected]"),)
        responses = ScheduleResponseQueue("REQUEST", responsecode.OK)

        delivery = ScheduleViaIMip(scheduler, recipients, responses, False)
        yield delivery.generateSchedulingResponses()

        self.assertEqual(len(responses.responses), 1)
        self.assertEqual(str(responses.responses[0].reqstatus), iTIPRequestStatus.MESSAGE_SENT)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

        self.assertEqual(len(results), 1)
        self.assertEqual(results[0], ("mailto:[email protected]", "mailto:[email protected]",))
예제 #5
0
    def _processFBURL(self, request):
        #
        # Check authentication and access controls
        #
        yield self.authorize(request, (davxml.Read(),))

        # Extract query parameters from the URL
        args = ('start', 'end', 'duration', 'token', 'format', 'user',)
        for arg in args:
            setattr(self, arg, request.args.get(arg, [None])[0])

        # Some things we do not handle
        if self.token or self.user:
            raise HTTPError(ErrorResponse(
                responsecode.NOT_ACCEPTABLE,
                (calendarserver_namespace, "supported-query-parameter"),
                "Invalid query parameter",
            ))

        # Check format
        if self.format:
            self.format = self.format.split(";")[0]
            if self.format not in ("text/calendar", "text/plain"):
                raise HTTPError(ErrorResponse(
                    responsecode.NOT_ACCEPTABLE,
                    (calendarserver_namespace, "supported-format"),
                    "Invalid return format requested",
                ))
        else:
            self.format = "text/calendar"

        # Start/end/duration must be valid iCalendar DATE-TIME UTC or DURATION values
        try:
            if self.start:
                self.start = DateTime.parseText(self.start)
                if not self.start.utc():
                    raise ValueError()
            if self.end:
                self.end = DateTime.parseText(self.end)
                if not self.end.utc():
                    raise ValueError()
            if self.duration:
                self.duration = Duration.parseText(self.duration)
        except ValueError:
            raise HTTPError(ErrorResponse(
                responsecode.BAD_REQUEST,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # Sanity check start/end/duration

        # End and duration cannot both be present
        if self.end and self.duration:
            raise HTTPError(ErrorResponse(
                responsecode.NOT_ACCEPTABLE,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # Duration must be positive
        if self.duration and self.duration.getTotalSeconds() < 0:
            raise HTTPError(ErrorResponse(
                responsecode.BAD_REQUEST,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # Now fill in the missing pieces
        if self.start is None:
            self.start = DateTime.getNowUTC()
            self.start.setHHMMSS(0, 0, 0)
        if self.duration:
            self.end = self.start + self.duration
        if self.end is None:
            self.end = self.start + Duration(days=config.FreeBusyURL.TimePeriod)

        # End > start
        if self.end <= self.start:
            raise HTTPError(ErrorResponse(
                responsecode.BAD_REQUEST,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long)

        # Now lookup the principal details for the targeted user
        principal = (yield self.parent.principalForRecord())

        # Pick the first mailto cu address or the first other type
        cuaddr = None
        for item in principal.calendarUserAddresses():
            if cuaddr is None:
                cuaddr = item
            if item.startswith("mailto:"):
                cuaddr = item
                break

        # Get inbox details
        inboxURL = principal.scheduleInboxURL()
        if inboxURL is None:
            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox URL for principal: %s" % (principal,)))
        try:
            inbox = (yield request.locateResource(inboxURL))
        except:
            log.error("No schedule inbox for principal: {p}", p=principal)
            inbox = None
        if inbox is None:
            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,)))

        organizer = recipient = LocalCalendarUser(cuaddr, principal.record)
        recipient.inbox = inbox._newStoreObject
        attendeeProp = Property("ATTENDEE", recipient.cuaddr)
        timerange = Period(self.start, self.end)

        fbresult = yield FreebusyQuery(
            organizer=organizer,
            recipient=recipient,
            attendeeProp=attendeeProp,
            timerange=timerange,
        ).generateAttendeeFreeBusyResponse()

        response = Response()
        response.stream = MemoryStream(str(fbresult))
        response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,)))

        returnValue(response)
예제 #6
0
    def test_queueAttendeeUpdate_count_suppressed(self):

        self.patch(config.Scheduling.Options, "AttendeeRefreshCountLimit", 5)
        self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)

        calendar_small = Component.fromString("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
UID:12345-67890
DTSTART:20080601T120000Z
DTEND:20080601T130000Z
ORGANIZER:urn:uuid:user01
ATTENDEE:urn:uuid:user01
ATTENDEE:urn:uuid:user02
ATTENDEE:urn:uuid:user03
ATTENDEE:urn:uuid:user04
END:VEVENT
END:VCALENDAR
""")
        itip_small = Component.fromString("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
METHOD:REPLY
BEGIN:VEVENT
UID:12345-67890
DTSTART:20080601T120000Z
DTEND:20080601T130000Z
ORGANIZER:urn:uuid:user01
ATTENDEE;PARTSTAT="ACCEPTED":urn:uuid:user02
END:VEVENT
END:VCALENDAR
""")
        calendar_large = Component.fromString("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
UID:12345-67890
DTSTART:20080601T120000Z
DTEND:20080601T130000Z
ORGANIZER:urn:uuid:user01
ATTENDEE:urn:uuid:user01
ATTENDEE:urn:uuid:user02
ATTENDEE:urn:uuid:user03
ATTENDEE:urn:uuid:user04
ATTENDEE:urn:uuid:user05
ATTENDEE:urn:uuid:user06
ATTENDEE:urn:uuid:user07
ATTENDEE:urn:uuid:user08
ATTENDEE:urn:uuid:user09
ATTENDEE:urn:uuid:user10
END:VEVENT
END:VCALENDAR
""")
        itip_large = Component.fromString("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
METHOD:REPLY
BEGIN:VEVENT
UID:12345-67890
DTSTART:20080601T120000Z
DTEND:20080601T130000Z
ORGANIZER:urn:uuid:user01
ATTENDEE;PARTSTAT="ACCEPTED":urn:uuid:user02
END:VEVENT
END:VCALENDAR
""")

        for count, calendar, itip, result, msg in (
            (5, calendar_small, itip_small, 1, "Small, count=5"),
            (5, calendar_large, itip_large, 0, "Large, count=5"),
            (0, calendar_small, itip_small, 1, "Small, count=0"),
            (0, calendar_large, itip_large, 1, "Large, count=0"),
        ):
            config.Scheduling.Options.AttendeeRefreshCountLimit = count
            processor = FakeImplicitProcessor()
            processor.txn = ""
            processor.recipient_calendar = calendar.duplicate()
            processor.uid = processor.recipient_calendar.newUID()
            processor.recipient_calendar_resource = FakeResource()
            processor.message = itip.duplicate()
            processor.message.newUID(processor.uid)
            processor.originator = LocalCalendarUser(None, None)
            processor.recipient = LocalCalendarUser(None, None)
            processor.uid = calendar.resourceUID()
            processor.noAttendeeRefresh = False

            processed = yield processor.doImplicitOrganizerUpdate()
            self.assertTrue(processed[3] is not None, msg=msg)
            self.assertEqual(processor.batches, result, msg=msg)
예제 #7
0
def importCollectionComponent(store, component):
    """
    Import a component representing a collection (e.g. VCALENDAR) into the
    store.

    The homeUID and collection resource name the component will be imported
    into is derived from the SOURCE property on the VCALENDAR (which must
    be present).  The code assumes it will be a URI with slash-separated parts
    with the penultimate part specifying the homeUID and the last part
    specifying the calendar resource name.  The NAME property will be used
    to set the DAV:display-name, while the COLOR property will be used to set
    calendar-color.

    Subcomponents (e.g. VEVENTs) are grouped into resources by UID.  Objects
    which have a UID already in use within the home will be skipped.

    @param store: The db store to add the component to
    @type store: L{IDataStore}
    @param component: The component to store
    @type component: L{twistedcaldav.ical.Component}
    """

    sourceURI = component.propertyValue("SOURCE")
    if not sourceURI:
        raise ImportException("Calendar is missing SOURCE property")

    ownerUID, collectionResourceName = sourceURI.strip("/").split("/")[-2:]

    dir = store.directoryService()
    ownerRecord = yield dir.recordWithUID(ownerUID)
    if not ownerRecord:
        raise ImportException("{} is not in the directory".format(ownerUID))

    # Set properties on the collection
    txn = store.newTransaction()
    home = yield txn.calendarHomeWithUID(ownerUID, create=True)
    collection = yield home.childWithName(collectionResourceName)
    if not collection:
        print("Creating calendar: {}".format(collectionResourceName))
        collection = yield home.createChildWithName(collectionResourceName)
    for propertyName, element in (
        ("NAME", davxml.DisplayName),
        ("COLOR", customxml.CalendarColor),
    ):
        value = component.propertyValue(propertyName)
        if value is not None:
            setCollectionPropertyValue(collection, element, value)
            print("Setting {name} to {value}".format(name=propertyName,
                                                     value=value))
    yield txn.commit()

    # Populate the collection; NB we use a txn for each object, and we might
    # want to batch them?
    groupedComponents = Component.componentsFromComponent(component)
    for groupedComponent in groupedComponents:

        try:
            uid = list(
                groupedComponent.subcomponents())[0].propertyValue("UID")
        except:
            continue

        # If event is unscheduled or the organizer matches homeUID, store the
        # component

        print("Event UID: {}".format(uid))
        storeDirectly = True
        organizer = groupedComponent.getOrganizer()
        if organizer is not None:
            organizerRecord = yield dir.recordWithCalendarUserAddress(
                organizer)
            if organizerRecord is None:
                # Organizer does not exist, so skip this event
                continue
            else:
                if ownerRecord.uid != organizerRecord.uid:
                    # Owner is not the organizer
                    storeDirectly = False

        if storeDirectly:
            resourceName = "{}.ics".format(str(uuid.uuid4()))
            try:
                yield storeComponentInHomeAndCalendar(store, groupedComponent,
                                                      ownerUID,
                                                      collectionResourceName,
                                                      resourceName)
                print("Imported: {}".format(uid))
            except UIDExistsError:
                # That event is already in the home
                print("Skipping since UID already exists: {}".format(uid))

            except Exception, e:
                print("Failed to import due to: {error}\n{comp}".format(
                    error=e, comp=groupedComponent))

        else:
            # Owner is an attendee, not the organizer
            # Apply the PARTSTATs from the import and from the possibly
            # existing event (existing event takes precedence) to the
            # organizer's copy.

            # Put the attendee copy into the right calendar now otherwise it
            # could end up on the default calendar when the change to the
            # organizer's copy causes an attendee update
            resourceName = "{}.ics".format(str(uuid.uuid4()))
            try:
                yield storeComponentInHomeAndCalendar(store,
                                                      groupedComponent,
                                                      ownerUID,
                                                      collectionResourceName,
                                                      resourceName,
                                                      asAttendee=True)
                print("Imported: {}".format(uid))
            except UIDExistsError:
                # No need since the event is already in the home
                pass

            # Now use the iTip reply processing to update the organizer's copy
            # with the PARTSTATs from the component we're restoring.
            attendeeCUA = ownerRecord.canonicalCalendarUserAddress()
            organizerCUA = organizerRecord.canonicalCalendarUserAddress()
            processor = ImplicitProcessor()
            newComponent = iTipGenerator.generateAttendeeReply(
                groupedComponent, attendeeCUA, method="X-RESTORE")
            txn = store.newTransaction()
            yield processor.doImplicitProcessing(
                txn, newComponent, LocalCalendarUser(attendeeCUA, ownerRecord),
                LocalCalendarUser(organizerCUA, organizerRecord))
            yield txn.commit()