def updateVPOLLData(from_component, to_component, attendee): """ Update VPOLL sub-components with voter's response. @param from_component: component to copy from @type from_component: L{Component} @param to_component: component to copy to @type to_component: L{Component} @param attendee: attendee being processed @type attendee: L{Property} """ responses = {} for prop in from_component.properties("POLL-ITEM-ID"): responses[prop.value()] = prop for component in to_component.subcomponents(): if component.name() in ignoredComponents: continue poll_item_id = component.propertyValue("POLL-ITEM-ID") if poll_item_id is None: continue voter = component.getVoterProperty((attendee.value(),)) # If no response - remove if poll_item_id not in responses or not responses[poll_item_id].hasParameter("RESPONSE"): if voter is not None: component.removeProperty(voter) continue # Add or update voter if voter is None: voter = Property("VOTER", attendee.value()) component.addProperty(voter) voter.setParameter("RESPONSE", responses[poll_item_id].parameterValue("RESPONSE"))
def _addTask(self): # Don't perform any operations until the client is up and running if not self._client.started: return succeed(None) calendars = self._calendarsOfType(caldavxml.calendar, "VTODO") while calendars: calendar = self.random.choice(calendars) calendars.remove(calendar) # Copy the template task and fill in some of its fields # to make a new task to create on the calendar. vcalendar = self._taskTemplate.duplicate() vtodo = vcalendar.mainComponent() uid = str(uuid4()) due = self._taskStartDistribution.sample() vtodo.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vtodo.replaceProperty(Property("DUE", due)) vtodo.replaceProperty(Property("UID", uid)) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def exportToDirectory(collections, dirname, convertToMailto=False): """ Export some calendars to a file as their owner would see them. @param calendars: an iterable of L{ICalendar} providers (or L{Deferred}s of same). @param dirname: the path to a directory to store calendar files in; each calendar being exported will have its own .ics file @return: a L{Deferred} which fires when the export is complete. (Note that the file will not be closed.) @rtype: L{Deferred} that fires with C{None} """ for collection in collections: if isinstance(collection, Calendar): homeUID = collection.ownerCalendarHome().uid() calendarProperties = collection.properties() comp = Component.newCalendar() for element, propertyName in ( (davxml.DisplayName, "NAME"), (customxml.CalendarColor, "COLOR"), ): value = calendarProperties.get(PropertyName.fromElement(element), None) if value: comp.addProperty(Property(propertyName, str(value))) source = "/calendars/__uids__/{}/{}/".format(homeUID, collection.name()) comp.addProperty(Property("SOURCE", source)) for obj in (yield collection.calendarObjects()): evt = yield obj.filteredComponent(homeUID, True) for sub in evt.subcomponents(): if sub.name() != 'VTIMEZONE': # Omit all VTIMEZONE components here - we will include them later # when we serialize the whole calendar. if convertToMailto: convertCUAsToMailto(sub) comp.addComponent(sub) filename = os.path.join(dirname, "{}_{}.ics".format(homeUID, collection.name())) with open(filename, 'wb') as fileobj: fileobj.write(comp.getTextWithTimezones(True)) else: # addressbook homeUID = collection.ownerAddressBookHome().uid() filename = os.path.join(dirname, "{}_{}.vcf".format(homeUID, collection.name())) with open(filename, 'wb') as fileobj: for obj in (yield collection.addressbookObjects()): vcard = yield obj.component() fileobj.write(vcard.getText())
def _attendeeDecline(self, component): """ Mark attendee as DECLINED in the component. @param component: @type component: @return: C{bool} indicating whether the PARTSTAT value was in fact changed """ attendee = component.getAttendeeProperty((self.attendee, )) # Possible case where ATTENDEE prop is missing - this happens with a "fake" master sometimes if attendee is None: log.error( "ATTENDEE for user making an attendee change is missing: {attendee}", attendee=self.attendee) return False partstatChanged = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") != "DECLINED" attendee.setParameter("PARTSTAT", "DECLINED") prop = component.getProperty("X-APPLE-NEEDS-REPLY") if prop: component.removeProperty(prop) component.replaceProperty(Property("TRANSP", "TRANSPARENT")) return partstatChanged
def _updateEvent(self): """ Set the ACKNOWLEDGED property on an event. @return: C{None} if there are no events to play with, otherwise a L{Deferred} which fires when the acknowledged change has been made. """ # Only do updates when we reach of the designated minutes past the hour if not self._shouldUpdate(datetime.now().minute): return succeed(None) if not self._client.started: return succeed(None) if self.myEventHref is None: return self._initEvent() event = self._client.eventByHref(self.myEventHref) # Add/update the ACKNOWLEDGED property component = event.component.mainComponent() component.replaceProperty(Property("ACKNOWLEDGED", DateTime.getNowUTC())) d = self._client.changeEvent(event.url) return self._newOperation("update", d)
def _updateEvent(self): """ Try to add a new attendee to an event, or perhaps remove an existing attendee from an event. @return: C{None} if there are no events to play with, otherwise a L{Deferred} which fires when the attendee change has been made. """ if not self._client.started: return succeed(None) # If it does not exist, try to create it calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0] if not calendar.events: return self._initEvent() events = [ event for event in calendar.events.values() if event.url.endswith("event_to_update.ics") ] if not events: return self._initEvent() event = events[0] # Add/update the ACKNOWLEDGED property component = event.component.mainComponent() component.replaceProperty( Property("ACKNOWLEDGED", DateTime.getNowUTC())) d = self._client.changeEvent(event.url) return self._newOperation("update", d)
def attendeeProperty(self, params={}): """ Returns a pycalendar ATTENDEE property for this record. @params: extra parameters such as MEMBER to add to ATTENDEE property @type: C{dict} @return: the attendee property @rtype: C{Property} """ params = params.copy() if params else params if "PARTSTAT" not in params: params["PARTSTAT"] = "NEEDS-ACTION" if "CN"not in params: if self.displayName: params["CN"] = self.displayName.encode("utf-8") if "EMAIL" not in params: if hasattr(self, "emailAddresses"): params["EMAIL"] = list(self.emailAddresses)[0].encode("utf-8") if "CUTYPE" not in params: cuType = self.getCUType() if cuType != "INDIVIDUAL": params["CUTYPE"] = cuType return Property("ATTENDEE", self.canonicalCalendarUserAddress().encode("utf-8"), params=params)
def _updateEvent(self): """ Set the ACKNOWLEDGED property on an event. @return: C{None} if there are no events to play with, otherwise a L{Deferred} which fires when the acknowledged change has been made. """ # Only do updates when we reach of the designated minutes past the hour if not self._shouldUpdate(datetime.now().minute): return succeed(None) if not self._client.started: return succeed(None) # If it does not exist, try to create it calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0] if not calendar.events: return self._initEvent() events = [ event for event in calendar.events.values() if event.url.endswith("event_to_update.ics") ] if not events: return self._initEvent() event = events[0] # Add/update the ACKNOWLEDGED property component = event.component.mainComponent() component.replaceProperty( Property("ACKNOWLEDGED", DateTime.getNowUTC())) d = self._client.changeEvent(event.url) return self._newOperation("update", d)
def filter(self, ical): """ Filter the supplied iCalendar object using the request information. @param ical: iCalendar object @type ical: L{Component} or C{str} @return: L{Component} for the filtered calendar data """ master = ical.masterComponent() for component in tuple(ical.subcomponents()): if component.name() in ignoredComponents: continue rid = component.getRecurrenceIDUTC() if rid is None: continue if component.hasProperty(Component.HIDDEN_INSTANCE_PROPERTY): rid = component.getRecurrenceIDUTC() ical.removeComponent(component) # Add EXDATE and try to preserve same timezone as DTSTART if master is not None: dtstart = master.getProperty("DTSTART") if dtstart is not None and not dtstart.value().isDateOnly( ) and dtstart.value().local(): rid.adjustTimezone(dtstart.value().getTimezone()) master.addProperty(Property("EXDATE", [ rid, ])) return ical
def _initEvent(self): # Don't perform any operations until the client is up and running if not self._client.started: return succeed(None) try: calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0] except IndexError: # There is no calendar return succeed(None) self.myEventHref = '{}{}.ics'.format(calendar.url, str(uuid4())) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) vevent.replaceProperty(Property("DESCRIPTION", "AlarmAcknowledger")) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) d = self._client.addEvent(self.myEventHref, vcalendar) return self._newOperation("create", d)
def _makeSelfOrganizer(self): organizer = Property( name=u'ORGANIZER', value=self.email, params={ 'CN': self.record.commonName, }, ) return organizer
def attachProperty(self): """ Return an iCalendar ATTACH property for this attachment. """ attach = Property("ATTACH", "", valuetype=Value.VALUETYPE_URI) location = (yield self.updateProperty(attach)) returnValue(( attach, location, ))
def _transferVPOLLData(self, serverComponent, clientComponent): changed = False # Get the matching VVOTER component in each VPOLL serverVoter = serverComponent.voterComponentForVoter(self.attendee) clientVoter = clientComponent.voterComponentForVoter(self.attendee) # Now get a map of each response serverMap = serverVoter.voteMap() clientMap = clientVoter.voteMap() # Remove missing for poll_id in set(serverMap.keys()) - set(clientMap.keys()): serverVoter.removeComponent(serverMap[poll_id]) changed = True # Add new ones for poll_id in set(clientMap.keys()) - set(serverMap.keys()): vote = clientMap[poll_id].duplicate() vote.replaceProperty( Property("LAST-MODIFIED", DateTime.getNowUTC())) serverVoter.addComponent(vote) changed = True # Look for response change for poll_id in set(serverMap.keys()) & set(clientMap.keys()): server_vote = serverMap[poll_id] client_vote = clientMap[poll_id] server_response = server_vote.propertyValue("RESPONSE") client_response = client_vote.propertyValue("RESPONSE") if server_response != client_response: if client_response is not None: server_vote.replaceProperty( Property("RESPONSE", client_response)) else: server_vote.removeProperty("RESPONSE") server_vote.replaceProperty( Property("LAST-MODIFIED", DateTime.getNowUTC())) changed = True return changed
def _makeSelfAttendee(self): attendee = Property( name=u'ATTENDEE', value=self.email, params={ 'CN': self.record.commonName, 'CUTYPE': 'INDIVIDUAL', 'PARTSTAT': 'ACCEPTED', }, ) return attendee
def _defaultFilter(self, ical): """ There is no per-user component. Instead apply default properties to the data for this user. @param ical: the iCalendar object to process @type ical: L{Component} """ # TRANSP property behavior: if the event is all-day add TRANSP:TRANSPARENT, otherwise do nothing comp = ical.mainComponent() if comp.name() == "VEVENT" and comp.propertyValue("DTSTART").isDateOnly() and not comp.hasProperty("TRANSP"): ical.addPropertyToAllComponents(Property("TRANSP", "TRANSPARENT"))
def _transferProperty(self, propName, serverComponent, clientComponent): changed = False serverProp = serverComponent.getProperty(propName) clientProp = clientComponent.getProperty(propName) if serverProp != clientProp: if clientProp: serverComponent.replaceProperty(Property(propName, clientProp.value())) else: serverComponent.removeProperty(serverProp) changed = True return changed
def _addEvent(self): # Don't perform any operations until the client is up and running if not self._client.started: return succeed(None) calendar = self._getRandomCalendarOfType('VEVENT') if not calendar: # No VEVENT calendars, so no new event... return succeed(None) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration( seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def _initEvent(self): if not self._client.started: return succeed(None) # If it already exists, don't re-create calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0] if calendar.events: events = [ event for event in calendar.events.values() if event.url.endswith("event_to_update.ics") ] if events: return succeed(None) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration( seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) href = '%s%s' % (calendar.url, "event_to_update.ics") d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def _addEvent(self): if not self._client.started: return succeed(None) calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT") while calendars: calendar = self.random.choice(calendars) calendars.remove(calendar) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration( seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def _defaultMerge(self, component): """ Handle default property values during a merge. @param component: the iCalendar component to process @type component: L{Component} """ # Special handling for TRANSP: if it is not present on an all-day event, # add TRANSP:OPAQUE so that the default is explicitly included, since otherwise # L_defaultFilter) will add TRANSP:TRANSPARENT if component.name() == "VEVENT" and component.propertyValue("DTSTART").isDateOnly() and not component.hasProperty("TRANSP"): component.addProperty(Property("TRANSP", "OPAQUE"))
def _invite(self): """ Try to add a new event, or perhaps remove an existing attendee from an event. @return: C{None} if there are no events to play with, otherwise a L{Deferred} which fires when the attendee change has been made. """ if not self._client.started: return succeed(None) # Find calendars which are eligible for invites calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT") while calendars: # Pick one at random from which to try to create an event # to modify. calendar = self.random.choice(calendars) calendars.remove(calendar) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration( seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) vevent.addProperty(self._client._makeSelfOrganizer()) vevent.addProperty(self._client._makeSelfAttendee()) attendees = list(vevent.properties('ATTENDEE')) for _ignore in range(int(self._inviteeCountDistribution.sample())): try: self._addAttendee(vevent, attendees) except CannotAddAttendee: self._failedOperation("invite", "Cannot add attendee") return succeed(None) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addInvite(href, vcalendar) return self._newOperation("invite", d)
def split(self, ical, rid=None, olderUID=None): """ Split the specified iCalendar object. This assumes that L{willSplit} has already been called and returned C{True}. Splitting is done by carving out old instances into a new L{Component} and adjusting the specified component for the on-going set. A RELATED-TO property is added to link old to new. @param ical: the iCalendar object to split @type ical: L{Component} @param rid: recurrence-id where the split should occur, or C{None} to determine it here @type rid: L{DateTime} or C{None} @param olderUID: UID to use for the split off component, or C{None} to generate one here @type olderUID: C{str} or C{None} @return: iCalendar objects for the old and new "carved out" instances @rtype: C{tuple} of two L{Component}'s """ # Find the instance RECURRENCE-ID where a split is going to happen rid = self.whereSplit(ical) if rid is None else rid # Create the old one with a new UID value (or the one passed in) icalOld = ical.duplicate() oldUID = icalOld.newUID(newUID=olderUID) icalOld.onlyPastInstances(rid) # Adjust the current one icalNew = ical.duplicate() icalNew.onlyFutureInstances(rid) # Relate them - add RELATED-TO;RELTYPE=RECURRENCE-SET if not already present if not icalOld.hasPropertyWithParameterMatch( "RELATED-TO", "RELTYPE", "X-CALENDARSERVER-RECURRENCE-SET"): property = Property( "RELATED-TO", str(uuid.uuid5(self.uuid_namespace, oldUID)), params={"RELTYPE": "X-CALENDARSERVER-RECURRENCE-SET"}) icalOld.addPropertyToAllComponents(property) icalNew.addPropertyToAllComponents(property) return ( icalOld, icalNew, )
def processDSN(self, calBody, msgId): calendar = Component.fromString(calBody) # Extract the token (from organizer property) organizer = calendar.getOrganizer() token = self._extractToken(organizer) if not token: log.error("Mail gateway can't find token in DSN {msgid}", msgid=msgId) return txn = self.store.newTransaction(label="MailReceiver.processDSN") records = (yield txn.imipLookupByToken(token)) yield txn.commit() try: # Note the results are returned as utf-8 encoded strings record = records[0] except: # This isn't a token we recognize log.error( "Mail gateway found a token ({token}) but didn't recognize it in message {msgid}", token=token, msgid=msgId, ) returnValue(self.UNKNOWN_TOKEN) calendar.removeAllButOneAttendee(record.attendee) calendar.getOrganizerProperty().setValue(organizer) for comp in calendar.subcomponents(): if comp.name() == "VEVENT": comp.addProperty( Property("REQUEST-STATUS", ["5.1", "Service unavailable"])) break else: # no VEVENT in the calendar body. # TODO: what to do in this case? pass log.warn("Mail gateway processing DSN {msgid}", msgid=msgId) txn = self.store.newTransaction(label="MailReceiver.processDSN") yield txn.enqueue(IMIPReplyWork, organizer=record.organizer, attendee=record.attendee, icalendarText=str(calendar)) yield txn.commit() returnValue(self.INJECTION_SUBMITTED)
def action(self): # Don't perform any operations until the client is up and running if not self._client.started: returnValue(None) event = self._getRandomEventOfType('VEVENT', justOwned=True) if not event: returnValue(None) component = event.component vevent = component.mainComponent() label = yield self.modifyEvent(event.url, vevent) if label: vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) event.component = component yield self._newOperation(label, self._client.changeEvent(event.url))
def sanitizeCalendar(calendar): """ Clean up specific issues seen in the wild from third party IMIP capable servers. @param calendar: the calendar Component to sanitize @type calendar: L{Component} """ # Don't let a missing PRODID prevent the reply from being processed if not calendar.hasProperty("PRODID"): calendar.addProperty(Property("PRODID", "Unknown")) # For METHOD:REPLY we can remove STATUS properties methodProperty = calendar.getProperty("METHOD") if methodProperty is not None: # Uppercase any METHOD properties methodProperty.setValue(methodProperty.value().upper()) if methodProperty.value() == "REPLY": calendar.removeAllPropertiesWithName("STATUS")
def _addAttendee(self, event, attendees): """ Create a new attendee to add to the list of attendees for the given event. """ selfRecord = self._sim.getUserRecord(self._number) invitees = set([u'mailto:%s' % (selfRecord.email, )]) for att in attendees: invitees.add(att.value()) for _ignore_i in range(10): sample = self._inviteeDistribution.sample() if self._inviteeClumping: sample = self._number + sample invitee = max(0, sample) try: record = self._sim.getUserRecord(invitee) except IndexError: continue cuaddr = u'mailto:%s' % (record.email, ) if cuaddr not in invitees: break else: raise CannotAddAttendee("Can't find uninvited user to invite.") attendee = Property( name=u'ATTENDEE', value=cuaddr.encode("utf-8"), params={ 'CN': record.commonName, 'CUTYPE': 'INDIVIDUAL', 'PARTSTAT': 'NEEDS-ACTION', 'ROLE': 'REQ-PARTICIPANT', 'RSVP': 'TRUE', }, ) event.addProperty(attendee) attendees.append(attendee)
def updateAttendeeData(from_component, to_component): """ Called when processing a REPLY only. Copy the PARTSTAT of the Attendee in the from_component to the matching ATTENDEE in the to_component. Ignore if no match found. Also update the private comments. For VPOLL we need to copy POLL-ITEM-ID response values into the actual matching polled sub-components as VOTER properties. @param from_component: component to copy from @type from_component: L{Component} @param to_component: component to copy to @type to_component: L{Component} """ # Track what changed partstat_changed = False private_comment_changed = False # Get REQUEST-STATUS as we need to write that into the saved ATTENDEE property reqstatus = tuple(from_component.properties("REQUEST-STATUS")) if reqstatus: reqstatus = ",".join(status.value()[0] for status in reqstatus) else: reqstatus = "2.0" # Get attendee in from_component - there MUST be only one attendees = tuple(from_component.properties(from_component.recipientPropertyName())) if len(attendees) != 1: log.error("There must be one and only one ATTENDEE property in a REPLY\n%s" % (str(from_component),)) return None, False, False attendee = attendees[0] partstat = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") # Now find matching ATTENDEE in to_component existing_attendee = to_component.getAttendeeProperty((attendee.value(),)) if existing_attendee: oldpartstat = existing_attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") existing_attendee.setParameter("PARTSTAT", partstat) existing_attendee.setParameter("SCHEDULE-STATUS", reqstatus) partstat_changed = (oldpartstat != partstat) # Always delete RSVP on PARTSTAT change if partstat_changed: try: existing_attendee.removeParameter("RSVP") except KeyError: pass # Handle attendee comments if config.Scheduling.CalDAV.get("EnablePrivateComments", True): # Look for X-CALENDARSERVER-PRIVATE-COMMENT property in iTIP component (State 1 in spec) attendee_comment = tuple(from_component.properties("X-CALENDARSERVER-PRIVATE-COMMENT")) attendee_comment = attendee_comment[0] if len(attendee_comment) else None # Look for matching X-CALENDARSERVER-ATTENDEE-COMMENT property in existing data (State 2 in spec) private_comments = tuple(to_component.properties("X-CALENDARSERVER-ATTENDEE-COMMENT")) for comment in private_comments: attendeeref = comment.parameterValue("X-CALENDARSERVER-ATTENDEE-REF") if attendeeref == attendee.value(): private_comment = comment break else: private_comment = None else: attendee_comment = None private_comment = None # Now do update logic if attendee_comment is None and private_comment is None: # Nothing to do pass elif attendee_comment is None and private_comment is not None: # We now remove the private comment on the organizer's side if the attendee removed it to_component.removeProperty(private_comment) private_comment_changed = True elif attendee_comment is not None and private_comment is None: # Add new property private_comment = Property( "X-CALENDARSERVER-ATTENDEE-COMMENT", attendee_comment.value(), params={ "X-CALENDARSERVER-ATTENDEE-REF": attendee.value(), "X-CALENDARSERVER-DTSTAMP": DateTime.getNowUTC().getText(), } ) to_component.addProperty(private_comment) private_comment_changed = True else: # Only change if different if private_comment.value() != attendee_comment.value(): # Remove all property parameters private_comment.removeAllParameters() # Add default parameters private_comment.setParameter("X-CALENDARSERVER-ATTENDEE-REF", attendee.value()) private_comment.setParameter("X-CALENDARSERVER-DTSTAMP", DateTime.getNowUTC().getText()) # Set new value private_comment.setValue(attendee_comment.value()) private_comment_changed = True # Do VPOLL transfer if from_component.name() == "VPOLL": # TODO: figure out how to report changes back iTipProcessing.updateVPOLLData(from_component, to_component, attendee) return attendee.value(), partstat_changed, private_comment_changed
def updateAttendeeData(from_component, to_component): """ Copy the PARTSTAT of the Attendee in the from_component to the matching ATTENDEE in the to_component. Ignore if no match found. Also update the private comments. @param from_component: @type from_component: @param to_component: @type to_component: """ # Track what changed partstat_changed = False private_comment_changed = False # Get REQUEST-STATUS as we need to write that into the saved ATTENDEE property reqstatus = tuple(from_component.properties("REQUEST-STATUS")) if reqstatus: reqstatus = ",".join(status.value()[0] for status in reqstatus) else: reqstatus = "2.0" # Get attendee in from_component - there MUST be only one attendees = tuple(from_component.properties("ATTENDEE")) if len(attendees) != 1: log.error("There must be one and only one ATTENDEE property in a REPLY\n%s" % (str(from_component),)) return None, False, False attendee = attendees[0] partstat = attendee.params().get("PARTSTAT", ("NEEDS-ACTION",))[0] # Now find matching ATTENDEE in to_component existing_attendee = to_component.getAttendeeProperty((attendee.value(),)) if existing_attendee: oldpartstat = existing_attendee.params().get("PARTSTAT", ("NEEDS-ACTION",))[0] existing_attendee.params()["PARTSTAT"] = [partstat] existing_attendee.params()["SCHEDULE-STATUS"] = [reqstatus] partstat_changed = (oldpartstat != partstat) # Always delete RSVP on PARTSTAT change if partstat_changed: try: del existing_attendee.params()["RSVP"] except KeyError: pass # Handle attendee comments if config.Scheduling.CalDAV.get("EnablePrivateComments", True): # Look for X-CALENDARSERVER-PRIVATE-COMMENT property in iTIP component (State 1 in spec) attendee_comment = tuple(from_component.properties("X-CALENDARSERVER-PRIVATE-COMMENT")) attendee_comment = attendee_comment[0] if len(attendee_comment) else None # Look for matching X-CALENDARSERVER-ATTENDEE-COMMENT property in existing data (State 2 in spec) private_comments = tuple(to_component.properties("X-CALENDARSERVER-ATTENDEE-COMMENT")) for comment in private_comments: params = comment.params()["X-CALENDARSERVER-ATTENDEE-REF"] if len(params) != 1: log.error("Must be one and only one X-CALENDARSERVER-ATTENDEE-REF parameter in X-CALENDARSERVER-ATTENDEE-COMMENT") params = (None,) param = params[0] if param == attendee.value(): private_comment = comment break else: private_comment = None else: attendee_comment = None private_comment = None # Now do update logic if attendee_comment is None and private_comment is None: # Nothing to do pass elif attendee_comment is None and private_comment is not None: # Remove all property parameters private_comment.params().clear() # Add default parameters private_comment.params()["X-CALENDARSERVER-ATTENDEE-REF"] = [attendee.value()] private_comment.params()["X-CALENDARSERVER-DTSTAMP"] = [dateTimeToString(datetime.datetime.now(tz=utc))] # Set value empty private_comment.setValue("") private_comment_changed = True elif attendee_comment is not None and private_comment is None: # Add new property private_comment = Property( "X-CALENDARSERVER-ATTENDEE-COMMENT", attendee_comment.value(), params = { "X-CALENDARSERVER-ATTENDEE-REF": [attendee.value()], "X-CALENDARSERVER-DTSTAMP": [dateTimeToString(datetime.datetime.now(tz=utc))], } ) to_component.addProperty(private_comment) private_comment_changed = True else: # Only change if different if private_comment.value() != attendee_comment.value(): # Remove all property parameters private_comment.params().clear() # Add default parameters private_comment.params()["X-CALENDARSERVER-ATTENDEE-REF"] = [attendee.value()] private_comment.params()["X-CALENDARSERVER-DTSTAMP"] = [dateTimeToString(datetime.datetime.now(tz=utc))] # Set new value private_comment.setValue(attendee_comment.value()) private_comment_changed = True return attendee.value(), partstat_changed, private_comment_changed
def modifyEvent(self, _ignore_href, vevent): length = int(self._descriptionLength.sample()) vevent.replaceProperty(Property("DESCRIPTION", "." * length)) return succeed("update{description}")
def modifyEvent(self, _ignore_href, vevent): length = max(5, int(self._titleLength.sample())) vevent.replaceProperty( Property("SUMMARY", "Event" + "." * (length - 5))) return succeed("update{title}")
def buildFreeBusyResult(self, fbinfo, method=None): """ Generate a VCALENDAR object containing a single VFREEBUSY that is the aggregate of the free busy info passed in. @param fbinfo: the array of busy periods to use. @param method: the METHOD property value to insert. @return: the L{Component} containing the calendar data. """ # Merge overlapping time ranges in each fb info section normalizePeriodList(fbinfo.busy) normalizePeriodList(fbinfo.tentative) normalizePeriodList(fbinfo.unavailable) # Now build a new calendar object with the free busy info we have fbcalendar = Component("VCALENDAR") fbcalendar.addProperty(Property("VERSION", "2.0")) fbcalendar.addProperty(Property("PRODID", iCalendarProductID)) if method: fbcalendar.addProperty(Property("METHOD", method)) fb = Component("VFREEBUSY") fbcalendar.addComponent(fb) if self.organizerProp is not None: fb.addProperty(self.organizerProp) if self.attendeeProp is not None: fb.addProperty(self.attendeeProp) fb.addProperty(Property("DTSTART", self.timerange.getStart())) fb.addProperty(Property("DTEND", self.timerange.getEnd())) fb.addProperty(Property("DTSTAMP", DateTime.getNowUTC())) if len(fbinfo.busy) != 0: fb.addProperty(Property("FREEBUSY", fbinfo.busy, {"FBTYPE": "BUSY"})) if len(fbinfo.tentative) != 0: fb.addProperty(Property("FREEBUSY", fbinfo.tentative, {"FBTYPE": "BUSY-TENTATIVE"})) if len(fbinfo.unavailable) != 0: fb.addProperty(Property("FREEBUSY", fbinfo.unavailable, {"FBTYPE": "BUSY-UNAVAILABLE"})) if self.uid is not None: fb.addProperty(Property("UID", self.uid)) else: uid = str(uuid.uuid4()) fb.addProperty(Property("UID", uid)) if self.event_details: for vevent in self.event_details: fbcalendar.addComponent(vevent) return fbcalendar
def _splitPerUserData(self, ical): """ Split the per-user data out of the "normal" iCalendar components into separate per-user components. Along the way keep the iCalendar representation in a "minimal" state by eliminating any components that are the same as the master derived component. @param ical: calendar data to process @type ical: L{Component} """ def init_peruser_component(): peruser = Component(PERUSER_COMPONENT) peruser.addProperty(Property("UID", ical.resourceUID())) peruser.addProperty(Property(PERUSER_UID, self.uid)) return peruser components = tuple(ical.subcomponents()) peruser_component = init_peruser_component() if self.uid else None perinstance_components = {} for component in components: if component.name() == "VTIMEZONE": continue rid = component.propertyValue("RECURRENCE-ID") rid = rid.duplicate() if rid is not None else None perinstance_component = Component( PERINSTANCE_COMPONENT) if self.uid else None perinstance_id_different = False # Special case certain default property values self._defaultMerge(component) # Transfer per-user properties from main component to per-instance component for property in tuple(component.properties()): if property.name( ) in PerUserDataFilter.PERUSER_PROPERTIES or property.name( ).startswith("X-") and property.name( ) not in PerUserDataFilter.IGNORE_X_PROPERTIES: if self.uid: perinstance_component.addProperty(property) component.removeProperty(property) perinstance_id_different = True # Transfer per-user components from main component to per-instance component for subcomponent in tuple(component.subcomponents()): if subcomponent.name( ) in PerUserDataFilter.PERUSER_SUBCOMPONENTS or subcomponent.name( ).startswith("X-"): if self.uid: perinstance_component.addComponent(subcomponent) component.removeComponent(subcomponent) perinstance_id_different = True if perinstance_id_different and perinstance_component: perinstance_components[rid] = perinstance_component if self.uid: # Add unique per-instance components into the per-user component peruser_component_different = False master_perinstance = perinstance_components.get(None) if master_perinstance: peruser_component.addComponent(master_perinstance) peruser_component_different = True for rid, perinstance in sorted(perinstance_components.iteritems(), key=lambda x: x[0]): if rid is None: continue if master_perinstance is None or perinstance != master_perinstance: perinstance.addProperty(Property("RECURRENCE-ID", rid)) peruser_component.addComponent(perinstance) peruser_component_different = True if peruser_component_different: ical.addComponent(peruser_component) self._compactInstances(ical)
def init_peruser_component(): peruser = Component(PERUSER_COMPONENT) peruser.addProperty(Property("UID", ical.resourceUID())) peruser.addProperty(Property(PERUSER_UID, self.uid)) return peruser