def doAccounting(self): # # Accounting # # Note that we associate logging with the organizer, not the # originator, which is good for looking for why something # shows up in a given principal's calendars, rather than # tracking the activities of a specific user. # if isinstance(self.organizer, LocalCalendarUser): accountingType = "iTIP-VFREEBUSY" if self.calendar.mainType( ) == "VFREEBUSY" else "iTIP" if accountingEnabled(accountingType, self.organizer.record): emitAccounting( accountingType, self.organizer.record, "Originator: {o}\nRecipients:\n{r}Method:{method}\n\n{cal}" .format( o=str(self.originator), r=str("".join([ " {}\n".format(recipient, ) for recipient in self.recipients ])), method=str(self.method), cal=str(self.calendar), ))
def doAccounting(self): # # Accounting # # Note that we associate logging with the organizer, not the # originator, which is good for looking for why something # shows up in a given principal's calendars, rather than # tracking the activities of a specific user. # if isinstance(self.organizer, LocalCalendarUser): accountingType = "iTIP-VFREEBUSY" if self.calendar.mainType() == "VFREEBUSY" else "iTIP" if accountingEnabled(accountingType, self.organizer.record): emitAccounting( accountingType, self.organizer.record, "Originator: {o}\nRecipients:\n{r}Method:{method}\n\n{cal}".format( o=str(self.originator), r=str("".join([" {}\n".format(recipient,) for recipient in self.recipients])), method=str(self.method), cal=str(self.calendar), ) )
def doAccounting(self): # # Accounting # # Note that we associate logging with the organizer, not the # originator, which is good for looking for why something # shows up in a given principal's calendars, rather than # tracking the activities of a specific user. # if isinstance(self.organizer, LocalCalendarUser): accountingType = "iTIP-VFREEBUSY" if self.calendar.mainType() == "VFREEBUSY" else "iTIP" if accountingEnabled(accountingType, self.organizer.principal): emitAccounting( accountingType, self.organizer.principal, "Originator: %s\nRecipients:\n%sServer Instance:%s\nMethod:%s\n\n%s" % ( str(self.originator), str("".join([" %s\n" % (recipient,) for recipient in self.recipients])), str(self.request.serverInstance), str(self.method), str(self.calendar), ) )
def checkAttendeeAutoReply(self, calendar, automode): """ Check whether a reply to the given iTIP message is needed and if so make the appropriate changes to the calendar data. Changes are only made for the case where the PARTSTAT of the attendee is NEEDS-ACTION - i.e., any existing state is left unchanged. This allows, e.g., proxies to decline events that would otherwise have been auto-accepted and those stay declined as non-schedule-change updates are received. @param calendar: the iTIP message to process @type calendar: L{Component} @param automode: the auto-schedule mode for the recipient @type automode: L{txdav.who.idirectory.AutoScheduleMode} @return: C{tuple} of C{bool}, C{bool}, C{str} indicating whether changes were made, whether the inbox item should be added, and the new PARTSTAT. """ if accountingEnabled("AutoScheduling", self.recipient.record): accounting = { "when": DateTime.getNowUTC().getText(), "automode": automode.name, "changed": False, } else: accounting = None # First ignore the none mode if automode == AutoScheduleMode.none: returnValue((False, True, "", accounting,)) elif not automode: automode = { "none": AutoScheduleMode.none, "accept-always": AutoScheduleMode.accept, "decline-always": AutoScheduleMode.decline, "accept-if-free": AutoScheduleMode.acceptIfFree, "decline-if-busy": AutoScheduleMode.declineIfBusy, "automatic": AutoScheduleMode.acceptIfFreeDeclineIfBusy, }.get( config.Scheduling.Options.AutoSchedule.DefaultMode, AutoScheduleMode.acceptIfFreeDeclineIfBusy ) log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - checking for auto-reply with mode: %s" % (self.recipient.cuaddr, self.uid, automode.name,)) cuas = self.recipient.record.calendarUserAddresses # First expand current one to get instances (only go 1 year into the future) default_future_expansion_duration = Duration(days=config.Scheduling.Options.AutoSchedule.FutureFreeBusyDays) expand_max = DateTime.getToday() + default_future_expansion_duration instances = calendar.expandTimeRanges(expand_max, ignoreInvalidInstances=True) if accounting is not None: accounting["expand-max"] = expand_max.getText() accounting["instances"] = len(instances.instances) # We are going to ignore auto-accept processing for anything more than a day old (actually use -2 days # to add some slop to account for possible timezone offsets) min_date = DateTime.getToday() min_date.offsetDay(-2) allOld = True # Cache the current attendee partstat on the instance object for later use, and # also mark whether the instance time slot would be free for instance in instances.instances.itervalues(): attendee = instance.component.getAttendeeProperty(cuas) instance.partstat = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") if attendee else None instance.free = True instance.active = (instance.end > min_date) if instance.active: allOld = False instances = sorted(instances.instances.values(), key=lambda x: x.rid) # If every instance is in the past we punt right here so we don't waste time on freebusy lookups etc. # There will be no auto-accept and no inbox item stored (so as not to waste storage on items that will # never be processed). if allOld: if accounting is not None: accounting["status"] = "all instances are old" returnValue((False, False, "", accounting,)) # Extract UID from primary component as we want to ignore this one if we match it # in any calendars. uid = calendar.resourceUID() # Now compare each instance time-range with the index and see if there is an overlap fbset = (yield self.recipient.inbox.ownerHome().loadCalendars()) fbset = [fbcalendar for fbcalendar in fbset if fbcalendar.isUsedForFreeBusy()] if accounting is not None: accounting["fbset"] = [testcal.name() for testcal in fbset] accounting["tr"] = [] for testcal in fbset: # Get the timezone property from the collection, and store in the query filter # for use during the query itself. tz = testcal.getTimezone() tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True) # Now do search for overlapping time-range and set instance.free based # on whether there is an overlap or not. # NB Do this in reverse order so that the date farthest in the future is tested first - that will # ensure that freebusy that far into the future is determined and will trigger time-range caching # and indexing out that far - and that will happen only once through this loop. for instance in reversed(instances): if instance.partstat == "NEEDS-ACTION" and instance.free and instance.active: try: # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE fbinfo = ([], [], []) def makeTimedUTC(dt): dt = dt.duplicate() if dt.isDateOnly(): dt.setDateOnly(False) dt.setHHMMSS(0, 0, 0) if dt.floating(): dt.setTimezone(tzinfo) dt.adjustToUTC() return dt tr = caldavxml.TimeRange( start=str(makeTimedUTC(instance.start)), end=str(makeTimedUTC(instance.end)), ) yield generateFreeBusyInfo(testcal, fbinfo, tr, 0, uid, servertoserver=True, accountingItems=accounting if len(instances) == 1 else None) # If any fbinfo entries exist we have an overlap if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]): instance.free = False if accounting is not None: accounting["tr"].insert(0, (tr.attributes["start"], tr.attributes["end"], instance.free,)) except QueryMaxResources: instance.free[instance] = False log.info("Exceeded number of matches whilst trying to find free-time.") if accounting is not None: accounting["problem"] = "Exceeded number of matches" # If everything is declined we can exit now if not any([instance.free for instance in instances]): break if accounting is not None: accounting["tr"] = accounting["tr"][:30] # Now adjust the instance.partstat currently set to "NEEDS-ACTION" to the # value determined by auto-accept logic based on instance.free state. However, # ignore any instance in the past - leave them as NEEDS-ACTION. partstat_counts = collections.defaultdict(int) for instance in instances: if instance.partstat == "NEEDS-ACTION" and instance.active: if automode == AutoScheduleMode.accept: freePartstat = busyPartstat = "ACCEPTED" elif automode == AutoScheduleMode.decline: freePartstat = busyPartstat = "DECLINED" else: freePartstat = "ACCEPTED" if automode in ( AutoScheduleMode.acceptIfFree, AutoScheduleMode.acceptIfFreeDeclineIfBusy, ) else "NEEDS-ACTION" busyPartstat = "DECLINED" if automode in ( AutoScheduleMode.declineIfBusy, AutoScheduleMode.acceptIfFreeDeclineIfBusy, ) else "NEEDS-ACTION" instance.partstat = freePartstat if instance.free else busyPartstat partstat_counts[instance.partstat] += 1 if len(partstat_counts) == 0: # Nothing to do if accounting is not None: accounting["status"] = "no partstat changes" returnValue((False, False, "", accounting,)) elif len(partstat_counts) == 1: # Do the simple case of all PARTSTATs the same separately # Extract the ATTENDEE property matching current recipient from the calendar data attendeeProps = calendar.getAttendeeProperties(cuas) if not attendeeProps: if accounting is not None: accounting["status"] = "no attendee to change" returnValue((False, False, "", accounting,)) made_changes = False partstat = partstat_counts.keys()[0] for component in calendar.subcomponents(): made_changes |= self.resetAttendeePartstat(component, cuas, partstat) store_inbox = partstat == "NEEDS-ACTION" if accounting is not None: accounting["status"] = "setting all partstats to {}".format(partstat) if made_changes else "all partstats correct" accounting["changed"] = made_changes else: # Hard case: some accepted, some declined, some needs-action # What we will do is mark any master instance as accepted, then mark each existing # overridden instance as accepted or declined, and generate new overridden instances for # any other declines. made_changes = False store_inbox = False partstat = "MIXED RESPONSE" # Default state is whichever of free or busy has most instances defaultPartStat = max(sorted(partstat_counts.items()), key=lambda x: x[1])[0] # See if there is a master component first hadMasterRsvp = False master = calendar.masterComponent() if master: attendee = master.getAttendeeProperty(cuas) if attendee: hadMasterRsvp = attendee.parameterValue("RSVP", "FALSE") == "TRUE" if defaultPartStat == "NEEDS-ACTION": store_inbox = True made_changes |= self.resetAttendeePartstat(master, cuas, defaultPartStat) # Look at expanded instances and change partstat accordingly for instance in instances: overridden = calendar.overriddenComponent(instance.rid) if not overridden and instance.partstat == defaultPartStat: # Nothing to do as state matches the master continue if overridden: # Change ATTENDEE property to match new state if instance.partstat == "NEEDS-ACTION" and instance.active: store_inbox = True made_changes |= self.resetAttendeePartstat(overridden, cuas, instance.partstat) else: # Derive a new overridden component and change partstat. We also need to make sure we restore any RSVP # value that may have been overwritten by any change to the master itself. derived = calendar.deriveInstance(instance.rid) if derived is not None: attendee = derived.getAttendeeProperty(cuas) if attendee: if instance.partstat == "NEEDS-ACTION" and instance.active: store_inbox = True self.resetAttendeePartstat(derived, cuas, instance.partstat, hadMasterRsvp) made_changes = True calendar.addComponent(derived) if accounting is not None: accounting["status"] = "mixed partstat changes" if made_changes else "mixed partstats correct" accounting["changed"] = made_changes # Fake a SCHEDULE-STATUS on the ORGANIZER property if made_changes: calendar.setParameterToValueForPropertyWithValue("SCHEDULE-STATUS", iTIPRequestStatus.MESSAGE_DELIVERED_CODE, "ORGANIZER", None) returnValue((made_changes, store_inbox, partstat, accounting,))
def doImplicitAttendeeCancel(self): """ An iTIP CANCEL message has been sent to an attendee. If there is no existing resource, we will simply ignore the message. If there is an existing resource we need to reconcile the changes between it and the iTIP message. @return: C{tuple} of (processed, auto-processed, store inbox item, changes) """ # If there is no existing copy, then ignore if self.recipient_calendar is None: log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:CANCEL, UID: '%s' - attendee has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid)) result = (True, True, True, None) else: # Need to check for auto-respond attendees. These need to suppress the inbox message # if the cancel is processed. However, if the principal is a user we always force the # inbox item on them even if auto-schedule is true so that they get a notification # of the cancel. organizer = normalizeCUAddr(self.message.getOrganizer()) autoprocessed = yield self.recipient.record.canAutoSchedule(organizer=organizer) store_inbox = not autoprocessed or self.recipient.record.getCUType() == "INDIVIDUAL" # Check to see if this is a cancel of the entire event processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar, autoprocessing=autoprocessed) if processed_message: if autoprocessed and accountingEnabled("AutoScheduling", self.recipient.record): accounting = { "action": "cancel", "when": DateTime.getNowUTC().getText(), "deleting": delete_original, } emitAccounting( "AutoScheduling", self.recipient.record, json.dumps(accounting) + "\r\n", filename=self.uid.encode("base64")[:-1] + ".txt" ) if delete_original: # Delete the attendee's copy of the event log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - deleting entire event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid)) yield self.deleteCalendarResource(self.recipient_calendar_resource) # Build the schedule-changes XML element changes = customxml.ScheduleChanges( customxml.DTStamp(), customxml.Action( customxml.Cancel(), ), ) result = (True, autoprocessed, store_inbox, changes,) else: # Update the attendee's copy of the event log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid)) yield self.writeCalendarResource(None, self.recipient_calendar_resource, self.recipient_calendar) # Build the schedule-changes XML element if rids: action = customxml.Cancel( *[customxml.Recurrence(customxml.RecurrenceID.fromString(rid.getText())) for rid in sorted(rids)] ) else: action = customxml.Cancel() changes = customxml.ScheduleChanges( customxml.DTStamp(), customxml.Action(action), ) result = (True, autoprocessed, store_inbox, changes) else: log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid)) result = (True, True, False, None) returnValue(result)