示例#1
0
 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),
                 ))
示例#2
0
 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),
                 )
             )
示例#4
0
    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,))
示例#5
0
    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)