예제 #1
0
    def __init__(self, parent=None):

        super(VAlarm, self).__init__(parent=parent)

        self.mAction = definitions.eAction_VAlarm_Display
        self.mTriggerAbsolute = False
        self.mTriggerOnStart = True
        self.mTriggerOn = DateTime()

        # Set duration default to 1 hour
        self.mTriggerBy = Duration()
        self.mTriggerBy.setDuration(60 * 60)

        # Does not repeat by default
        self.mRepeats = 0
        self.mRepeatInterval = Duration()
        self.mRepeatInterval.setDuration(5 * 60)  # Five minutes

        # Status
        self.mStatusInit = False
        self.mAlarmStatus = definitions.eAlarm_Status_Pending
        self.mLastTrigger = DateTime()
        self.mNextTrigger = DateTime()
        self.mDoneCount = 0

        # Create action data
        self.mActionData = VAlarm.VAlarmDisplay("")
예제 #2
0
    def __init__(self, parent=None):

        super(VAlarm, self).__init__(parent=parent)

        self.mAction = definitions.eAction_VAlarm_Display
        self.mTriggerAbsolute = False
        self.mTriggerOnStart = True
        self.mTriggerOn = DateTime()

        # Set duration default to 1 hour
        self.mTriggerBy = Duration()
        self.mTriggerBy.setDuration(60 * 60)

        # Does not repeat by default
        self.mRepeats = 0
        self.mRepeatInterval = Duration()
        self.mRepeatInterval.setDuration(5 * 60) # Five minutes

        # Status
        self.mStatusInit = False
        self.mAlarmStatus = definitions.eAlarm_Status_Pending
        self.mLastTrigger = DateTime()
        self.mNextTrigger = DateTime()
        self.mDoneCount = 0

        # Create action data
        self.mActionData = VAlarm.VAlarmDisplay("")
예제 #3
0
    def getCacheEntry(cls, calresource, useruid, timerange):

        key = str(calresource.id()) + "/" + useruid
        token = (yield calresource.syncToken())
        entry = (yield cls.fbcacher.get(key))

        if entry:

            # Offset one day at either end to account for floating
            entry_timerange = Period.parseText(entry.timerange)
            cached_start = entry_timerange.getStart() + Duration(
                days=cls.CACHE_DAYS_FLOATING_ADJUST)
            cached_end = entry_timerange.getEnd() - Duration(
                days=cls.CACHE_DAYS_FLOATING_ADJUST)

            # Verify that the requested time range lies within the cache time range
            if compareDateTime(timerange.getEnd(),
                               cached_end) <= 0 and compareDateTime(
                                   timerange.getStart(), cached_start) >= 0:

                # Verify that cached entry is still valid
                if token == entry.token:
                    returnValue(entry.fbresults)

        returnValue(None)
예제 #4
0
    def _getMasterEventDetails(self, component):
        """
        Logic here comes from RFC4791 Section 9.9
        """

        start = component.getStartDateUTC()
        if start is None:
            return None
        rulestart = component.propertyValue("DTSTART")

        end = component.getEndDateUTC()
        duration = None
        if end is None:
            if not start.isDateOnly():
                # Timed event with zero duration
                duration = Duration(days=0)
            else:
                # All day event default duration is one day
                duration = Duration(days=1)
            end = start + duration
        else:
            duration = differenceDateTime(start, end)

        return (
            rulestart,
            start,
            end,
            duration,
        )
예제 #5
0
 def sample(self):
     offset = PyDuration(seconds=int(self._helperDistribution.sample()))
     beginning = self.now(Timezone(tzid=self._tzname))
     while offset:
         start, end = self._findWorkAfter(beginning)
         if end - start > offset:
             result = start + offset
             result.setMinutes(result.getMinutes() // 15 * 15)
             result.setSeconds(0)
             return result
         offset.setDuration(offset.getTotalSeconds() - (end - start).getTotalSeconds())
         beginning = end
예제 #6
0
    def time(self, component):
        """
        Examples:

        3:30 PM to 4:30 PM PDT
        All day
        3:30 PM PDT
        3:30 PM PDT to 7:30 PM EDT

        1 day
        2 days
        1 day 1 hour
        1 day 4 hours 18 minutes
        """

        # Bind to '_' so pygettext.py will pick this up for translation
        _ = self.translation.ugettext

        tzStart = tzEnd = None
        dtStart = component.propertyValue("DTSTART")
        if dtStart.isDateOnly():
            return ("", _("All day"))
        else:
            tzStart = dtStart.timeZoneDescriptor()

        dtEnd = component.propertyValue("DTEND")
        if dtEnd:
            if not dtEnd.isDateOnly():
                tzEnd = dtEnd.timeZoneDescriptor()
            duration = dtEnd - dtStart
        else:
            tzEnd = tzStart
            duration = component.propertyValue("DURATION")
            if duration:
                dtEnd = dtStart + duration
            else:
                if dtStart.isDateOnly():
                    dtEnd = None
                    duration = Duration(days=1)
                else:
                    dtEnd = dtStart + Duration(days=1)
                    dtEnd.setHHMMSS(0, 0, 0)
                    duration = dtEnd - dtStart

        if dtStart == dtEnd:
            return (self.dtTime(dtStart), "")

        return (_("%(startTime)s to %(endTime)s") % {
            'startTime':
            self.dtTime(dtStart, includeTimezone=(tzStart != tzEnd)),
            'endTime':
            self.dtTime(dtEnd),
        }, self.dtDuration(duration))
예제 #7
0
    def testRelaxedBad(self):

        test_relaxed_data = (
            ("P12DT", 12 * 24 * 60 * 60, "P12D"),
            ("-P1WT", -7 * 24 * 60 * 60, "-P1W"),
            ("-P1W1D", -7 * 24 * 60 * 60, "-P1W"),
        )
        for text, seconds, result in test_relaxed_data:

            ParserContext.INVALID_DURATION_VALUE = ParserContext.PARSER_FIX
            self.assertEqual(Duration.parseText(text).getTotalSeconds(), seconds)
            self.assertEqual(Duration.parseText(text).getText(), result)

            ParserContext.INVALID_DURATION_VALUE = ParserContext.PARSER_RAISE
            self.assertRaises(ValueError, Duration.parseText, text)
예제 #8
0
    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)
예제 #9
0
    def testRelaxedBad(self):

        test_relaxed_data = (
            ("P12DT", 12 * 24 * 60 * 60, "P12D"),
            ("-P1WT", -7 * 24 * 60 * 60, "-P1W"),
            ("-P1W1D", -7 * 24 * 60 * 60, "-P1W"),
        )
        for text, seconds, result in test_relaxed_data:

            ParserContext.INVALID_DURATION_VALUE = ParserContext.PARSER_FIX
            self.assertEqual(
                Duration.parseText(text).getTotalSeconds(), seconds)
            self.assertEqual(Duration.parseText(text).getText(), result)

            ParserContext.INVALID_DURATION_VALUE = ParserContext.PARSER_RAISE
            self.assertRaises(ValueError, Duration.parseText, text)
예제 #10
0
    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)
예제 #11
0
    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)
예제 #12
0
    def __init__(self, start=None, end=None, duration=None):

        self.mStart = start if start is not None else DateTime()

        if end is not None:
            self.mEnd = end
            self.mDuration = self.mEnd - self.mStart
            self.mUseDuration = False
        elif duration is not None:
            self.mDuration = duration
            self.mEnd = self.mStart + self.mDuration
            self.mUseDuration = True
        else:
            self.mEnd = self.mStart.duplicate()
            self.mDuration = Duration()
            self.mUseDuration = False
예제 #13
0
    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)
예제 #14
0
    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)
예제 #15
0
    def testGenerate(self):
        def _doTest(d, result):

            if result[0] == "+":
                result = result[1:]
            os = StringIO()
            d.generate(os)
            self.assertEqual(os.getvalue(), result)

        for seconds, result in TestDuration.test_data:
            _doTest(Duration(duration=seconds), result)
예제 #16
0
    def testSetUseDuration(self):

        p1 = Period(
            start=DateTime(2000, 1, 1, 0, 0, 0),
            end=DateTime(2000, 1, 1, 1, 0, 0),
        )
        p1.setUseDuration(True)
        self.assertTrue(p1.getText(), "20000101T000000/PT1H")

        p2 = Period(
            start=DateTime(2000, 1, 1, 0, 0, 0),
            duration=Duration(hours=1),
        )
        p2.setUseDuration(False)
        self.assertTrue(p2.getText(), "20000101T000000/20000101T010000")
예제 #17
0
    def getCacheEntry(cls, calresource, useruid, timerange):

        key = calresource.resourceID() + "/" + useruid
        token = (yield calresource.getInternalSyncToken())
        entry = (yield fbcacher.get(key))

        if entry:

            # Offset one day at either end to account for floating
            cached_start = entry.timerange.start + Duration(
                days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
            cached_end = entry.timerange.end - Duration(
                days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)

            # Verify that the requested timerange lies within the cache timerange
            if compareDateTime(timerange.end,
                               cached_end) <= 0 and compareDateTime(
                                   timerange.start, cached_start) >= 0:

                # Verify that cached entry is still valid
                if token == entry.token:
                    returnValue(entry.fbresults)

        returnValue(None)
예제 #18
0
    def __sub__(self, dateorduration):

        if isinstance(dateorduration, DateTime):

            date = dateorduration

            # Look for floating
            if self.floating() or date.floating():
                # Adjust the floating ones to fixed
                copy1 = self.duplicate()
                copy2 = date.duplicate()

                if copy1.floating() and copy2.floating():
                    # Set both to UTC and do comparison
                    copy1.setTimezoneUTC(True)
                    copy2.setTimezoneUTC(True)
                    return copy1 - copy2
                elif copy1.floating():
                    # Set to be the same
                    copy1.setTimezoneID(copy2.getTimezoneID())
                    copy1.setTimezoneUTC(copy2.getTimezoneUTC())
                    return copy1 - copy2
                else:
                    # Set to be the same
                    copy2.setTimezoneID(copy1.getTimezoneID())
                    copy2.setTimezoneUTC(copy1.getTimezoneUTC())
                    return copy1 - copy2

            else:
                # Do diff of date-time in seconds
                diff = self.getPosixTime() - date.getPosixTime()
                return Duration(duration=diff)

        elif isinstance(dateorduration, Duration):

            duration = dateorduration
            result = self.duplicate()
            result.mSeconds -= duration.getTotalSeconds()
            result.normalise()
            return result

        raise ValueError("DateTime: cannot subtract: {}".format(dateorduration))
예제 #19
0
    def parse(self, data, fullISO=False):
        try:
            splits = data.split('/', 1)
            if len(splits) == 2:
                start = splits[0]
                end = splits[1]

                self.mStart.parse(start, fullISO)
                if end[0] == 'P':
                    self.mDuration = Duration.parseText(end)
                    self.mUseDuration = True
                    self.mEnd = None
                else:
                    self.mEnd.parse(end, fullISO)
                    self.mUseDuration = False
                    self.mDuration = None
            else:
                raise ValueError
        except IndexError:
            raise ValueError
예제 #20
0
    def parse(self, data, fullISO=False):
        try:
            splits = data.split('/', 1)
            if len(splits) == 2:
                start = splits[0]
                end = splits[1]

                self.mStart.parse(start, fullISO)
                if end[0] == 'P':
                    self.mDuration = Duration.parseText(end)
                    self.mUseDuration = True
                    self.mEnd = None
                else:
                    self.mEnd.parse(end, fullISO)
                    self.mUseDuration = False
                    self.mDuration = None
            else:
                raise ValueError
        except IndexError:
            raise ValueError
예제 #21
0
class Period(ValueMixin):
    def __init__(self, start=None, end=None, duration=None):

        self.mStart = start if start is not None else DateTime()

        if end is not None:
            self.mEnd = end
            self.mDuration = self.mEnd - self.mStart
            self.mUseDuration = False
        elif duration is not None:
            self.mDuration = duration
            self.mEnd = self.mStart + self.mDuration
            self.mUseDuration = True
        else:
            self.mEnd = self.mStart.duplicate()
            self.mDuration = Duration()
            self.mUseDuration = False

    def duplicate(self):
        other = Period(start=self.mStart.duplicate(),
                       end=self.mEnd.duplicate())
        other.mUseDuration = self.mUseDuration
        return other

    def __hash__(self):
        return hash((
            self.mStart,
            self.mEnd,
        ))

    def __repr__(self):
        return "Period %s" % (self.getText(), )

    def __str__(self):
        return self.getText()

    def __eq__(self, comp):
        return self.mStart == comp.mStart and self.mEnd == comp.mEnd

    def __gt__(self, comp):
        return self.mStart > comp

    def __lt__(self, comp):
        return self.mStart < comp.mStart  \
            or (self.mStart == comp.mStart) and self.mEnd < comp.mEnd

    @classmethod
    def parseText(cls, data):
        period = cls()
        period.parse(data)
        return period

    def parse(self, data, fullISO=False):
        splits = data.split('/', 1)
        if len(splits) == 2:
            start = splits[0]
            end = splits[1]

            self.mStart.parse(start, fullISO)
            if end[0] == 'P':
                self.mDuration.parse(end)
                self.mUseDuration = True
                self.mEnd = self.mStart + self.mDuration
            else:
                self.mEnd.parse(end, fullISO)
                self.mUseDuration = False
                self.mDuration = self.mEnd - self.mStart
        else:
            raise ValueError

    def generate(self, os):
        try:
            self.mStart.generate(os)
            os.write("/")
            if self.mUseDuration:
                self.mDuration.generate(os)
            else:
                self.mEnd.generate(os)
        except:
            pass

    def writeXML(self, node, namespace):
        start = XML.SubElement(
            node, xmlutils.makeTag(namespace, xmldefinitions.period_start))
        start.text = self.mStart.getXMLText()

        if self.mUseDuration:
            duration = XML.SubElement(
                node,
                xmlutils.makeTag(namespace, xmldefinitions.period_duration))
            duration.text = self.mDuration.getText()
        else:
            end = XML.SubElement(
                node, xmlutils.makeTag(namespace, xmldefinitions.period_end))
            end.text = self.mEnd.getXMLText()

    def parseJSON(self, jobject):
        """
        jCal encodes this as an array of two values. We convert back into a single "/"
        separated string and parse as normal.
        """
        self.parse("%s/%s" % tuple(jobject), True)

    def writeJSON(self, jobject):
        """
        jCal encodes this value as an array with two components.
        """
        value = [
            self.mStart.getXMLText(),
        ]
        if self.mUseDuration:
            value.append(self.mDuration.getText())
        else:
            value.append(self.mEnd.getXMLText())
        jobject.append(value)

    def getStart(self):
        return self.mStart

    def getEnd(self):
        return self.mEnd

    def getDuration(self):
        return self.mDuration

    def getUseDuration(self):
        return self.mUseDuration

    def setUseDuration(self, use):
        self.mUseDuration = use

    def isDateWithinPeriod(self, dt):
        # Inclusive start, exclusive end
        return dt >= self.mStart and dt < self.mEnd

    def isDateBeforePeriod(self, dt):
        # Inclusive start
        return dt < self.mStart

    def isDateAfterPeriod(self, dt):
        # Exclusive end
        return dt >= self.mEnd

    def isPeriodOverlap(self, p):
        # Inclusive start, exclusive end
        return not (self.mStart >= p.mEnd or self.mEnd <= p.mStart)

    def adjustToUTC(self):
        self.mStart.adjustToUTC()
        self.mEnd.adjustToUTC()

    def describeDuration(self):
        return ""
예제 #22
0
    def indexedSearch(self, filter, useruid="", fbtype=False):
        """
        Finds resources matching the given qualifiers.
        @param filter: the L{Filter} for the calendar-query to execute.
        @return: an iterable of tuples for each resource matching the
            given C{qualifiers}. The tuples are C{(name, uid, type)}, where
            C{name} is the resource name, C{uid} is the resource UID, and
            C{type} is the resource iCalendar component type.
        """

        # Make sure we have a proper Filter element and get the partial SQL
        # statement to use.
        if isinstance(filter, Filter):
            if fbtype:
                # Lookup the useruid - try the empty (default) one if needed
                dbuseruid = self._db_value_for_sql(
                    "select PERUSERID from PERUSER where USERUID == :1",
                    useruid,
                )
            else:
                dbuseruid = ""

            qualifiers = sqlcalendarquery(filter, None, dbuseruid, fbtype)
            if qualifiers is not None:
                # Determine how far we need to extend the current expansion of
                # events. If we have an open-ended time-range we will expand one
                # year past the start. That should catch bounded recurrences - unbounded
                # will have been indexed with an "infinite" value always included.
                maxDate, isStartDate = filter.getmaxtimerange()
                if maxDate:
                    maxDate = maxDate.duplicate()
                    maxDate.setDateOnly(True)
                    if isStartDate:
                        maxDate += Duration(days=365)
                    self.testAndUpdateIndex(maxDate)
            else:
                # We cannot handle this filter in an indexed search
                raise IndexedSearchException()

        else:
            qualifiers = None

        # Perform the search
        if qualifiers is None:
            rowiter = self._db_execute("select NAME, UID, TYPE from RESOURCE")
        else:
            if fbtype:
                # For a free-busy time-range query we return all instances
                rowiter = self._db_execute(
                    "select DISTINCT RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE, RESOURCE.ORGANIZER, TIMESPAN.FLOAT, TIMESPAN.START, TIMESPAN.END, TIMESPAN.FBTYPE, TIMESPAN.TRANSPARENT, TRANSPARENCY.TRANSPARENT"
                    + qualifiers[0], *qualifiers[1])
            else:
                rowiter = self._db_execute(
                    "select DISTINCT RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE"
                    + qualifiers[0], *qualifiers[1])

        # Check result for missing resources
        results = []
        for row in rowiter:
            name = row[0]
            if self.resource.getChild(name.encode("utf-8")):
                if fbtype:
                    row = list(row)
                    if row[9]:
                        row[8] = row[9]
                    del row[9]
                results.append(row)
            else:
                log.error(
                    "Calendar resource %s is missing from %s. Removing from index."
                    % (name, self.resource))
                self.deleteResource(name)

        return results
예제 #23
0
    def _add_to_db(self,
                   name,
                   calendar,
                   cursor=None,
                   expand_until=None,
                   reCreate=False):
        """
        Records the given calendar resource in the index with the given name.
        Resource names and UIDs must both be unique; only one resource name may
        be associated with any given UID and vice versa.
        NB This method does not commit the changes to the db - the caller
        MUST take care of that
        @param name: the name of the resource to add.
        @param calendar: a L{Calendar} object representing the resource
            contents.
        """
        uid = calendar.resourceUID()
        organizer = calendar.getOrganizer()
        if not organizer:
            organizer = ""

        # Decide how far to expand based on the component
        doInstanceIndexing = False
        master = calendar.masterComponent()
        if master is None or not calendar.isRecurring():
            # When there is no master we have a set of overridden components - index them all.
            # When there is one instance - index it.
            expand = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
            doInstanceIndexing = True
        else:
            # If migrating or re-creating or config option for delayed indexing is off, always index
            if reCreate or not config.FreeBusyIndexDelayedExpand:
                doInstanceIndexing = True

            # Duration into the future through which recurrences are expanded in the index
            # by default.  This is a caching parameter which affects the size of the index;
            # it does not affect search results beyond this period, but it may affect
            # performance of such a search.
            expand = (DateTime.getToday() +
                      Duration(days=config.FreeBusyIndexExpandAheadDays))

            if expand_until and expand_until > expand:
                expand = expand_until

            # Maximum duration into the future through which recurrences are expanded in the
            # index.  This is a caching parameter which affects the size of the index; it
            # does not affect search results beyond this period, but it may affect
            # performance of such a search.
            #
            # When a search is performed on a time span that goes beyond that which is
            # expanded in the index, we have to open each resource which may have data in
            # that time period.  In order to avoid doing that multiple times, we want to
            # cache those results.  However, we don't necessarily want to cache all
            # occurrences into some obscenely far-in-the-future date, so we cap the caching
            # period.  Searches beyond this period will always be relatively expensive for
            # resources with occurrences beyond this period.
            if expand > (DateTime.getToday() +
                         Duration(days=config.FreeBusyIndexExpandMaxDays)):
                raise IndexedSearchException()

        # Always do recurrence expansion even if we do not intend to index - we need this to double-check the
        # validity of the iCalendar recurrence data.
        try:
            instances = calendar.expandTimeRanges(
                expand, ignoreInvalidInstances=reCreate)
            recurrenceLimit = instances.limit
        except InvalidOverriddenInstanceError, e:
            log.error("Invalid instance %s when indexing %s in %s" % (
                e.rid,
                name,
                self.resource,
            ))
            raise
예제 #24
0
    def doWork(self):

        # Delete all other work items for this event
        yield Delete(
            From=self.table,
            Where=self.group,
        ).on(self.transaction)

        # get db object
        calendarObject = yield CalendarStoreFeatures(
            self.transaction._store).calendarObjectWithID(
                self.transaction, self.resourceID)
        component = yield calendarObject.componentForUser()

        # Change a copy of the original, as we need the original cached on the resource
        # so we can do a diff to test implicit scheduling changes
        component = component.duplicate()

        # sync group attendees
        if (yield calendarObject.reconcileGroupAttendees(component)):

            # group attendees in event have changed
            if (component.masterComponent() is None
                    or not component.isRecurring()):

                # skip non-recurring old events, no instances
                if (yield calendarObject.removeOldEventGroupLink(
                        component,
                        instances=None,
                        inserting=False,
                        txn=self.transaction)):
                    returnValue(None)
            else:
                # skip recurring old events
                expand = (DateTime.getToday() +
                          Duration(days=config.FreeBusyIndexExpandAheadDays))

                if config.FreeBusyIndexLowerLimitDays:
                    truncateLowerLimit = DateTime.getToday()
                    truncateLowerLimit.offsetDay(
                        -config.FreeBusyIndexLowerLimitDays)
                else:
                    truncateLowerLimit = None

                instances = component.expandTimeRanges(
                    expand,
                    lowerLimit=truncateLowerLimit,
                    ignoreInvalidInstances=True)
                if (yield calendarObject.removeOldEventGroupLink(
                        component,
                        instances=instances,
                        inserting=False,
                        txn=self.transaction)):
                    returnValue(None)

                # split spanning events and only update present-future split result
                splitter = iCalSplitter(0, 1)
                break_point = DateTime.getToday() - Duration(
                    seconds=config.GroupAttendees.UpdateOldEventLimitSeconds)
                rid = splitter.whereSplit(component, break_point=break_point)
                if rid is not None:
                    yield calendarObject.split(onlyThis=True, rid=rid)

                    # remove group link to ensure update (update to unknown hash would work too)
                    # FIXME: its possible that more than one group id gets updated during this single work item, so we
                    # need to make sure that ALL the group_id's are removed by this query.
                    ga = schema.GROUP_ATTENDEE
                    yield Delete(From=ga,
                                 Where=(ga.RESOURCE_ID == self.resourceID).And(
                                     ga.GROUP_ID == self.groupID)).on(
                                         self.transaction)

                    # update group attendee in remaining component
                    component = yield calendarObject.componentForUser()
                    component = component.duplicate()
                    change = yield calendarObject.reconcileGroupAttendees(
                        component)
                    assert change
                    yield calendarObject._setComponentInternal(
                        component, False, ComponentUpdateState.SPLIT_OWNER)
                    returnValue(None)

            yield calendarObject.setComponent(component)
예제 #25
0
    def _matchCalendarResources(self, calresource):

        # Get the timezone property from the collection.
        tz = calresource.getTimezone()

        # Try cache
        aggregated_resources = (yield FBCacheEntry.getCacheEntry(calresource, self.attendee_uid, self.timerange)) if config.EnableFreeBusyCache else None

        if aggregated_resources is None:

            if self.accountingItems is not None:
                self.accountingItems["fb-uncached"] = self.accountingItems.get("fb-uncached", 0) + 1

            caching = False
            if config.EnableFreeBusyCache:
                # Log extended item
                if self.logItems is not None:
                    self.logItems["fb-uncached"] = self.logItems.get("fb-uncached", 0) + 1

                # We want to cache a large range of time based on the current date
                cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack))
                cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward))

                # If the requested time range would fit in our allowed cache range, trigger the cache creation
                if compareDateTime(self.timerange.getStart(), cache_start) >= 0 and compareDateTime(self.timerange.getEnd(), cache_end) <= 0:
                    cache_timerange = Period(cache_start, cache_end)
                    caching = True

            #
            # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
            # We then take those results and merge them into one VFREEBUSY component
            # with appropriate FREEBUSY properties, and return that single item as iCal data.
            #

            # Create fake filter element to match time-range
            tr = TimeRange(
                start=(cache_timerange if caching else self.timerange).getStart().getText(),
                end=(cache_timerange if caching else self.timerange).getEnd().getText(),
            )
            filter = caldavxml.Filter(
                caldavxml.ComponentFilter(
                    caldavxml.ComponentFilter(
                        tr,
                        name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
                    ),
                    name="VCALENDAR",
                )
            )
            filter = Filter(filter)
            tzinfo = filter.settimezone(tz)
            if self.accountingItems is not None:
                self.accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),)

            try:
                resources = yield calresource.search(filter, useruid=self.attendee_uid, fbtype=True)

                aggregated_resources = {}
                for name, uid, comptype, test_organizer, float, start, end, fbtype, transp in resources:
                    if transp == 'T' and fbtype != '?':
                        fbtype = 'F'
                    aggregated_resources.setdefault((name, uid, comptype, test_organizer,), []).append((
                        float,
                        tupleFromDateTime(parseSQLTimestampToPyCalendar(start)),
                        tupleFromDateTime(parseSQLTimestampToPyCalendar(end)),
                        fbtype,
                    ))

                if caching:
                    yield FBCacheEntry.makeCacheEntry(calresource, self.attendee_uid, cache_timerange, aggregated_resources)
            except IndexedSearchException:
                raise InternalDataStoreError("Invalid indexedSearch query")

        else:
            if self.accountingItems is not None:
                self.accountingItems["fb-cached"] = self.accountingItems.get("fb-cached", 0) + 1

            # Log extended item
            if self.logItems is not None:
                self.logItems["fb-cached"] = self.logItems.get("fb-cached", 0) + 1

            # Determine appropriate timezone (UTC is the default)
            tzinfo = tz.gettimezone() if tz is not None else Timezone.UTCTimezone
            filter = None

        returnValue((aggregated_resources, tzinfo, filter,))
예제 #26
0
    def testParse(self):

        for seconds, result in TestDuration.test_data:
            duration = Duration().parseText(result)
            self.assertEqual(duration.getTotalSeconds(), seconds)
예제 #27
0
class VAlarm(Component):

    sActionMap = {
        definitions.cICalProperty_ACTION_AUDIO: definitions.eAction_VAlarm_Audio,
        definitions.cICalProperty_ACTION_DISPLAY: definitions.eAction_VAlarm_Display,
        definitions.cICalProperty_ACTION_EMAIL: definitions.eAction_VAlarm_Email,
        definitions.cICalProperty_ACTION_PROCEDURE: definitions.eAction_VAlarm_Procedure,
        definitions.cICalProperty_ACTION_URI: definitions.eAction_VAlarm_URI,
        definitions.cICalProperty_ACTION_NONE: definitions.eAction_VAlarm_None,
    }

    sActionValueMap = {
        definitions.eAction_VAlarm_Audio: definitions.cICalProperty_ACTION_AUDIO,
        definitions.eAction_VAlarm_Display: definitions.cICalProperty_ACTION_DISPLAY,
        definitions.eAction_VAlarm_Email: definitions.cICalProperty_ACTION_EMAIL,
        definitions.eAction_VAlarm_Procedure: definitions.cICalProperty_ACTION_PROCEDURE,
        definitions.eAction_VAlarm_URI: definitions.cICalProperty_ACTION_URI,
        definitions.eAction_VAlarm_None: definitions.cICalProperty_ACTION_NONE,
    }

    # Classes for each action encapsulating action-specific data
    class VAlarmAction(object):

        propertyCardinality_1 = ()
        propertyCardinality_1_Fix_Empty = ()
        propertyCardinality_0_1 = ()
        propertyCardinality_1_More = ()

        def __init__(self, type):
            self.mType = type

        def duplicate(self):
            return VAlarm.VAlarmAction(self.mType)

        def load(self, valarm):
            pass

        def add(self, valarm):
            pass

        def remove(self, valarm):
            pass

        def getType(self):
            return self.mType


    class VAlarmAudio(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
            definitions.cICalProperty_TRIGGER,
        )

        propertyCardinality_0_1 = (
            definitions.cICalProperty_DURATION,
            definitions.cICalProperty_REPEAT,
            definitions.cICalProperty_ATTACH,
            definitions.cICalProperty_ACKNOWLEDGED,
        )

        def __init__(self, speak=None):
            super(VAlarm.VAlarmAudio, self).__init__(type=definitions.eAction_VAlarm_Audio)
            self.mSpeakText = speak

        def duplicate(self):
            return VAlarm.VAlarmAudio(self.mSpeakText)

        def load(self, valarm):
            # Get properties
            self.mSpeakText = valarm.loadValueString(definitions.cICalProperty_ACTION_X_SPEAKTEXT)

        def add(self, valarm):
            # Delete existing then add
            self.remove(valarm)

            prop = Property(definitions.cICalProperty_ACTION_X_SPEAKTEXT, self.mSpeakText)
            valarm.addProperty(prop)

        def remove(self, valarm):
            valarm.removeProperties(definitions.cICalProperty_ACTION_X_SPEAKTEXT)

        def isSpeakText(self):
            return len(self.mSpeakText) != 0

        def getSpeakText(self):
            return self.mSpeakText


    class VAlarmDisplay(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
            definitions.cICalProperty_TRIGGER,
        )

        propertyCardinality_1_Fix_Empty = (
            definitions.cICalProperty_DESCRIPTION,
        )

        propertyCardinality_0_1 = (
            definitions.cICalProperty_DURATION,
            definitions.cICalProperty_REPEAT,
            definitions.cICalProperty_ACKNOWLEDGED,
        )

        def __init__(self, description=None):
            super(VAlarm.VAlarmDisplay, self).__init__(type=definitions.eAction_VAlarm_Display)
            self.mDescription = description

        def duplicate(self):
            return VAlarm.VAlarmDisplay(self.mDescription)

        def load(self, valarm):
            # Get properties
            self.mDescription = valarm.loadValueString(definitions.cICalProperty_DESCRIPTION)

        def add(self, valarm):
            # Delete existing then add
            self.remove(valarm)

            prop = Property(definitions.cICalProperty_DESCRIPTION, self.mDescription)
            valarm.addProperty(prop)

        def remove(self, valarm):
            valarm.removeProperties(definitions.cICalProperty_DESCRIPTION)

        def getDescription(self):
            return self.mDescription


    class VAlarmEmail(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
            definitions.cICalProperty_TRIGGER,
        )

        propertyCardinality_1_Fix_Empty = (
            definitions.cICalProperty_DESCRIPTION,
            definitions.cICalProperty_SUMMARY,
        )

        propertyCardinality_0_1 = (
            definitions.cICalProperty_DURATION,
            definitions.cICalProperty_REPEAT,
            definitions.cICalProperty_ACKNOWLEDGED,
        )

        propertyCardinality_1_More = (
            definitions.cICalProperty_ATTENDEE,
        )

        def __init__(self, description=None, summary=None, attendees=None):
            super(VAlarm.VAlarmEmail, self).__init__(type=definitions.eAction_VAlarm_Email)
            self.mDescription = description
            self.mSummary = summary
            self.mAttendees = attendees

        def duplicate(self):
            return VAlarm.VAlarmEmail(self.mDescription, self.mSummary, self.mAttendees)

        def load(self, valarm):
            # Get properties
            self.mDescription = valarm.loadValueString(definitions.cICalProperty_DESCRIPTION)
            self.mSummary = valarm.loadValueString(definitions.cICalProperty_SUMMARY)

            self.mAttendees = []
            if valarm.hasProperty(definitions.cICalProperty_ATTENDEE):
                # Get each attendee
                range = valarm.getProperties().get(definitions.cICalProperty_ATTENDEE, ())
                for iter in range:
                    # Get the attendee value
                    attendee = iter.getCalAddressValue()
                    if attendee is not None:
                        self.mAttendees.append(attendee.getValue())

        def add(self, valarm):
            # Delete existing then add
            self.remove(valarm)

            prop = Property(definitions.cICalProperty_DESCRIPTION, self.mDescription)
            valarm.addProperty(prop)

            prop = Property(definitions.cICalProperty_SUMMARY, self.mSummary)
            valarm.addProperty(prop)

            for iter in self.mAttendees:
                prop = Property(definitions.cICalProperty_ATTENDEE, iter, Value.VALUETYPE_CALADDRESS)
                valarm.addProperty(prop)

        def remove(self, valarm):
            valarm.removeProperties(definitions.cICalProperty_DESCRIPTION)
            valarm.removeProperties(definitions.cICalProperty_SUMMARY)
            valarm.removeProperties(definitions.cICalProperty_ATTENDEE)

        def getDescription(self):
            return self.mDescription

        def getSummary(self):
            return self.mSummary

        def getAttendees(self):
            return self.mAttendees


    class VAlarmUnknown(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
            definitions.cICalProperty_TRIGGER,
        )

        propertyCardinality_0_1 = (
            definitions.cICalProperty_DURATION,
            definitions.cICalProperty_REPEAT,
            definitions.cICalProperty_ACKNOWLEDGED,
        )

        def __init__(self):
            super(VAlarm.VAlarmUnknown, self).__init__(type=definitions.eAction_VAlarm_Unknown)

        def duplicate(self):
            return VAlarm.VAlarmUnknown()


    class VAlarmURI(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
            definitions.cICalProperty_TRIGGER,
            definitions.cICalProperty_URL,
        )

        propertyCardinality_0_1 = (
            definitions.cICalProperty_DURATION,
            definitions.cICalProperty_REPEAT,
            definitions.cICalProperty_ACKNOWLEDGED,
        )

        def __init__(self, uri=None):
            super(VAlarm.VAlarmURI, self).__init__(type=definitions.eAction_VAlarm_URI)
            self.mURI = uri

        def duplicate(self):
            return VAlarm.VAlarmURI(self.mURI)

        def load(self, valarm):
            # Get properties
            self.mURI = valarm.loadValueString(definitions.cICalProperty_URL)

        def add(self, valarm):
            # Delete existing then add
            self.remove(valarm)

            prop = Property(definitions.cICalProperty_URL, self.mURI)
            valarm.addProperty(prop)

        def remove(self, valarm):
            valarm.removeProperties(definitions.cICalProperty_URL)

        def getURI(self):
            return self.mURI


    class VAlarmNone(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
        )

        def __init__(self):
            super(VAlarm.VAlarmNone, self).__init__(type=definitions.eAction_VAlarm_None)

        def duplicate(self):
            return VAlarm.VAlarmNone()


    def getMimeComponentName(self):
        # Cannot be sent as a separate MIME object
        return None

    sActionToAlarmMap = {
        definitions.eAction_VAlarm_Audio: VAlarmAudio,
        definitions.eAction_VAlarm_Display: VAlarmDisplay,
        definitions.eAction_VAlarm_Email: VAlarmEmail,
        definitions.eAction_VAlarm_URI: VAlarmURI,
        definitions.eAction_VAlarm_None: VAlarmNone,
    }

    propertyValueChecks = ICALENDAR_VALUE_CHECKS

    def __init__(self, parent=None):

        super(VAlarm, self).__init__(parent=parent)

        self.mAction = definitions.eAction_VAlarm_Display
        self.mTriggerAbsolute = False
        self.mTriggerOnStart = True
        self.mTriggerOn = DateTime()

        # Set duration default to 1 hour
        self.mTriggerBy = Duration()
        self.mTriggerBy.setDuration(60 * 60)

        # Does not repeat by default
        self.mRepeats = 0
        self.mRepeatInterval = Duration()
        self.mRepeatInterval.setDuration(5 * 60) # Five minutes

        # Status
        self.mStatusInit = False
        self.mAlarmStatus = definitions.eAlarm_Status_Pending
        self.mLastTrigger = DateTime()
        self.mNextTrigger = DateTime()
        self.mDoneCount = 0

        # Create action data
        self.mActionData = VAlarm.VAlarmDisplay("")


    def duplicate(self, parent=None):
        other = super(VAlarm, self).duplicate(parent=parent)
        other.mAction = self.mAction
        other.mTriggerAbsolute = self.mTriggerAbsolute
        other.mTriggerOn = self.mTriggerOn.duplicate()
        other.mTriggerBy = self.mTriggerBy.duplicate()
        other.mTriggerOnStart = self.mTriggerOnStart

        other.mRepeats = self.mRepeats
        other.mRepeatInterval = self.mRepeatInterval.duplicate()

        other.mAlarmStatus = self.mAlarmStatus
        if self.mLastTrigger is not None:
            other.mLastTrigger = self.mLastTrigger.duplicate()
        if self.mNextTrigger is not None:
            other.mNextTrigger = self.mNextTrigger.duplicate()
        other.mDoneCount = self.mDoneCount

        other.mActionData = self.mActionData.duplicate()
        return other


    def getType(self):
        return definitions.cICalComponent_VALARM


    def getAction(self):
        return self.mAction


    def getActionData(self):
        return self.mActionData


    def isTriggerAbsolute(self):
        return self.mTriggerAbsolute


    def getTriggerOn(self):
        return self.mTriggerOn


    def getTriggerDuration(self):
        return self.mTriggerBy


    def isTriggerOnStart(self):
        return self.mTriggerOnStart


    def getRepeats(self):
        return self.mRepeats


    def getInterval(self):
        return self.mRepeatInterval


    def added(self):
        # Added to calendar so add to calendar notifier
        # calstore::CCalendarNotifier::sCalendarNotifier.AddAlarm(this)

        # Do inherited
        super(VAlarm, self).added()


    def removed(self):
        # Removed from calendar so add to calendar notifier
        # calstore::CCalendarNotifier::sCalendarNotifier.RemoveAlarm(this)

        # Do inherited
        super(VAlarm, self).removed()


    def changed(self):
        # Always force recalc of trigger status
        self.mStatusInit = False

        # Changed in calendar so change in calendar notifier
        # calstore::CCalendarNotifier::sCalendarNotifier.ChangedAlarm(this)

        # Do not do inherited as this is always a sub-component and we do not
        # do top-level component changes
        # super.changed()


    def finalise(self):
        # Do inherited
        super(VAlarm, self).finalise()

        # Get the ACTION
        temp = self.loadValueString(definitions.cICalProperty_ACTION)
        if temp is not None:
            self.mAction = VAlarm.sActionMap.get(temp, definitions.eAction_VAlarm_Unknown)
            self.loadAction()

        # Get the trigger
        if self.hasProperty(definitions.cICalProperty_TRIGGER):
            # Determine the type of the value
            temp1 = self.loadValueDateTime(definitions.cICalProperty_TRIGGER)
            temp2 = self.loadValueDuration(definitions.cICalProperty_TRIGGER)
            if temp1 is not None:
                self.mTriggerAbsolute = True
                self.mTriggerOn = temp1
            elif temp2 is not None:
                self.mTriggerAbsolute = False
                self.mTriggerBy = temp2

                # Get the property
                prop = self.findFirstProperty(definitions.cICalProperty_TRIGGER)

                # Look for RELATED parameter
                if prop.hasParameter(definitions.cICalParameter_RELATED):
                    temp = prop.getParameterValue(definitions.cICalParameter_RELATED)
                    if temp == definitions.cICalParameter_RELATED_START:
                        self.mTriggerOnStart = True
                    elif temp == definitions.cICalParameter_RELATED_END:
                        self.mTriggerOnStart = False
                else:
                    self.mTriggerOnStart = True

        # Get repeat & interval
        temp = self.loadValueInteger(definitions.cICalProperty_REPEAT)
        if temp is not None:
            self.mRepeats = temp
        temp = self.loadValueDuration(definitions.cICalProperty_DURATION)
        if temp is not None:
            self.mRepeatInterval = temp

        # Set a map key for sorting
        self.mMapKey = "%s:%s" % (self.mAction, self.mTriggerOn if self.mTriggerAbsolute else self.mTriggerBy,)

        # Alarm status - private to Mulberry
        status = self.loadValueString(definitions.cICalProperty_ALARM_X_ALARMSTATUS)
        if status is not None:
            if status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING:
                self.mAlarmStatus = definitions.eAlarm_Status_Pending
            elif status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED:
                self.mAlarmStatus = definitions.eAlarm_Status_Completed
            elif status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED:
                self.mAlarmStatus = definitions.eAlarm_Status_Disabled
            else:
                self.mAlarmStatus = definitions.eAlarm_Status_Pending

        # Last trigger time - private to Mulberry
        temp = self.loadValueDateTime(definitions.cICalProperty_ALARM_X_LASTTRIGGER)
        if temp is not None:
            self.mLastTrigger = temp


    def validate(self, doFix=False):
        """
        Validate the data in this component and optionally fix any problems, else raise. If
        loggedProblems is not None it must be a C{list} and problem descriptions are appended
        to that.
        """

        # Validate using action specific constraints
        self.propertyCardinality_1 = self.mActionData.propertyCardinality_1
        self.propertyCardinality_1_Fix_Empty = self.mActionData.propertyCardinality_1_Fix_Empty
        self.propertyCardinality_0_1 = self.mActionData.propertyCardinality_0_1
        self.propertyCardinality_1_More = self.mActionData.propertyCardinality_1_More

        fixed, unfixed = super(VAlarm, self).validate(doFix)

        # Extra constraint: both DURATION and REPEAT must be present togethe
        if self.hasProperty(definitions.cICalProperty_DURATION) ^ self.hasProperty(definitions.cICalProperty_REPEAT):
            # Cannot fix this
            logProblem = "[%s] Properties must be present together: %s, %s" % (
                self.getType(),
                definitions.cICalProperty_DURATION,
                definitions.cICalProperty_REPEAT,
            )
            unfixed.append(logProblem)

        return fixed, unfixed


    def editStatus(self, status):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_ALARM_X_ALARMSTATUS)

        # Updated cached values
        self.mAlarmStatus = status

        # Add new
        status_txt = ""
        if self.mAlarmStatus == definitions.eAlarm_Status_Pending:
            status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING
        elif self.mAlarmStatus == definitions.eAlarm_Status_Completed:
            status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED
        elif self.mAlarmStatus == definitions.eAlarm_Status_Disabled:
            status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED
        self.addProperty(Property(definitions.cICalProperty_ALARM_X_ALARMSTATUS, status_txt))


    def editAction(self, action, data):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_ACTION)
        self.mActionData.remove(self)
        self.mActionData = None

        # Updated cached values
        self.mAction = action
        self.mActionData = data

        # Add new properties to alarm
        action_txt = VAlarm.sActionValueMap.get(self.mAction, definitions.cICalProperty_ACTION_PROCEDURE)

        prop = Property(definitions.cICalProperty_ACTION, action_txt)
        self.addProperty(prop)

        self.mActionData.add(self)


    def editTriggerOn(self, dt):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_TRIGGER)

        # Updated cached values
        self.mTriggerAbsolute = True
        self.mTriggerOn = dt

        # Add new
        prop = Property(definitions.cICalProperty_TRIGGER, dt)
        self.addProperty(prop)


    def editTriggerBy(self, duration, trigger_start):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_TRIGGER)

        # Updated cached values
        self.mTriggerAbsolute = False
        self.mTriggerBy = duration
        self.mTriggerOnStart = trigger_start

        # Add new (with parameter)
        prop = Property(definitions.cICalProperty_TRIGGER, duration)
        attr = Parameter(
            definitions.cICalParameter_RELATED,
            (
                definitions.cICalParameter_RELATED_START,
                definitions.cICalParameter_RELATED_END
            )[not trigger_start]
        )
        prop.addParameter(attr)
        self.addProperty(prop)


    def editRepeats(self, repeat, interval):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_REPEAT)
        self.removeProperties(definitions.cICalProperty_DURATION)

        # Updated cached values
        self.mRepeats = repeat
        self.mRepeatInterval = interval

        # Add new
        if self.mRepeats > 0:
            self.addProperty(Property(definitions.cICalProperty_REPEAT, repeat))
            self.addProperty(Property(definitions.cICalProperty_DURATION, interval))


    def getAlarmStatus(self):
        return self.mAlarmStatus


    def getNextTrigger(self, dt):
        if not self.mStatusInit:
            self.initNextTrigger()
        dt.copy(self.mNextTrigger)


    def alarmTriggered(self, dt):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_ALARM_X_LASTTRIGGER)
        self.removeProperties(definitions.cICalProperty_ALARM_X_ALARMSTATUS)

        # Updated cached values
        self.mLastTrigger.copy(dt)

        if self.mDoneCount < self.mRepeats:
            self.mNextTrigger = self.mLastTrigger + self.mRepeatInterval
            dt.copy(self.mNextTrigger)
            self.mDoneCount += 1
            self.mAlarmStatus = definitions.eAlarm_Status_Pending
        else:
            self.mAlarmStatus = definitions.eAlarm_Status_Completed

        # Add new
        self.addProperty(Property(definitions.cICalProperty_ALARM_X_LASTTRIGGER, dt))
        status = ""
        if self.mAlarmStatus == definitions.eAlarm_Status_Pending:
            status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING
        elif self.mAlarmStatus == definitions.eAlarm_Status_Completed:
            status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED
        elif self.mAlarmStatus == definitions.eAlarm_Status_Disabled:
            status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED
        self.addProperty(Property(definitions.cICalProperty_ALARM_X_ALARMSTATUS, status))

        # Now update dt to the next alarm time
        return self.mAlarmStatus == definitions.eAlarm_Status_Pending


    def loadAction(self):
        # Delete current one
        self.mActionData = None
        self.mActionData = VAlarm.sActionToAlarmMap.get(self.mAction, VAlarm.VAlarmUnknown)()
        self.mActionData.load(self)


    def initNextTrigger(self):
        # Do not bother if its completed
        if self.mAlarmStatus == definitions.eAlarm_Status_Completed:
            return
        self.mStatusInit = True

        # Look for trigger immediately preceeding or equal to utc now
        nowutc = DateTime.getNowUTC()

        # Init done counter
        self.mDoneCount = 0

        # Determine the first trigger
        trigger = DateTime()
        self.getFirstTrigger(trigger)

        while self.mDoneCount < self.mRepeats:
            # See if next trigger is later than now
            next_trigger = trigger + self.mRepeatInterval
            if next_trigger > nowutc:
                break
            self.mDoneCount += 1
            trigger = next_trigger

        # Check for completion
        if trigger == self.mLastTrigger or (nowutc - trigger).getTotalSeconds() > 24 * 60 * 60:
            if self.mDoneCount == self.mRepeats:
                self.mAlarmStatus = definitions.eAlarm_Status_Completed
                return
            else:
                trigger = trigger + self.mRepeatInterval
                self.mDoneCount += 1

        self.mNextTrigger = trigger


    def getFirstTrigger(self, dt):
        # If absolute trigger, use that
        if self.isTriggerAbsolute():
            # Get the trigger on
            dt.copy(self.getTriggerOn())
        else:
            # Get the parent embedder class (must be CICalendarComponentRecur type)
            owner = self.getEmbedder()
            if owner is not None:
                # Determine time at which alarm will trigger
                trigger = (owner.getStart(), owner.getEnd())[not self.isTriggerOnStart()]

                # Offset by duration
                dt.copy(trigger + self.getTriggerDuration())
예제 #28
0
    def testParse(self):

        for seconds, result in TestDuration.test_data:
            duration = Duration().parseText(result)
            self.assertEqual(duration.getTotalSeconds(), seconds)
예제 #29
0
    def outbound(self, txn, originator, recipient, calendar, onlyAfter=None):
        """
        Generates and sends an outbound IMIP message.

        @param txn: the transaction to use for looking up/creating tokens
        @type txn: L{CommonStoreTransaction}
        """

        if onlyAfter is None:
            duration = Duration(days=self.suppressionDays)
            onlyAfter = DateTime.getNowUTC() - duration

        icaluid = calendar.resourceUID()
        method = calendar.propertyValue("METHOD")

        # Clean up the attendee list which is purely used within the human
        # readable email message (not modifying the calendar body)
        attendees = []
        for attendeeProp in calendar.getAllAttendeeProperties():
            cutype = attendeeProp.parameterValue("CUTYPE", "INDIVIDUAL")
            if cutype == "INDIVIDUAL":
                cn = attendeeProp.parameterValue("CN", None)
                if cn is not None:
                    cn = cn.decode("utf-8")
                cuaddr = normalizeCUAddr(attendeeProp.value())
                if cuaddr.startswith("mailto:"):
                    mailto = cuaddr[7:]
                    if not cn:
                        cn = mailto
                else:
                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
                    if emailAddress:
                        mailto = emailAddress
                    else:
                        mailto = None

                if cn or mailto:
                    attendees.append((cn, mailto))

        toAddr = recipient
        if not recipient.lower().startswith("mailto:"):
            raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP "
                             "operation." % (recipient, ))
        recipient = recipient[7:]

        if method != "REPLY":
            # Invites and cancellations:

            # Reuse or generate a token based on originator, toAddr, and
            # event uid
            token = (yield txn.imipGetToken(originator, toAddr.lower(),
                                            icaluid))
            if token is None:

                # Because in the past the originator was sometimes in mailto:
                # form, lookup an existing token by mailto: as well
                organizerProperty = calendar.getOrganizerProperty()
                organizerEmailAddress = organizerProperty.parameterValue(
                    "EMAIL", None)
                if organizerEmailAddress is not None:
                    token = (yield txn.imipGetToken(
                        "mailto:%s" % (organizerEmailAddress.lower(), ),
                        toAddr.lower(), icaluid))

            if token is None:
                token = (yield txn.imipCreateToken(originator, toAddr.lower(),
                                                   icaluid))
                self.log.debug(
                    "Mail gateway created token %s for %s "
                    "(originator), %s (recipient) and %s (icaluid)" %
                    (token, originator, toAddr, icaluid))
                inviteState = "new"

            else:
                self.log.debug(
                    "Mail gateway reusing token %s for %s "
                    "(originator), %s (recipient) and %s (icaluid)" %
                    (token, originator, toAddr, icaluid))
                inviteState = "update"

            fullServerAddress = self.address
            _ignore_name, serverAddress = email.utils.parseaddr(
                fullServerAddress)
            pre, post = serverAddress.split('@')
            addressWithToken = "%s+%s@%s" % (pre, token, post)

            organizerProperty = calendar.getOrganizerProperty()
            organizerEmailAddress = organizerProperty.parameterValue(
                "EMAIL", None)
            organizerValue = organizerProperty.value()
            organizerProperty.setValue("mailto:%s" % (addressWithToken, ))

            # If the organizer is also an attendee, update that attendee value
            # to match
            organizerAttendeeProperty = calendar.getAttendeeProperty(
                [organizerValue])
            if organizerAttendeeProperty is not None:
                organizerAttendeeProperty.setValue("mailto:%s" %
                                                   (addressWithToken, ))

            # The email's From will include the originator's real name email
            # address if available.  Otherwise it will be the server's email
            # address (without # + addressing)
            if organizerEmailAddress:
                orgEmail = fromAddr = organizerEmailAddress
            else:
                fromAddr = serverAddress
                orgEmail = None
            cn = calendar.getOrganizerProperty().parameterValue('CN', None)
            if cn is None:
                cn = u'Calendar Server'
                orgCN = orgEmail
            else:
                orgCN = cn = cn.decode("utf-8")

            # a unicode cn (rather than an encode string value) means the
            # from address will get properly encoded per rfc2047 within the
            # MIMEMultipart in generateEmail
            formattedFrom = "%s <%s>" % (cn, fromAddr)

            # Reply-to address will be the server+token address

        else:  # REPLY
            inviteState = "reply"

            # Look up the attendee property corresponding to the originator
            # of this reply
            originatorAttendeeProperty = calendar.getAttendeeProperty(
                [originator])
            formattedFrom = fromAddr = originator = ""
            if originatorAttendeeProperty:
                originatorAttendeeEmailAddress = (
                    originatorAttendeeProperty.parameterValue("EMAIL", None))
                if originatorAttendeeEmailAddress:
                    formattedFrom = fromAddr = originator = (
                        originatorAttendeeEmailAddress)

            organizerMailto = str(calendar.getOrganizer())
            if not organizerMailto.lower().startswith("mailto:"):
                raise ValueError("ORGANIZER address '%s' must be mailto: "
                                 "for REPLY." % (organizerMailto, ))
            orgEmail = organizerMailto[7:]

            orgCN = calendar.getOrganizerProperty().parameterValue('CN', None)
            addressWithToken = formattedFrom

        # At the point we've created the token in the db, which we always
        # want to do, but if this message is for an event completely in
        # the past we don't want to actually send an email.
        if not calendar.hasInstancesAfter(onlyAfter):
            self.log.debug("Skipping IMIP message for old event")
            returnValue(True)

        # Now prevent any "internal" CUAs from being exposed by converting
        # to mailto: if we have one
        for attendeeProp in calendar.getAllAttendeeProperties():
            cutype = attendeeProp.parameterValue('CUTYPE', None)
            if cutype == "INDIVIDUAL":
                cuaddr = normalizeCUAddr(attendeeProp.value())
                if not cuaddr.startswith("mailto:"):
                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
                    if emailAddress:
                        attendeeProp.setValue("mailto:%s" % (emailAddress, ))

        msgId, message = self.generateEmail(inviteState,
                                            calendar,
                                            orgEmail,
                                            orgCN,
                                            attendees,
                                            formattedFrom,
                                            addressWithToken,
                                            recipient,
                                            language=self.language)

        try:
            success = (yield
                       self.smtpSender.sendMessage(fromAddr, toAddr, msgId,
                                                   message))
            returnValue(success)
        except Exception, e:
            self.log.error("Failed to send IMIP message (%s)" % (str(e), ))
            returnValue(False)
예제 #30
0
    def _processFBURL(self, request):
        #
        # Check authentication and access controls
        #
        yield self.authorize(request, (davxml.Read(),))

        # Extract query parameters from the URL
        args = ('start', 'end', 'duration', 'token', 'format', 'user',)
        for arg in args:
            setattr(self, arg, request.args.get(arg, [None])[0])

        # Some things we do not handle
        if self.token or self.user:
            raise HTTPError(ErrorResponse(
                responsecode.NOT_ACCEPTABLE,
                (calendarserver_namespace, "supported-query-parameter"),
                "Invalid query parameter",
            ))

        # Check format
        if self.format:
            self.format = self.format.split(";")[0]
            if self.format not in ("text/calendar", "text/plain"):
                raise HTTPError(ErrorResponse(
                    responsecode.NOT_ACCEPTABLE,
                    (calendarserver_namespace, "supported-format"),
                    "Invalid return format requested",
                ))
        else:
            self.format = "text/calendar"

        # Start/end/duration must be valid iCalendar DATE-TIME UTC or DURATION values
        try:
            if self.start:
                self.start = DateTime.parseText(self.start)
                if not self.start.utc():
                    raise ValueError()
            if self.end:
                self.end = DateTime.parseText(self.end)
                if not self.end.utc():
                    raise ValueError()
            if self.duration:
                self.duration = Duration.parseText(self.duration)
        except ValueError:
            raise HTTPError(ErrorResponse(
                responsecode.BAD_REQUEST,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # Sanity check start/end/duration

        # End and duration cannot both be present
        if self.end and self.duration:
            raise HTTPError(ErrorResponse(
                responsecode.NOT_ACCEPTABLE,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # Duration must be positive
        if self.duration and self.duration.getTotalSeconds() < 0:
            raise HTTPError(ErrorResponse(
                responsecode.BAD_REQUEST,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # Now fill in the missing pieces
        if self.start is None:
            self.start = DateTime.getNowUTC()
            self.start.setHHMMSS(0, 0, 0)
        if self.duration:
            self.end = self.start + self.duration
        if self.end is None:
            self.end = self.start + Duration(days=config.FreeBusyURL.TimePeriod)

        # End > start
        if self.end <= self.start:
            raise HTTPError(ErrorResponse(
                responsecode.BAD_REQUEST,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long)

        # Now lookup the principal details for the targeted user
        principal = (yield self.parent.principalForRecord())

        # Pick the first mailto cu address or the first other type
        cuaddr = None
        for item in principal.calendarUserAddresses():
            if cuaddr is None:
                cuaddr = item
            if item.startswith("mailto:"):
                cuaddr = item
                break

        # Get inbox details
        inboxURL = principal.scheduleInboxURL()
        if inboxURL is None:
            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox URL for principal: %s" % (principal,)))
        try:
            inbox = (yield request.locateResource(inboxURL))
        except:
            log.error("No schedule inbox for principal: {p}", p=principal)
            inbox = None
        if inbox is None:
            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,)))

        organizer = recipient = LocalCalendarUser(cuaddr, principal.record)
        recipient.inbox = inbox._newStoreObject
        attendeeProp = Property("ATTENDEE", recipient.cuaddr)
        timerange = Period(self.start, self.end)

        fbresult = yield FreebusyQuery(
            organizer=organizer,
            recipient=recipient,
            attendeeProp=attendeeProp,
            timerange=timerange,
        ).generateAttendeeFreeBusyResponse()

        response = Response()
        response.stream = MemoryStream(str(fbresult))
        response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,)))

        returnValue(response)
예제 #31
0
def _internalGenerateFreeBusyInfo(
    calresource,
    fbinfo,
    timerange,
    matchtotal,
    excludeuid=None,
    organizer=None,
    organizerPrincipal=None,
    same_calendar_user=False,
    servertoserver=False,
    event_details=None,
    logItems=None,
    accountingItems=None,
):
    """
    Run a free busy report on the specified calendar collection
    accumulating the free busy info for later processing.
    @param calresource: the L{Calendar} for a calendar collection.
    @param fbinfo:      the array of busy periods to update.
    @param timerange:   the L{TimeRange} for the query.
    @param matchtotal:  the running total for the number of matches.
    @param excludeuid:  a C{str} containing a UID value to exclude any
        components with that UID from contributing to free-busy.
    @param organizer:   a C{str} containing the value of the ORGANIZER property
        in the VFREEBUSY request.  This is used in conjunction with the UID
        value to process exclusions.
    @param same_calendar_user: a C{bool} indicating whether the calendar user
        requesting the free-busy information is the same as the calendar user
        being targeted.
    @param servertoserver: a C{bool} indicating whether we are doing a local or
        remote lookup request.
    @param event_details: a C{list} into which to store extended VEVENT details if not C{None}
    @param logItems: a C{dict} to store logging info to
    @param accountingItems: a C{dict} to store accounting info to
    """

    # First check the privilege on this collection
    # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
    # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy

    # May need organizer principal
    organizer_record = (yield calresource.directoryService(
    ).recordWithCalendarUserAddress(organizer)) if organizer else None
    organizer_uid = organizer_record.uid if organizer_record else ""

    # Free busy is per-user
    attendee_uid = calresource.viewerHome().uid()
    attendee_record = yield calresource.directoryService().recordWithUID(
        attendee_uid.decode("utf-8"))

    # Get the timezone property from the collection.
    tz = calresource.getTimezone()

    # Look for possible extended free busy information
    rich_options = {
        "organizer": False,
        "delegate": False,
        "resource": False,
    }
    do_event_details = False
    if event_details is not None and organizer_record is not None and attendee_record is not None:

        # Get the principal of the authorized user which may be different from the organizer if a delegate of
        # the organizer is making the request
        authz_uid = organizer_uid
        authz_record = organizer_record
        if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid:
            authz_uid = calresource._txn._authz_uid
            authz_record = yield calresource.directoryService().recordWithUID(
                authz_uid.decode("utf-8"))

        # Check if attendee is also the organizer or the delegate doing the request
        if attendee_uid in (organizer_uid, authz_uid):
            do_event_details = True
            rich_options["organizer"] = True

        # Check if authorized user is a delegate of attendee
        proxy = (yield authz_record.isProxyFor(attendee_record))
        if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
            do_event_details = True
            rich_options["delegate"] = True

        # Check if attendee is room or resource
        if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType(
        ) in (
                "RESOURCE",
                "ROOM",
        ):
            do_event_details = True
            rich_options["resource"] = True

    # Try cache
    resources = (yield FBCacheEntry.getCacheEntry(
        calresource, attendee_uid,
        timerange)) if config.EnableFreeBusyCache else None

    if resources is None:

        if accountingItems is not None:
            accountingItems["fb-uncached"] = accountingItems.get(
                "fb-uncached", 0) + 1

        caching = False
        if config.EnableFreeBusyCache:
            # Log extended item
            if logItems is not None:
                logItems["fb-uncached"] = logItems.get("fb-uncached", 0) + 1

            # We want to cache a large range of time based on the current date
            cache_start = normalizeToUTC(DateTime.getToday() + Duration(
                days=0 - config.FreeBusyCacheDaysBack))
            cache_end = normalizeToUTC(DateTime.getToday() + Duration(
                days=config.FreeBusyCacheDaysForward))

            # If the requested time range would fit in our allowed cache range, trigger the cache creation
            if compareDateTime(timerange.start,
                               cache_start) >= 0 and compareDateTime(
                                   timerange.end, cache_end) <= 0:
                cache_timerange = TimeRange(start=cache_start.getText(),
                                            end=cache_end.getText())
                caching = True

        #
        # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
        # We then take those results and merge them into one VFREEBUSY component
        # with appropriate FREEBUSY properties, and return that single item as iCal data.
        #

        # Create fake filter element to match time-range
        filter = caldavxml.Filter(
            caldavxml.ComponentFilter(
                caldavxml.ComponentFilter(
                    cache_timerange if caching else timerange,
                    name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
                ),
                name="VCALENDAR",
            ))
        filter = Filter(filter)
        tzinfo = filter.settimezone(tz)
        if accountingItems is not None:
            tr = cache_timerange if caching else timerange
            accountingItems["fb-query-timerange"] = (
                str(tr.start),
                str(tr.end),
            )

        try:
            resources = yield calresource.search(filter,
                                                 useruid=attendee_uid,
                                                 fbtype=True)
            if caching:
                yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid,
                                                  cache_timerange, resources)
        except IndexedSearchException:
            raise InternalDataStoreError("Invalid indexedSearch query")

    else:
        if accountingItems is not None:
            accountingItems["fb-cached"] = accountingItems.get("fb-cached",
                                                               0) + 1

        # Log extended item
        if logItems is not None:
            logItems["fb-cached"] = logItems.get("fb-cached", 0) + 1

        # Determine appropriate timezone (UTC is the default)
        tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)

    # We care about separate instances for VEVENTs only
    aggregated_resources = {}
    for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
        if transp == 'T' and fbtype != '?':
            fbtype = 'F'
        aggregated_resources.setdefault((
            name,
            uid,
            type,
            test_organizer,
        ), []).append((
            float,
            start,
            end,
            fbtype,
        ))

    if accountingItems is not None:
        accountingItems["fb-resources"] = {}
        for k, v in aggregated_resources.items():
            name, uid, type, test_organizer = k
            accountingItems["fb-resources"][uid] = []
            for float, start, end, fbtype in v:
                fbstart = parseSQLTimestampToPyCalendar(start)
                if float == 'Y':
                    fbstart.setTimezone(tzinfo)
                else:
                    fbstart.setTimezone(Timezone(utc=True))
                fbend = parseSQLTimestampToPyCalendar(end)
                if float == 'Y':
                    fbend.setTimezone(tzinfo)
                else:
                    fbend.setTimezone(Timezone(utc=True))
                accountingItems["fb-resources"][uid].append((
                    float,
                    str(fbstart),
                    str(fbend),
                    fbtype,
                ))

    # Cache directory record lookup outside this loop as it is expensive and will likely
    # always end up being called with the same organizer address.
    recordUIDCache = {}
    for key in aggregated_resources.iterkeys():

        name, uid, type, test_organizer = key

        # Short-cut - if an fbtype exists we can use that
        if type == "VEVENT" and aggregated_resources[key][0][3] != '?':

            matchedResource = False

            # Look at each instance
            for float, start, end, fbtype in aggregated_resources[key]:
                # Ignore free time or unknown
                if fbtype in ('F', '?'):
                    continue

                # Ignore ones of this UID
                if excludeuid:
                    # See if we have a UID match
                    if (excludeuid == uid):
                        if test_organizer:
                            test_uid = recordUIDCache.get(test_organizer)
                            if test_uid is None:
                                test_record = (yield
                                               calresource.directoryService(
                                               ).recordWithCalendarUserAddress(
                                                   test_organizer))
                                test_uid = test_record.uid if test_record else ""
                                recordUIDCache[test_organizer] = test_uid
                        else:
                            test_uid = ""

                        # Check that ORGANIZER's match (security requirement)
                        if (organizer is None) or (organizer_uid == test_uid):
                            continue
                        # Check for no ORGANIZER and check by same calendar user
                        elif (test_uid == "") and same_calendar_user:
                            continue

                # Apply a timezone to any floating times
                fbstart = parseSQLTimestampToPyCalendar(start)
                if float == 'Y':
                    fbstart.setTimezone(tzinfo)
                else:
                    fbstart.setTimezone(Timezone(utc=True))
                fbend = parseSQLTimestampToPyCalendar(end)
                if float == 'Y':
                    fbend.setTimezone(tzinfo)
                else:
                    fbend.setTimezone(Timezone(utc=True))

                # Clip instance to time range
                clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart),
                                     Period(timerange.start, timerange.end))

                # Double check for overlap
                if clipped:
                    matchedResource = True
                    fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped)

            if matchedResource:
                # Check size of results is within limit
                matchtotal += 1
                if matchtotal > config.MaxQueryWithDataResults:
                    raise QueryMaxResources(config.MaxQueryWithDataResults,
                                            matchtotal)

                # Add extended details
                if do_event_details:
                    child = (yield calresource.calendarObjectWithName(name))
                    # Only add fully public events
                    if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
                        calendar = (yield child.componentForUser())
                        _addEventDetails(calendar, event_details, rich_options,
                                         timerange, tzinfo)

        else:
            child = (yield calresource.calendarObjectWithName(name))
            calendar = (yield child.componentForUser())

            # The calendar may come back as None if the resource is being changed, or was deleted
            # between our initial index query and getting here. For now we will ignore this error, but in
            # the longer term we need to implement some form of locking, perhaps.
            if calendar is None:
                log.error(
                    "Calendar %s is missing from calendar collection %r" %
                    (name, calresource))
                continue

            # Ignore ones of this UID
            if excludeuid:
                # See if we have a UID match
                if (excludeuid == uid):
                    test_organizer = calendar.getOrganizer()
                    if test_organizer:
                        test_uid = recordUIDCache.get(test_organizer)
                        if test_uid is None:
                            test_record = (yield calresource.directoryService(
                            ).recordWithCalendarUserAddress(test_organizer))
                            test_uid = test_record.uid if test_record else ""
                            recordUIDCache[test_organizer] = test_uid
                    else:
                        test_uid = ""

                    # Check that ORGANIZER's match (security requirement)
                    if (organizer is None) or (organizer_uid == test_uid):
                        continue
                    # Check for no ORGANIZER and check by same calendar user
                    elif (test_organizer is None) and same_calendar_user:
                        continue

            if accountingItems is not None:
                accountingItems.setdefault("fb-filter-match", []).append(uid)

            if filter.match(calendar, None):
                if accountingItems is not None:
                    accountingItems.setdefault("fb-filter-matched",
                                               []).append(uid)

                # Check size of results is within limit
                matchtotal += 1
                if matchtotal > config.MaxQueryWithDataResults:
                    raise QueryMaxResources(config.MaxQueryWithDataResults,
                                            matchtotal)

                if calendar.mainType() == "VEVENT":
                    processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
                elif calendar.mainType() == "VFREEBUSY":
                    processFreeBusyFreeBusy(calendar, fbinfo, timerange)
                elif calendar.mainType() == "VAVAILABILITY":
                    processAvailabilityFreeBusy(calendar, fbinfo, timerange)
                else:
                    assert "Free-busy query returned unwanted component: %s in %r", (
                        name,
                        calresource,
                    )

                # Add extended details
                if calendar.mainType() == "VEVENT" and do_event_details:
                    child = (yield calresource.calendarObjectWithName(name))
                    # Only add fully public events
                    if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
                        calendar = (yield child.componentForUser())
                        _addEventDetails(calendar, event_details, rich_options,
                                         timerange, tzinfo)

    returnValue(matchtotal)
예제 #32
0
    def _processFBURL(self, request):
        #
        # Check authentication and access controls
        #
        yield self.authorize(request, (davxml.Read(),))

        # Extract query parameters from the URL
        args = ('start', 'end', 'duration', 'token', 'format', 'user',)
        for arg in args:
            setattr(self, arg, request.args.get(arg, [None])[0])

        # Some things we do not handle
        if self.token or self.user:
            raise HTTPError(ErrorResponse(
                responsecode.NOT_ACCEPTABLE,
                (calendarserver_namespace, "supported-query-parameter"),
                "Invalid query parameter",
            ))

        # Check format
        if self.format:
            self.format = self.format.split(";")[0]
            if self.format not in ("text/calendar", "text/plain"):
                raise HTTPError(ErrorResponse(
                    responsecode.NOT_ACCEPTABLE,
                    (calendarserver_namespace, "supported-format"),
                    "Invalid return format requested",
                ))
        else:
            self.format = "text/calendar"

        # Start/end/duration must be valid iCalendar DATE-TIME UTC or DURATION values
        try:
            if self.start:
                self.start = DateTime.parseText(self.start)
                if not self.start.utc():
                    raise ValueError()
            if self.end:
                self.end = DateTime.parseText(self.end)
                if not self.end.utc():
                    raise ValueError()
            if self.duration:
                self.duration = Duration.parseText(self.duration)
        except ValueError:
            raise HTTPError(ErrorResponse(
                responsecode.BAD_REQUEST,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # Sanity check start/end/duration

        # End and duration cannot both be present
        if self.end and self.duration:
            raise HTTPError(ErrorResponse(
                responsecode.NOT_ACCEPTABLE,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # Duration must be positive
        if self.duration and self.duration.getTotalSeconds() < 0:
            raise HTTPError(ErrorResponse(
                responsecode.BAD_REQUEST,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # Now fill in the missing pieces
        if self.start is None:
            self.start = DateTime.getNowUTC()
            self.start.setHHMMSS(0, 0, 0)
        if self.duration:
            self.end = self.start + self.duration
        if self.end is None:
            self.end = self.start + Duration(days=config.FreeBusyURL.TimePeriod)

        # End > start
        if self.end <= self.start:
            raise HTTPError(ErrorResponse(
                responsecode.BAD_REQUEST,
                (calendarserver_namespace, "valid-query-parameters"),
                "Invalid query parameters",
            ))

        # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long)

        # Now lookup the principal details for the targeted user
        principal = (yield self.parent.principalForRecord())

        # Pick the first mailto cu address or the first other type
        cuaddr = None
        for item in principal.calendarUserAddresses():
            if cuaddr is None:
                cuaddr = item
            if item.startswith("mailto:"):
                cuaddr = item
                break

        # Get inbox details
        inboxURL = principal.scheduleInboxURL()
        if inboxURL is None:
            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox URL for principal: %s" % (principal,)))
        try:
            inbox = (yield request.locateResource(inboxURL))
        except:
            log.error("No schedule inbox for principal: %s" % (principal,))
            inbox = None
        if inbox is None:
            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,)))

        organizer = recipient = LocalCalendarUser(cuaddr, principal.record)
        recipient.inbox = inbox._newStoreObject
        attendeeProp = Property("ATTENDEE", recipient.cuaddr)
        timerange = Period(self.start, self.end)

        fbresult = yield FreebusyQuery(
            organizer=organizer,
            recipient=recipient,
            attendeeProp=attendeeProp,
            timerange=timerange,
        ).generateAttendeeFreeBusyResponse()

        response = Response()
        response.stream = MemoryStream(str(fbresult))
        response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,)))

        returnValue(response)
예제 #33
0
def generateFreeBusyInfo(
    request,
    calresource,
    fbinfo,
    timerange,
    matchtotal,
    excludeuid=None,
    organizer=None,
    organizerPrincipal=None,
    same_calendar_user=False,
    servertoserver=False,
    event_details=None,
):
    """
    Run a free busy report on the specified calendar collection
    accumulating the free busy info for later processing.
    @param request:     the L{IRequest} for the current request.
    @param calresource: the L{CalDAVResource} for a calendar collection.
    @param fbinfo:      the array of busy periods to update.
    @param timerange:   the L{TimeRange} for the query.
    @param matchtotal:  the running total for the number of matches.
    @param excludeuid:  a C{str} containing a UID value to exclude any
        components with that UID from contributing to free-busy.
    @param organizer:   a C{str} containing the value of the ORGANIZER property
        in the VFREEBUSY request.  This is used in conjunction with the UID
        value to process exclusions.
    @param same_calendar_user: a C{bool} indicating whether the calendar user
        requesting the free-busy information is the same as the calendar user
        being targeted.
    @param servertoserver: a C{bool} indicating whether we are doing a local or
        remote lookup request.
    @param event_details: a C{list} into which to store extended VEVENT details if not C{None}
    """

    # First check the privilege on this collection
    # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
    if not servertoserver:
        try:
            yield calresource.checkPrivileges(request,
                                              (caldavxml.ReadFreeBusy(), ),
                                              principal=organizerPrincipal)
        except AccessDeniedError:
            returnValue(matchtotal)

    # May need organizer principal
    organizer_principal = (yield calresource.principalForCalendarUserAddress(
        organizer)) if organizer else None
    organizer_uid = organizer_principal.principalUID(
    ) if organizer_principal else ""

    # Free busy is per-user
    userPrincipal = (yield calresource.resourceOwnerPrincipal(request))
    if userPrincipal:
        useruid = userPrincipal.principalUID()
    else:
        useruid = ""

    # Get the timezone property from the collection.
    has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request))
    if has_prop:
        tz = (yield calresource.readProperty(CalendarTimeZone(), request))
    else:
        tz = None

    # Look for possible extended free busy information
    rich_options = {
        "organizer": False,
        "delegate": False,
        "resource": False,
    }
    do_event_details = False
    if event_details is not None and organizer_principal is not None and userPrincipal is not None:

        # Check if organizer is attendee
        if organizer_principal == userPrincipal:
            do_event_details = True
            rich_options["organizer"] = True

        # Check if organizer is a delegate of attendee
        proxy = (yield organizer_principal.isProxyFor(userPrincipal))
        if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
            do_event_details = True
            rich_options["delegate"] = True

        # Check if attendee is room or resource
        if config.Scheduling.Options.RoomResourceRichFreeBusy and userPrincipal.getCUType(
        ) in (
                "RESOURCE",
                "ROOM",
        ):
            do_event_details = True
            rich_options["resource"] = True

    # Try cache
    resources = (yield FBCacheEntry.getCacheEntry(
        calresource, useruid,
        timerange)) if config.EnableFreeBusyCache else None

    if resources is None:

        caching = False
        if config.EnableFreeBusyCache:
            # Log extended item
            if not hasattr(request, "extendedLogItems"):
                request.extendedLogItems = {}
            request.extendedLogItems[
                "fb-uncached"] = request.extendedLogItems.get(
                    "fb-uncached", 0) + 1

            # We want to cache a large range of time based on the current date
            cache_start = normalizeToUTC(DateTime.getToday() + Duration(
                days=0 - config.FreeBusyCacheDaysBack))
            cache_end = normalizeToUTC(DateTime.getToday() + Duration(
                days=config.FreeBusyCacheDaysForward))

            # If the requested timerange would fit in our allowed cache range, trigger the cache creation
            if compareDateTime(timerange.start,
                               cache_start) >= 0 and compareDateTime(
                                   timerange.end, cache_end) <= 0:
                cache_timerange = TimeRange(start=cache_start.getText(),
                                            end=cache_end.getText())
                caching = True

        #
        # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
        # We then take those results and merge them into one VFREEBUSY component
        # with appropriate FREEBUSY properties, and return that single item as iCal data.
        #

        # Create fake filter element to match time-range
        filter = caldavxml.Filter(
            caldavxml.ComponentFilter(
                caldavxml.ComponentFilter(
                    cache_timerange if caching else timerange,
                    name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
                ),
                name="VCALENDAR",
            ))
        filter = Filter(filter)
        tzinfo = filter.settimezone(tz)

        try:
            resources = yield calresource.search(filter,
                                                 useruid=useruid,
                                                 fbtype=True)
            if caching:
                yield FBCacheEntry.makeCacheEntry(calresource, useruid,
                                                  cache_timerange, resources)
        except IndexedSearchException:
            raise HTTPError(
                StatusResponse(responsecode.INTERNAL_SERVER_ERROR,
                               "Failed freebusy query"))

    else:
        # Log extended item
        if not hasattr(request, "extendedLogItems"):
            request.extendedLogItems = {}
        request.extendedLogItems["fb-cached"] = request.extendedLogItems.get(
            "fb-cached", 0) + 1

        # Determine appropriate timezone (UTC is the default)
        tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)

    # We care about separate instances for VEVENTs only
    aggregated_resources = {}
    for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
        if transp == 'T' and fbtype != '?':
            fbtype = 'F'
        aggregated_resources.setdefault((
            name,
            uid,
            type,
            test_organizer,
        ), []).append((
            float,
            start,
            end,
            fbtype,
        ))

    for key in aggregated_resources.iterkeys():

        name, uid, type, test_organizer = key

        # Short-cut - if an fbtype exists we can use that
        if type == "VEVENT" and aggregated_resources[key][0][3] != '?':

            matchedResource = False

            # Look at each instance
            for float, start, end, fbtype in aggregated_resources[key]:
                # Ignore free time or unknown
                if fbtype in ('F', '?'):
                    continue

                # Ignore ones of this UID
                if excludeuid:
                    # See if we have a UID match
                    if (excludeuid == uid):
                        test_principal = (
                            yield calresource.principalForCalendarUserAddress(
                                test_organizer)) if test_organizer else None
                        test_uid = test_principal.principalUID(
                        ) if test_principal else ""

                        # Check that ORGANIZER's match (security requirement)
                        if (organizer is None) or (organizer_uid == test_uid):
                            continue
                        # Check for no ORGANIZER and check by same calendar user
                        elif (test_uid == "") and same_calendar_user:
                            continue

                # Apply a timezone to any floating times
                fbstart = parseSQLTimestampToPyCalendar(start)
                if float == 'Y':
                    fbstart.setTimezone(tzinfo)
                else:
                    fbstart.setTimezone(Timezone(utc=True))
                fbend = parseSQLTimestampToPyCalendar(end)
                if float == 'Y':
                    fbend.setTimezone(tzinfo)
                else:
                    fbend.setTimezone(Timezone(utc=True))

                # Clip instance to time range
                clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart),
                                     Period(timerange.start, timerange.end))

                # Double check for overlap
                if clipped:
                    matchedResource = True
                    fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped)

            if matchedResource:
                # Check size of results is within limit
                matchtotal += 1
                if matchtotal > max_number_of_matches:
                    raise NumberOfMatchesWithinLimits(max_number_of_matches)

                # Add extended details
                if do_event_details:
                    child = (yield
                             request.locateChildResource(calresource, name))
                    calendar = (yield child.iCalendarForUser(request))
                    _addEventDetails(calendar, event_details, rich_options,
                                     timerange, tzinfo)

        else:
            child = (yield request.locateChildResource(calresource, name))
            calendar = (yield child.iCalendarForUser(request))

            # The calendar may come back as None if the resource is being changed, or was deleted
            # between our initial index query and getting here. For now we will ignore this error, but in
            # the longer term we need to implement some form of locking, perhaps.
            if calendar is None:
                log.error(
                    "Calendar %s is missing from calendar collection %r" %
                    (name, calresource))
                continue

            # Ignore ones of this UID
            if excludeuid:
                # See if we have a UID match
                if (excludeuid == uid):
                    test_organizer = calendar.getOrganizer()
                    test_principal = (
                        yield calresource.principalForCalendarUserAddress(
                            test_organizer)) if test_organizer else None
                    test_uid = test_principal.principalUID(
                    ) if test_principal else ""

                    # Check that ORGANIZER's match (security requirement)
                    if (organizer is None) or (organizer_uid == test_uid):
                        continue
                    # Check for no ORGANIZER and check by same calendar user
                    elif (test_organizer is None) and same_calendar_user:
                        continue

            if filter.match(calendar, None):
                # Check size of results is within limit
                matchtotal += 1
                if matchtotal > max_number_of_matches:
                    raise NumberOfMatchesWithinLimits(max_number_of_matches)

                if calendar.mainType() == "VEVENT":
                    processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
                elif calendar.mainType() == "VFREEBUSY":
                    processFreeBusyFreeBusy(calendar, fbinfo, timerange)
                elif calendar.mainType() == "VAVAILABILITY":
                    processAvailabilityFreeBusy(calendar, fbinfo, timerange)
                else:
                    assert "Free-busy query returned unwanted component: %s in %r", (
                        name,
                        calresource,
                    )

                # Add extended details
                if calendar.mainType() == "VEVENT" and do_event_details:
                    child = (yield
                             request.locateChildResource(calresource, name))
                    calendar = (yield child.iCalendarForUser(request))
                    _addEventDetails(calendar, event_details, rich_options,
                                     timerange, tzinfo)

    returnValue(matchtotal)
예제 #34
0
class VAlarm(Component):

    sActionMap = {
        definitions.cICalProperty_ACTION_AUDIO:
        definitions.eAction_VAlarm_Audio,
        definitions.cICalProperty_ACTION_DISPLAY:
        definitions.eAction_VAlarm_Display,
        definitions.cICalProperty_ACTION_EMAIL:
        definitions.eAction_VAlarm_Email,
        definitions.cICalProperty_ACTION_PROCEDURE:
        definitions.eAction_VAlarm_Procedure,
        definitions.cICalProperty_ACTION_URI: definitions.eAction_VAlarm_URI,
        definitions.cICalProperty_ACTION_NONE: definitions.eAction_VAlarm_None,
    }

    sActionValueMap = {
        definitions.eAction_VAlarm_Audio:
        definitions.cICalProperty_ACTION_AUDIO,
        definitions.eAction_VAlarm_Display:
        definitions.cICalProperty_ACTION_DISPLAY,
        definitions.eAction_VAlarm_Email:
        definitions.cICalProperty_ACTION_EMAIL,
        definitions.eAction_VAlarm_Procedure:
        definitions.cICalProperty_ACTION_PROCEDURE,
        definitions.eAction_VAlarm_URI: definitions.cICalProperty_ACTION_URI,
        definitions.eAction_VAlarm_None: definitions.cICalProperty_ACTION_NONE,
    }

    # Classes for each action encapsulating action-specific data
    class VAlarmAction(object):

        propertyCardinality_1 = ()
        propertyCardinality_1_Fix_Empty = ()
        propertyCardinality_0_1 = ()
        propertyCardinality_1_More = ()

        def __init__(self, type):
            self.mType = type

        def duplicate(self):
            return VAlarm.VAlarmAction(self.mType)

        def load(self, valarm):
            pass

        def add(self, valarm):
            pass

        def remove(self, valarm):
            pass

        def getType(self):
            return self.mType

    class VAlarmAudio(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
            definitions.cICalProperty_TRIGGER,
        )

        propertyCardinality_0_1 = (
            definitions.cICalProperty_DURATION,
            definitions.cICalProperty_REPEAT,
            definitions.cICalProperty_ATTACH,
            definitions.cICalProperty_ACKNOWLEDGED,
        )

        def __init__(self, speak=None):
            super(VAlarm.VAlarmAudio,
                  self).__init__(type=definitions.eAction_VAlarm_Audio)
            self.mSpeakText = speak

        def duplicate(self):
            return VAlarm.VAlarmAudio(self.mSpeakText)

        def load(self, valarm):
            # Get properties
            self.mSpeakText = valarm.loadValueString(
                definitions.cICalProperty_ACTION_X_SPEAKTEXT)

        def add(self, valarm):
            # Delete existing then add
            self.remove(valarm)

            prop = Property(definitions.cICalProperty_ACTION_X_SPEAKTEXT,
                            self.mSpeakText)
            valarm.addProperty(prop)

        def remove(self, valarm):
            valarm.removeProperties(
                definitions.cICalProperty_ACTION_X_SPEAKTEXT)

        def isSpeakText(self):
            return len(self.mSpeakText) != 0

        def getSpeakText(self):
            return self.mSpeakText

    class VAlarmDisplay(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
            definitions.cICalProperty_TRIGGER,
        )

        propertyCardinality_1_Fix_Empty = (
            definitions.cICalProperty_DESCRIPTION, )

        propertyCardinality_0_1 = (
            definitions.cICalProperty_DURATION,
            definitions.cICalProperty_REPEAT,
            definitions.cICalProperty_ACKNOWLEDGED,
        )

        def __init__(self, description=None):
            super(VAlarm.VAlarmDisplay,
                  self).__init__(type=definitions.eAction_VAlarm_Display)
            self.mDescription = description

        def duplicate(self):
            return VAlarm.VAlarmDisplay(self.mDescription)

        def load(self, valarm):
            # Get properties
            self.mDescription = valarm.loadValueString(
                definitions.cICalProperty_DESCRIPTION)

        def add(self, valarm):
            # Delete existing then add
            self.remove(valarm)

            prop = Property(definitions.cICalProperty_DESCRIPTION,
                            self.mDescription)
            valarm.addProperty(prop)

        def remove(self, valarm):
            valarm.removeProperties(definitions.cICalProperty_DESCRIPTION)

        def getDescription(self):
            return self.mDescription

    class VAlarmEmail(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
            definitions.cICalProperty_TRIGGER,
        )

        propertyCardinality_1_Fix_Empty = (
            definitions.cICalProperty_DESCRIPTION,
            definitions.cICalProperty_SUMMARY,
        )

        propertyCardinality_0_1 = (
            definitions.cICalProperty_DURATION,
            definitions.cICalProperty_REPEAT,
            definitions.cICalProperty_ACKNOWLEDGED,
        )

        propertyCardinality_1_More = (definitions.cICalProperty_ATTENDEE, )

        def __init__(self, description=None, summary=None, attendees=None):
            super(VAlarm.VAlarmEmail,
                  self).__init__(type=definitions.eAction_VAlarm_Email)
            self.mDescription = description
            self.mSummary = summary
            self.mAttendees = attendees

        def duplicate(self):
            return VAlarm.VAlarmEmail(self.mDescription, self.mSummary,
                                      self.mAttendees)

        def load(self, valarm):
            # Get properties
            self.mDescription = valarm.loadValueString(
                definitions.cICalProperty_DESCRIPTION)
            self.mSummary = valarm.loadValueString(
                definitions.cICalProperty_SUMMARY)

            self.mAttendees = []
            if valarm.hasProperty(definitions.cICalProperty_ATTENDEE):
                # Get each attendee
                range = valarm.getProperties().get(
                    definitions.cICalProperty_ATTENDEE, ())
                for iter in range:
                    # Get the attendee value
                    attendee = iter.getCalAddressValue()
                    if attendee is not None:
                        self.mAttendees.append(attendee.getValue())

        def add(self, valarm):
            # Delete existing then add
            self.remove(valarm)

            prop = Property(definitions.cICalProperty_DESCRIPTION,
                            self.mDescription)
            valarm.addProperty(prop)

            prop = Property(definitions.cICalProperty_SUMMARY, self.mSummary)
            valarm.addProperty(prop)

            for iter in self.mAttendees:
                prop = Property(definitions.cICalProperty_ATTENDEE, iter,
                                Value.VALUETYPE_CALADDRESS)
                valarm.addProperty(prop)

        def remove(self, valarm):
            valarm.removeProperties(definitions.cICalProperty_DESCRIPTION)
            valarm.removeProperties(definitions.cICalProperty_SUMMARY)
            valarm.removeProperties(definitions.cICalProperty_ATTENDEE)

        def getDescription(self):
            return self.mDescription

        def getSummary(self):
            return self.mSummary

        def getAttendees(self):
            return self.mAttendees

    class VAlarmUnknown(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
            definitions.cICalProperty_TRIGGER,
        )

        propertyCardinality_0_1 = (
            definitions.cICalProperty_DURATION,
            definitions.cICalProperty_REPEAT,
            definitions.cICalProperty_ACKNOWLEDGED,
        )

        def __init__(self):
            super(VAlarm.VAlarmUnknown,
                  self).__init__(type=definitions.eAction_VAlarm_Unknown)

        def duplicate(self):
            return VAlarm.VAlarmUnknown()

    class VAlarmURI(VAlarmAction):

        propertyCardinality_1 = (
            definitions.cICalProperty_ACTION,
            definitions.cICalProperty_TRIGGER,
            definitions.cICalProperty_URL,
        )

        propertyCardinality_0_1 = (
            definitions.cICalProperty_DURATION,
            definitions.cICalProperty_REPEAT,
            definitions.cICalProperty_ACKNOWLEDGED,
        )

        def __init__(self, uri=None):
            super(VAlarm.VAlarmURI,
                  self).__init__(type=definitions.eAction_VAlarm_URI)
            self.mURI = uri

        def duplicate(self):
            return VAlarm.VAlarmURI(self.mURI)

        def load(self, valarm):
            # Get properties
            self.mURI = valarm.loadValueString(definitions.cICalProperty_URL)

        def add(self, valarm):
            # Delete existing then add
            self.remove(valarm)

            prop = Property(definitions.cICalProperty_URL, self.mURI)
            valarm.addProperty(prop)

        def remove(self, valarm):
            valarm.removeProperties(definitions.cICalProperty_URL)

        def getURI(self):
            return self.mURI

    class VAlarmNone(VAlarmAction):

        propertyCardinality_1 = (definitions.cICalProperty_ACTION, )

        def __init__(self):
            super(VAlarm.VAlarmNone,
                  self).__init__(type=definitions.eAction_VAlarm_None)

        def duplicate(self):
            return VAlarm.VAlarmNone()

    def getMimeComponentName(self):
        # Cannot be sent as a separate MIME object
        return None

    sActionToAlarmMap = {
        definitions.eAction_VAlarm_Audio: VAlarmAudio,
        definitions.eAction_VAlarm_Display: VAlarmDisplay,
        definitions.eAction_VAlarm_Email: VAlarmEmail,
        definitions.eAction_VAlarm_URI: VAlarmURI,
        definitions.eAction_VAlarm_None: VAlarmNone,
    }

    propertyValueChecks = ICALENDAR_VALUE_CHECKS

    def __init__(self, parent=None):

        super(VAlarm, self).__init__(parent=parent)

        self.mAction = definitions.eAction_VAlarm_Display
        self.mTriggerAbsolute = False
        self.mTriggerOnStart = True
        self.mTriggerOn = DateTime()

        # Set duration default to 1 hour
        self.mTriggerBy = Duration()
        self.mTriggerBy.setDuration(60 * 60)

        # Does not repeat by default
        self.mRepeats = 0
        self.mRepeatInterval = Duration()
        self.mRepeatInterval.setDuration(5 * 60)  # Five minutes

        # Status
        self.mStatusInit = False
        self.mAlarmStatus = definitions.eAlarm_Status_Pending
        self.mLastTrigger = DateTime()
        self.mNextTrigger = DateTime()
        self.mDoneCount = 0

        # Create action data
        self.mActionData = VAlarm.VAlarmDisplay("")

    def duplicate(self, parent=None):
        other = super(VAlarm, self).duplicate(parent=parent)
        other.mAction = self.mAction
        other.mTriggerAbsolute = self.mTriggerAbsolute
        other.mTriggerOn = self.mTriggerOn.duplicate()
        other.mTriggerBy = self.mTriggerBy.duplicate()
        other.mTriggerOnStart = self.mTriggerOnStart

        other.mRepeats = self.mRepeats
        other.mRepeatInterval = self.mRepeatInterval.duplicate()

        other.mAlarmStatus = self.mAlarmStatus
        if self.mLastTrigger is not None:
            other.mLastTrigger = self.mLastTrigger.duplicate()
        if self.mNextTrigger is not None:
            other.mNextTrigger = self.mNextTrigger.duplicate()
        other.mDoneCount = self.mDoneCount

        other.mActionData = self.mActionData.duplicate()
        return other

    def getType(self):
        return definitions.cICalComponent_VALARM

    def getAction(self):
        return self.mAction

    def getActionData(self):
        return self.mActionData

    def isTriggerAbsolute(self):
        return self.mTriggerAbsolute

    def getTriggerOn(self):
        return self.mTriggerOn

    def getTriggerDuration(self):
        return self.mTriggerBy

    def isTriggerOnStart(self):
        return self.mTriggerOnStart

    def getRepeats(self):
        return self.mRepeats

    def getInterval(self):
        return self.mRepeatInterval

    def added(self):
        # Added to calendar so add to calendar notifier
        # calstore::CCalendarNotifier::sCalendarNotifier.AddAlarm(this)

        # Do inherited
        super(VAlarm, self).added()

    def removed(self):
        # Removed from calendar so add to calendar notifier
        # calstore::CCalendarNotifier::sCalendarNotifier.RemoveAlarm(this)

        # Do inherited
        super(VAlarm, self).removed()

    def changed(self):
        # Always force recalc of trigger status
        self.mStatusInit = False

        # Changed in calendar so change in calendar notifier
        # calstore::CCalendarNotifier::sCalendarNotifier.ChangedAlarm(this)

        # Do not do inherited as this is always a sub-component and we do not
        # do top-level component changes
        # super.changed()

    def finalise(self):
        # Do inherited
        super(VAlarm, self).finalise()

        # Get the ACTION
        temp = self.loadValueString(definitions.cICalProperty_ACTION)
        if temp is not None:
            self.mAction = VAlarm.sActionMap.get(
                temp, definitions.eAction_VAlarm_Unknown)
            self.loadAction()

        # Get the trigger
        if self.hasProperty(definitions.cICalProperty_TRIGGER):
            # Determine the type of the value
            temp1 = self.loadValueDateTime(definitions.cICalProperty_TRIGGER)
            temp2 = self.loadValueDuration(definitions.cICalProperty_TRIGGER)
            if temp1 is not None:
                self.mTriggerAbsolute = True
                self.mTriggerOn = temp1
            elif temp2 is not None:
                self.mTriggerAbsolute = False
                self.mTriggerBy = temp2

                # Get the property
                prop = self.findFirstProperty(
                    definitions.cICalProperty_TRIGGER)

                # Look for RELATED parameter
                if prop.hasParameter(definitions.cICalParameter_RELATED):
                    temp = prop.getParameterValue(
                        definitions.cICalParameter_RELATED)
                    if temp == definitions.cICalParameter_RELATED_START:
                        self.mTriggerOnStart = True
                    elif temp == definitions.cICalParameter_RELATED_END:
                        self.mTriggerOnStart = False
                else:
                    self.mTriggerOnStart = True

        # Get repeat & interval
        temp = self.loadValueInteger(definitions.cICalProperty_REPEAT)
        if temp is not None:
            self.mRepeats = temp
        temp = self.loadValueDuration(definitions.cICalProperty_DURATION)
        if temp is not None:
            self.mRepeatInterval = temp

        # Set a map key for sorting
        self.mMapKey = "%s:%s" % (
            self.mAction,
            self.mTriggerOn if self.mTriggerAbsolute else self.mTriggerBy,
        )

        # Alarm status - private to Mulberry
        status = self.loadValueString(
            definitions.cICalProperty_ALARM_X_ALARMSTATUS)
        if status is not None:
            if status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING:
                self.mAlarmStatus = definitions.eAlarm_Status_Pending
            elif status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED:
                self.mAlarmStatus = definitions.eAlarm_Status_Completed
            elif status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED:
                self.mAlarmStatus = definitions.eAlarm_Status_Disabled
            else:
                self.mAlarmStatus = definitions.eAlarm_Status_Pending

        # Last trigger time - private to Mulberry
        temp = self.loadValueDateTime(
            definitions.cICalProperty_ALARM_X_LASTTRIGGER)
        if temp is not None:
            self.mLastTrigger = temp

    def validate(self, doFix=False):
        """
        Validate the data in this component and optionally fix any problems, else raise. If
        loggedProblems is not None it must be a C{list} and problem descriptions are appended
        to that.
        """

        # Validate using action specific constraints
        self.propertyCardinality_1 = self.mActionData.propertyCardinality_1
        self.propertyCardinality_1_Fix_Empty = self.mActionData.propertyCardinality_1_Fix_Empty
        self.propertyCardinality_0_1 = self.mActionData.propertyCardinality_0_1
        self.propertyCardinality_1_More = self.mActionData.propertyCardinality_1_More

        fixed, unfixed = super(VAlarm, self).validate(doFix)

        # Extra constraint: both DURATION and REPEAT must be present togethe
        if self.hasProperty(
                definitions.cICalProperty_DURATION) ^ self.hasProperty(
                    definitions.cICalProperty_REPEAT):
            # Cannot fix this
            logProblem = "[%s] Properties must be present together: %s, %s" % (
                self.getType(),
                definitions.cICalProperty_DURATION,
                definitions.cICalProperty_REPEAT,
            )
            unfixed.append(logProblem)

        return fixed, unfixed

    def editStatus(self, status):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_ALARM_X_ALARMSTATUS)

        # Updated cached values
        self.mAlarmStatus = status

        # Add new
        status_txt = ""
        if self.mAlarmStatus == definitions.eAlarm_Status_Pending:
            status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING
        elif self.mAlarmStatus == definitions.eAlarm_Status_Completed:
            status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED
        elif self.mAlarmStatus == definitions.eAlarm_Status_Disabled:
            status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED
        self.addProperty(
            Property(definitions.cICalProperty_ALARM_X_ALARMSTATUS,
                     status_txt))

    def editAction(self, action, data):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_ACTION)
        self.mActionData.remove(self)
        self.mActionData = None

        # Updated cached values
        self.mAction = action
        self.mActionData = data

        # Add new properties to alarm
        action_txt = VAlarm.sActionValueMap.get(
            self.mAction, definitions.cICalProperty_ACTION_PROCEDURE)

        prop = Property(definitions.cICalProperty_ACTION, action_txt)
        self.addProperty(prop)

        self.mActionData.add(self)

    def editTriggerOn(self, dt):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_TRIGGER)

        # Updated cached values
        self.mTriggerAbsolute = True
        self.mTriggerOn = dt

        # Add new
        prop = Property(definitions.cICalProperty_TRIGGER, dt)
        self.addProperty(prop)

    def editTriggerBy(self, duration, trigger_start):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_TRIGGER)

        # Updated cached values
        self.mTriggerAbsolute = False
        self.mTriggerBy = duration
        self.mTriggerOnStart = trigger_start

        # Add new (with parameter)
        prop = Property(definitions.cICalProperty_TRIGGER, duration)
        attr = Parameter(
            definitions.cICalParameter_RELATED,
            (definitions.cICalParameter_RELATED_START,
             definitions.cICalParameter_RELATED_END)[not trigger_start])
        prop.addParameter(attr)
        self.addProperty(prop)

    def editRepeats(self, repeat, interval):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_REPEAT)
        self.removeProperties(definitions.cICalProperty_DURATION)

        # Updated cached values
        self.mRepeats = repeat
        self.mRepeatInterval = interval

        # Add new
        if self.mRepeats > 0:
            self.addProperty(Property(definitions.cICalProperty_REPEAT,
                                      repeat))
            self.addProperty(
                Property(definitions.cICalProperty_DURATION, interval))

    def getAlarmStatus(self):
        return self.mAlarmStatus

    def getNextTrigger(self, dt):
        if not self.mStatusInit:
            self.initNextTrigger()
        dt.copy(self.mNextTrigger)

    def alarmTriggered(self, dt):
        # Remove existing
        self.removeProperties(definitions.cICalProperty_ALARM_X_LASTTRIGGER)
        self.removeProperties(definitions.cICalProperty_ALARM_X_ALARMSTATUS)

        # Updated cached values
        self.mLastTrigger.copy(dt)

        if self.mDoneCount < self.mRepeats:
            self.mNextTrigger = self.mLastTrigger + self.mRepeatInterval
            dt.copy(self.mNextTrigger)
            self.mDoneCount += 1
            self.mAlarmStatus = definitions.eAlarm_Status_Pending
        else:
            self.mAlarmStatus = definitions.eAlarm_Status_Completed

        # Add new
        self.addProperty(
            Property(definitions.cICalProperty_ALARM_X_LASTTRIGGER, dt))
        status = ""
        if self.mAlarmStatus == definitions.eAlarm_Status_Pending:
            status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING
        elif self.mAlarmStatus == definitions.eAlarm_Status_Completed:
            status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED
        elif self.mAlarmStatus == definitions.eAlarm_Status_Disabled:
            status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED
        self.addProperty(
            Property(definitions.cICalProperty_ALARM_X_ALARMSTATUS, status))

        # Now update dt to the next alarm time
        return self.mAlarmStatus == definitions.eAlarm_Status_Pending

    def loadAction(self):
        # Delete current one
        self.mActionData = None
        self.mActionData = VAlarm.sActionToAlarmMap.get(
            self.mAction, VAlarm.VAlarmUnknown)()
        self.mActionData.load(self)

    def initNextTrigger(self):
        # Do not bother if its completed
        if self.mAlarmStatus == definitions.eAlarm_Status_Completed:
            return
        self.mStatusInit = True

        # Look for trigger immediately preceeding or equal to utc now
        nowutc = DateTime.getNowUTC()

        # Init done counter
        self.mDoneCount = 0

        # Determine the first trigger
        trigger = DateTime()
        self.getFirstTrigger(trigger)

        while self.mDoneCount < self.mRepeats:
            # See if next trigger is later than now
            next_trigger = trigger + self.mRepeatInterval
            if next_trigger > nowutc:
                break
            self.mDoneCount += 1
            trigger = next_trigger

        # Check for completion
        if trigger == self.mLastTrigger or (
                nowutc - trigger).getTotalSeconds() > 24 * 60 * 60:
            if self.mDoneCount == self.mRepeats:
                self.mAlarmStatus = definitions.eAlarm_Status_Completed
                return
            else:
                trigger = trigger + self.mRepeatInterval
                self.mDoneCount += 1

        self.mNextTrigger = trigger

    def getFirstTrigger(self, dt):
        # If absolute trigger, use that
        if self.isTriggerAbsolute():
            # Get the trigger on
            dt.copy(self.getTriggerOn())
        else:
            # Get the parent embedder class (must be CICalendarComponentRecur type)
            owner = self.getEmbedder()
            if owner is not None:
                # Determine time at which alarm will trigger
                trigger = (owner.getStart(),
                           owner.getEnd())[not self.isTriggerOnStart()]

                # Offset by duration
                dt.copy(trigger + self.getTriggerDuration())