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)
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)
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),) ))
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]",))
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)
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)
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()