Example #1
0
    def test_one_event(self):
        """
        Test when the calendar is empty.
        """

        data = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
UID:1234-5678
DTSTAMP:20080601T000000Z
DTSTART:%s
DTEND:%s
END:VEVENT
END:VCALENDAR
""" % (self.now_12H.getText(), self.now_13H.getText(),)

        yield self._createCalendarObject(data, "user01", "test.ics")
        calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
        fbinfo = [[], [], [], ]
        matchtotal = 0
        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
        self.assertEqual(result, 1)
        self.assertEqual(fbinfo[0], [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
        self.assertEqual(len(fbinfo[1]), 0)
        self.assertEqual(len(fbinfo[2]), 0)
Example #2
0
    def test_one_event(self):
        """
        Test when the calendar is empty.
        """

        data = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
UID:1234-5678
DTSTAMP:20080601T000000Z
DTSTART:%s
DTEND:%s
END:VEVENT
END:VCALENDAR
""" % (self.now_12H.getText(), self.now_13H.getText(),)

        yield self._createCalendarObject(data, "user01", "test.ics")
        calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
        fbinfo = [[], [], [], ]
        matchtotal = 0
        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
        self.assertEqual(result, 1)
        self.assertEqual(fbinfo[0], [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
        self.assertEqual(len(fbinfo[1]), 0)
        self.assertEqual(len(fbinfo[2]), 0)
Example #3
0
    def recv_freebusy(self, txn, request):
        """
        Process a freebusy cross-pod request. Message arguments as per L{send_freebusy}.

        @param request: request arguments
        @type request: C{dict}
        """

        # Operate on the L{CommonHomeChild}
        calresource, _ignore = yield self._getStoreObjectForRequest(txn, request)

        fbinfo = [[], [], []]
        matchtotal = yield generateFreeBusyInfo(
            calresource,
            fbinfo,
            TimeRange(start=request["timerange"][0], end=request["timerange"][1]),
            request["matchtotal"],
            request["excludeuid"],
            request["organizer"],
            request["organizerPrincipal"],
            request["same_calendar_user"],
            request["servertoserver"],
            request["event_details"],
            logItems=None
        )

        # Convert L{DateTime} objects to text for JSON response
        for i in range(3):
            for j in range(len(fbinfo[i])):
                fbinfo[i][j] = fbinfo[i][j].getText()

        returnValue({
            "fbresults": fbinfo,
            "matchtotal": matchtotal,
        })
Example #4
0
    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None):

        # Find the current recipients calendars that are not transparent
        fbset = (yield recipient.inbox.ownerHome().loadCalendars())
        fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()]

        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
        fbinfo = ([], [], [])

        # Process the availability property from the Inbox.
        availability = recipient.inbox.ownerHome().getAvailability()
        if availability is not None:
            processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)

        # Check to see if the recipient is the same calendar user as the organizer.
        # Needed for masked UID stuff.
        if isinstance(self.scheduler.organizer, LocalCalendarUser):
            same_calendar_user = self.scheduler.organizer.record.uid == recipient.record.uid
        else:
            same_calendar_user = False

        # Now process free-busy set calendars
        matchtotal = 0
        for calendar in fbset:
            matchtotal = (yield generateFreeBusyInfo(
                calendar,
                fbinfo,
                self.scheduler.timeRange,
                matchtotal,
                excludeuid=self.scheduler.excludeUID,
                organizer=self.scheduler.organizer.cuaddr,
                organizerPrincipal=organizerPrincipal,
                same_calendar_user=same_calendar_user,
                servertoserver=remote,
                event_details=event_details,
                logItems=self.scheduler.logItems,
            ))

        # Build VFREEBUSY iTIP reply for this recipient
        fbresult = buildFreeBusyResult(
            fbinfo,
            self.scheduler.timeRange,
            organizer=organizerProp,
            attendee=attendeeProp,
            uid=uid,
            method="REPLY",
            event_details=event_details,
        )

        returnValue(fbresult)
    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None):

        # Find the current recipients calendars that are not transparent
        fbset = (yield recipient.inbox.ownerHome().loadCalendars())
        fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()]

        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
        fbinfo = ([], [], [])

        # Process the availability property from the Inbox.
        availability = recipient.inbox.ownerHome().getAvailability()
        if availability is not None:
            processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)

        # Check to see if the recipient is the same calendar user as the organizer.
        # Needed for masked UID stuff.
        if isinstance(self.scheduler.organizer, LocalCalendarUser):
            same_calendar_user = self.scheduler.organizer.principal.uid == recipient.principal.uid
        else:
            same_calendar_user = False

        # Now process free-busy set calendars
        matchtotal = 0
        for calendar in fbset:
            matchtotal = (yield generateFreeBusyInfo(
                calendar,
                fbinfo,
                self.scheduler.timeRange,
                matchtotal,
                excludeuid=self.scheduler.excludeUID,
                organizer=self.scheduler.organizer.cuaddr,
                organizerPrincipal=organizerPrincipal,
                same_calendar_user=same_calendar_user,
                servertoserver=remote,
                event_details=event_details,
                logItems=self.scheduler.logItems,
            ))

        # Build VFREEBUSY iTIP reply for this recipient
        fbresult = buildFreeBusyResult(
            fbinfo,
            self.scheduler.timeRange,
            organizer=organizerProp,
            attendee=attendeeProp,
            uid=uid,
            method="REPLY",
            event_details=event_details,
        )

        returnValue(fbresult)
Example #6
0
    def test_no_events(self):
        """
        Test when the calendar is empty.
        """

        calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
        fbinfo = [[], [], [], ]
        matchtotal = 0
        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
        self.assertEqual(result, 0)
        self.assertEqual(len(fbinfo[0]), 0)
        self.assertEqual(len(fbinfo[1]), 0)
        self.assertEqual(len(fbinfo[2]), 0)
Example #7
0
    def test_no_events(self):
        """
        Test when the calendar is empty.
        """

        calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
        fbinfo = [[], [], [], ]
        matchtotal = 0
        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
        self.assertEqual(result, 0)
        self.assertEqual(len(fbinfo[0]), 0)
        self.assertEqual(len(fbinfo[1]), 0)
        self.assertEqual(len(fbinfo[2]), 0)
Example #8
0
    def recv_freebusy(self, txn, message):
        """
        Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}.

        @param message: message arguments
        @type message: C{dict}
        """

        shareeView, _ignore_objectResource = yield self._recv(
            txn, message, "freebusy")
        try:
            # Operate on the L{CommonHomeChild}
            fbinfo = [[], [], []]
            matchtotal = yield generateFreeBusyInfo(
                shareeView,
                fbinfo,
                TimeRange(start=message["timerange"][0],
                          end=message["timerange"][1]),
                message["matchtotal"],
                message["excludeuid"],
                message["organizer"],
                message["organizerPrincipal"],
                message["same_calendar_user"],
                message["servertoserver"],
                message["event_details"],
                logItems=None)
        except Exception as e:
            returnValue({
                "result":
                "exception",
                "class":
                ".".join((
                    e.__class__.__module__,
                    e.__class__.__name__,
                )),
                "message":
                str(e),
            })

        for i in range(3):
            for j in range(len(fbinfo[i])):
                fbinfo[i][j] = fbinfo[i][j].getText()

        returnValue({
            "result": "ok",
            "fbresults": fbinfo,
            "matchtotal": matchtotal,
        })
Example #9
0
    def recv_freebusy(self, txn, message):
        """
        Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}.

        @param message: message arguments
        @type message: C{dict}
        """

        shareeView, _ignore_objectResource = yield self._recv(txn, message, "freebusy")
        try:
            # Operate on the L{CommonHomeChild}
            fbinfo = [[], [], []]
            matchtotal = yield generateFreeBusyInfo(
                shareeView,
                fbinfo,
                TimeRange(start=message["timerange"][0], end=message["timerange"][1]),
                message["matchtotal"],
                message["excludeuid"],
                message["organizer"],
                message["organizerPrincipal"],
                message["same_calendar_user"],
                message["servertoserver"],
                message["event_details"],
                logItems=None
            )
        except Exception as e:
            returnValue({
                "result": "exception",
                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
                "message": str(e),
            })

        for i in range(3):
            for j in range(len(fbinfo[i])):
                fbinfo[i][j] = fbinfo[i][j].getText()

        returnValue({
            "result": "ok",
            "fbresults": fbinfo,
            "matchtotal": matchtotal,
        })
    def test_one_event_event_details(self):
        """
        Test when the calendar is empty.
        """

        data = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
UID:1234-5678
DTSTAMP:20080601T000000Z
DTSTART:%s
DTEND:%s
END:VEVENT
END:VCALENDAR
""" % (self.now_12H.getText(), self.now_13H.getText(),)

        yield self._createCalendarObject(data, "user01", "test.ics")
        calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
        fbinfo = [[], [], [], ]
        matchtotal = 0
        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
        event_details = []
        result = (yield generateFreeBusyInfo(
            calendar,
            fbinfo,
            timerange,
            matchtotal,
            organizer="mailto:[email protected]",
            event_details=event_details
        ))
        self.assertEqual(result, 1)
        self.assertEqual(fbinfo[0], [PyCalendarPeriod.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
        self.assertEqual(len(fbinfo[1]), 0)
        self.assertEqual(len(fbinfo[2]), 0)
        self.assertEqual(len(event_details), 1)
        self.assertEqual(str(event_details[0]), str(tuple(Component.fromString(data).subcomponents())[0]))
Example #11
0
    def test_freebusy(self):
        """
        Test that action=component works.
        """

        yield self.createShare("user01", "puser01")

        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
        yield self.commit()

        fbstart = "{now:04d}0102T000000Z".format(**self.nowYear)
        fbend = "{now:04d}0103T000000Z".format(**self.nowYear)

        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")

        fbinfo = [[], [], []]
        matchtotal = yield generateFreeBusyInfo(
            shared,
            fbinfo,
            TimeRange(start=fbstart, end=fbend),
            0,
            excludeuid=None,
            organizer=None,
            organizerPrincipal=None,
            same_calendar_user=False,
            servertoserver=False,
            event_details=False,
            logItems=None
        )

        self.assertEqual(matchtotal, 1)
        self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
        self.assertEqual(len(fbinfo[1]), 0)
        self.assertEqual(len(fbinfo[2]), 0)
        yield self.otherCommit()
Example #12
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,))