def _addEvent(self): if not self._client.started: return succeed(None) calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT") while calendars: calendar = self.random.choice(calendars) calendars.remove(calendar) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def _initEvent(self): if not self._client.started: return succeed(None) # If it already exists, don't re-create calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0] if calendar.events: events = [event for event in calendar.events.values() if event.url.endswith("event_to_update.ics")] if events: return succeed(None) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) href = '%s%s' % (calendar.url, "event_to_update.ics") d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def _invite(self): """ Try to add a new event, or perhaps remove an existing attendee from an event. @return: C{None} if there are no events to play with, otherwise a L{Deferred} which fires when the attendee change has been made. """ if not self._client.started: return succeed(None) # Find calendars which are eligible for invites calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT") while calendars: # Pick one at random from which to try to create an event # to modify. calendar = self.random.choice(calendars) calendars.remove(calendar) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) vevent.addProperty(self._client._makeSelfOrganizer()) vevent.addProperty(self._client._makeSelfAttendee()) attendees = list(vevent.properties('ATTENDEE')) for _ignore in range(int(self._inviteeCountDistribution.sample())): try: self._addAttendee(vevent, attendees) except CannotAddAttendee: self._failedOperation("invite", "Cannot add attendee") return succeed(None) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addInvite(href, vcalendar) return self._newOperation("invite", d)
def getEventDetails(event): detail = {} nowPyDT = DateTime.getNowUTC() nowDT = datetime.datetime.utcnow() oneYearInFuture = DateTime.getNowUTC() oneYearInFuture.offsetDay(365) component = yield event.component() mainSummary = component.mainComponent().propertyValue("SUMMARY", u"<no title>") whenTrashed = event.whenTrashed() ago = nowDT - whenTrashed detail["summary"] = mainSummary detail["whenTrashed"] = agoString(ago) detail["recoveryID"] = event._resourceID if component.isRecurring(): detail["recurring"] = True detail["instances"] = [] instances = component.cacheExpandedTimeRanges(oneYearInFuture) instances = sorted(instances.instances.values(), key=lambda x: x.start) limit = 3 count = 0 for instance in instances: if instance.start >= nowPyDT: summary = instance.component.propertyValue("SUMMARY", u"<no title>") location = locationString(instance.component) tzid = instance.component.getProperty("DTSTART").parameterValue("TZID", None) dtstart = instance.start if tzid is not None: timezone = Timezone(tzid=tzid) dtstart.adjustTimezone(timezone) detail["instances"].append( { "summary": summary, "starttime": dtstart.getLocaleDateTime(DateTime.FULLDATE, False, True, dtstart.getTimezoneID()), "location": location, } ) count += 1 limit -= 1 if limit == 0: break else: detail["recurring"] = False dtstart = component.mainComponent().propertyValue("DTSTART") detail["starttime"] = dtstart.getLocaleDateTime(DateTime.FULLDATE, False, True, dtstart.getTimezoneID()) detail["location"] = locationString(component.mainComponent()) returnValue(detail)
def printEventDetails(event): nowPyDT = DateTime.getNowUTC() nowDT = datetime.datetime.utcnow() oneYearInFuture = DateTime.getNowUTC() oneYearInFuture.offsetDay(365) component = yield event.component() mainSummary = component.mainComponent().propertyValue("SUMMARY", u"<no title>") whenTrashed = event.whenTrashed() ago = nowDT - whenTrashed print(" Trashed {}:".format(agoString(ago))) if component.isRecurring(): print( " \"{}\" (repeating) Recovery ID = {}".format( mainSummary, event._resourceID ) ) print(" ...upcoming instances:") instances = component.cacheExpandedTimeRanges(oneYearInFuture) instances = sorted(instances.instances.values(), key=lambda x: x.start) limit = 3 count = 0 for instance in instances: if instance.start >= nowPyDT: summary = instance.component.propertyValue("SUMMARY", u"<no title>") location = locationString(instance.component) tzid = instance.component.getProperty("DTSTART").parameterValue("TZID", None) dtstart = instance.start if tzid is not None: timezone = Timezone(tzid=tzid) dtstart.adjustTimezone(timezone) print(" \"{}\" {} {}".format(summary, startString(dtstart), location)) count += 1 limit -= 1 if limit == 0: break if not count: print(" (none)") else: print( " \"{}\" (non-repeating) Recovery ID = {}".format( mainSummary, event._resourceID ) ) dtstart = component.mainComponent().propertyValue("DTSTART") location = locationString(component.mainComponent()) print(" {} {}".format(startString(dtstart), location))
def __init__(self, threshold, past): """ @param threshold: the size in bytes that will trigger a split @type threshold: C{int} @param past: number of days in the past where the split will occur @type past: C{int} """ self.threshold = threshold self.past = DateTime.getNowUTC() self.past.setHHMMSS(0, 0, 0) self.past.offsetDay(-past) self.now = DateTime.getNowUTC() self.now.setHHMMSS(0, 0, 0) self.now.offsetDay(-1)
def _updateEvent(self): """ Try to add a new attendee to an event, or perhaps remove an existing attendee from an event. @return: C{None} if there are no events to play with, otherwise a L{Deferred} which fires when the attendee change has been made. """ if not self._client.started: return succeed(None) # If it does not exist, try to create it calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0] if not calendar.events: return self._initEvent() events = [event for event in calendar.events.values() if event.url.endswith("event_to_update.ics")] if not events: return self._initEvent() event = events[0] # Add/update the ACKNOWLEDGED property component = event.component.mainComponent() component.replaceProperty(Property("ACKNOWLEDGED", DateTime.getNowUTC())) d = self._client.changeEvent(event.url) return self._newOperation("update", d)
def 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 createNewDatabase(self): """ Create a new DB xml file from scratch by scanning zoneinfo. """ self.dtstamp = DateTime.getNowUTC().getXMLText() self._scanTZs("") self._dumpTZs()
def doRequest(self): """ Execute the actual HTTP request. """ now = DateTime.getNowUTC() href = joinURL(self.sessions[0].calendarHref, "put.ics") self.sessions[0].writeData(URL(path=href), ICAL % (now.getYear() + 1,), "text/calendar")
def _purgeUID(self, uid): if self.when is None: self.when = DateTime.getNowUTC() # Does the record exist? record = self.directory.recordWithUID(uid) if record is None: # The user has already been removed from the directory service. We # need to fashion a temporary, fake record # FIXME: probably want a more elegant way to accomplish this, # since it requires the aggregate directory to examine these first: record = DirectoryRecord(self.directory, "users", uid, shortNames=(uid,), enabledForCalendaring=True) self.directory._tmpRecords["shortNames"][uid] = record self.directory._tmpRecords["uids"][uid] = record # Override augments settings for this record record.enabled = True record.enabledForCalendaring = True record.enabledForAddressBooks = True cua = "urn:uuid:%s" % (uid,) principalCollection = self.directory.principalCollection principal = principalCollection.principalForRecord(record) # See if calendar home is provisioned txn = self.store.newTransaction() storeCalHome = (yield txn.calendarHomeWithUID(uid)) calHomeProvisioned = storeCalHome is not None # If in "completely" mode, unshare collections, remove notifications if calHomeProvisioned and self.completely: yield self._cleanHome(txn, storeCalHome) yield txn.commit() count = 0 assignments = [] if calHomeProvisioned: count = (yield self._cancelEvents(txn, uid, cua)) # Remove empty calendar collections (and calendar home if no more # calendars) yield self._removeCalendarHome(uid) # Remove VCards count += (yield self._removeAddressbookHome(uid)) if self.proxies and not self.dryrun: if self.verbose: print("Deleting any proxy assignments") assignments = (yield self._purgeProxyAssignments(principal)) returnValue((count, assignments))
def updateDatabase(self): """ Update existing DB info by comparing md5's. """ self.dtstamp = DateTime.getNowUTC().getXMLText() self.changeCount = 0 self.changed = set() self._scanTZs("", checkIfChanged=True) if self.changeCount: self._dumpTZs()
def prepare(self): """ Do some setup prior to the real request. """ # Add resources to create required number of changes self.start = DateTime.getNowUTC() self.start.setHHMMSS(12, 0, 0) self.end = self.start.duplicate() self.end.offsetHours(1) for i in range(self.count): href = joinURL(self.sessions[0].calendarHref, "tr-query-%d.ics" % (i + 1,)) self.sessions[0].writeData(URL(path=href), ICAL % (self.start.getText(), i + 1,), "text/calendar")
def buildFreeBusyResult(fbinfo, timerange, organizer=None, attendee=None, uid=None, method=None, event_details=None): """ Generate a VCALENDAR object containing a single VFREEBUSY that is the aggregate of the free busy info passed in. @param fbinfo: the array of busy periods to use. @param timerange: the L{TimeRange} for the query. @param organizer: the L{Property} for the Organizer of the free busy request, or None. @param attendee: the L{Property} for the Attendee responding to the free busy request, or None. @param uid: the UID value from the free busy request. @param method: the METHOD property value to insert. @param event_details: VEVENT components to add. @return: the L{Component} containing the calendar data. """ # Merge overlapping time ranges in each fb info section normalizePeriodList(fbinfo[0]) normalizePeriodList(fbinfo[1]) normalizePeriodList(fbinfo[2]) # Now build a new calendar object with the free busy info we have fbcalendar = Component("VCALENDAR") fbcalendar.addProperty(Property("VERSION", "2.0")) fbcalendar.addProperty(Property("PRODID", iCalendarProductID)) if method: fbcalendar.addProperty(Property("METHOD", method)) fb = Component("VFREEBUSY") fbcalendar.addComponent(fb) if organizer is not None: fb.addProperty(organizer) if attendee is not None: fb.addProperty(attendee) fb.addProperty(Property("DTSTART", timerange.start)) fb.addProperty(Property("DTEND", timerange.end)) fb.addProperty(Property("DTSTAMP", DateTime.getNowUTC())) if len(fbinfo[0]) != 0: fb.addProperty(Property("FREEBUSY", fbinfo[0], {"FBTYPE": "BUSY"})) if len(fbinfo[1]) != 0: fb.addProperty(Property("FREEBUSY", fbinfo[1], {"FBTYPE": "BUSY-TENTATIVE"})) if len(fbinfo[2]) != 0: fb.addProperty(Property("FREEBUSY", fbinfo[2], {"FBTYPE": "BUSY-UNAVAILABLE"})) if uid is not None: fb.addProperty(Property("UID", uid)) else: uid = md5(str(fbcalendar) + str(time.time())).hexdigest() fb.addProperty(Property("UID", uid)) if event_details: for vevent in event_details: fbcalendar.addComponent(vevent) return fbcalendar
def _transferVPOLLData(self, serverComponent, clientComponent): changed = False # Get the matching VVOTER component in each VPOLL serverVoter = serverComponent.voterComponentForVoter(self.attendee) clientVoter = clientComponent.voterComponentForVoter(self.attendee) # Now get a map of each response serverMap = serverVoter.voteMap() clientMap = clientVoter.voteMap() # Remove missing for poll_id in set(serverMap.keys()) - set(clientMap.keys()): serverVoter.removeComponent(serverMap[poll_id]) changed = True # Add new ones for poll_id in set(clientMap.keys()) - set(serverMap.keys()): vote = clientMap[poll_id].duplicate() vote.replaceProperty(Property("LAST-MODIFIED", DateTime.getNowUTC())) serverVoter.addComponent(vote) changed = True # Look for response change for poll_id in set(serverMap.keys()) & set(clientMap.keys()): server_vote = serverMap[poll_id] client_vote = clientMap[poll_id] server_response = server_vote.propertyValue("RESPONSE") client_response = client_vote.propertyValue("RESPONSE") if server_response != client_response: if client_response is not None: server_vote.replaceProperty(Property("RESPONSE", client_response)) else: server_vote.removeProperty("RESPONSE") server_vote.replaceProperty(Property("LAST-MODIFIED", DateTime.getNowUTC())) changed = True return changed
def doRequest(self): """ Execute the actual HTTP request. """ # Invite as user02 now = DateTime.getNowUTC() href = joinURL(self.sessions[1].calendarHref, "organizer.ics") attendees = "\r\n".join(["ATTENDEE:mailto:[email protected]"] + [ATTENDEE % (ctr + 3,) for ctr in range(self.count - 1)]) self.sessions[1].writeData( URL(path=href), ICAL.format(year=now.getYear() + 1, count=self.count, attendees=attendees), "text/calendar", )
def prepare(self): """ Do some setup prior to the real request. """ if not self.full: # Get current sync token results, _ignore_bad = self.sessions[0].getProperties(URL(path=self.sessions[0].calendarHref), (davxml.sync_token,)) self.synctoken = results[davxml.sync_token] # Add resources to create required number of changes now = DateTime.getNowUTC() for i in range(self.count): href = joinURL(self.sessions[0].calendarHref, "sync-collection-%d.ics" % (i + 1,)) self.sessions[0].writeData(URL(path=href), ICAL % (now.getYear() + 1, i + 1,), "text/calendar")
def ensureEvents(self, session, calendarhref, n): """ Make sure the required number of events are present in the calendar. @param n: number of events @type n: C{int} """ now = DateTime.getNowUTC() for i in range(n - self.currentCount): index = self.currentCount + i + 1 href = joinURL(calendarhref, "%d.ics" % (index,)) session.writeData(URL(path=href), ICAL % (now.getYear() + 1, index), "text/calendar") self.currentCount = n
def prepare(self): """ Do some setup prior to the real request. """ if not self.full: # Get current sync token results, _ignore_bad = self.sessions[0].getProperties(URL(path=self.sessions[0].calendarHref), (davxml.sync_token,)) self.synctoken = results[davxml.sync_token] # Add resources to create required number of changes now = DateTime.getNowUTC() for i in range(self.count): href = joinURL(self.sessions[0].calendarHref, "sync-collection-%d.ics" % (i + 1,)) self.sessions[0].writeData(URL(path=href), ICAL % (now.getYear() + 1, i + 1,), "text/calendar")
def doRequest(self): """ Execute the actual HTTP request. """ # Invite as user02 now = DateTime.getNowUTC() href = joinURL(self.sessions[1].calendarHref, "organizer.ics") attendees = "\r\n".join(["ATTENDEE:mailto:[email protected]"] + [ATTENDEE % (ctr + 3,) for ctr in range(self.count - 1)]) self.sessions[1].writeData( URL(path=href), ICAL.format(year=now.getYear() + 1, count=self.count, attendees=attendees), "text/calendar", )
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)) choice = self.random.randint(1, 100) if choice <= self._fileAttachPercentage: attachmentSize = int(self._fileSizeDistribution.sample()) else: attachmentSize = 0 # no attachment href = '%s%s.ics' % (calendar.url, uid) d = self._client.addEvent(href, vcalendar, attachmentSize=attachmentSize) return self._newOperation("create", d)
def _addTask(self): if not self._client.started: return succeed(None) calendars = self._calendarsOfType(caldavxml.calendar, "VTODO") while calendars: calendar = self.random.choice(calendars) calendars.remove(calendar) # Copy the template task and fill in some of its fields # to make a new task to create on the calendar. vcalendar = self._taskTemplate.duplicate() vtodo = vcalendar.mainComponent() uid = str(uuid4()) due = self._taskStartDistribution.sample() vtodo.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vtodo.replaceProperty(Property("DUE", due)) vtodo.replaceProperty(Property("UID", uid)) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def _initEvent(self): # Don't perform any operations until the client is up and running if not self._client.started: return succeed(None) # If it already exists, don't re-create calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0] if calendar.events: events = [ event for event in calendar.events.values() if event.url.endswith("event_to_update.ics") ] if events: return succeed(None) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration( seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) href = '%s%s' % (calendar.url, "event_to_update.ics") d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def _addTask(self): if not self._client.started: return succeed(None) calendars = self._calendarsOfType(caldavxml.calendar, "VTODO") while calendars: calendar = self.random.choice(calendars) calendars.remove(calendar) # Copy the template task and fill in some of its fields # to make a new task to create on the calendar. vcalendar = self._taskTemplate.duplicate() vtodo = vcalendar.mainComponent() uid = str(uuid4()) due = self._taskStartDistribution.sample() vtodo.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vtodo.replaceProperty(Property("DUE", due)) vtodo.replaceProperty(Property("UID", uid)) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def setUp(self): yield super(SchedulerFreeBusyRequest, self).setUp() yield self.buildStoreAndDirectory() yield self.populate() self.now = DateTime.getNowUTC() self.now.setHHMMSS(0, 0, 0) self.now_12H = self.now.duplicate() self.now_12H.offsetHours(12) self.now_13H = self.now.duplicate() self.now_13H.offsetHours(13) self.now_1D = self.now.duplicate() self.now_1D.offsetDay(1)
def setUp(self): yield super(GenerateFreeBusyInfo, self).setUp() self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory) yield self.populate() self.now = DateTime.getNowUTC() self.now.setHHMMSS(0, 0, 0) self.now_12H = self.now.duplicate() self.now_12H.offsetHours(12) self.now_13H = self.now.duplicate() self.now_13H.offsetHours(13) self.now_1D = self.now.duplicate() self.now_1D.offsetDay(1)
def setUp(self): yield super(GenerateFreeBusyInfo, self).setUp() yield self.buildStoreAndDirectory() yield self.populate() self.now = DateTime.getNowUTC() self.now.setHHMMSS(0, 0, 0) self.now_12H = self.now.duplicate() self.now_12H.offsetHours(12) self.now_13H = self.now.duplicate() self.now_13H.offsetHours(13) self.now_1D = self.now.duplicate() self.now_1D.offsetDay(1)
def prepare(self): """ Do some setup prior to the real request. """ # Add resources to create required number of changes self.start = DateTime.getNowUTC() self.start.setHHMMSS(12, 0, 0) self.end = self.start.duplicate() self.end.offsetHours(1) for i in range(self.count): href = joinURL(self.sessions[0].calendarHref, "tr-query-%d.ics" % (i + 1, )) self.sessions[0].writeData(URL(path=href), ICAL % ( self.start.getText(), i + 1, ), "text/calendar")
def buildFreeBusyResult(self, fbinfo, method=None): """ Generate a VCALENDAR object containing a single VFREEBUSY that is the aggregate of the free busy info passed in. @param fbinfo: the array of busy periods to use. @param method: the METHOD property value to insert. @return: the L{Component} containing the calendar data. """ # Merge overlapping time ranges in each fb info section normalizePeriodList(fbinfo.busy) normalizePeriodList(fbinfo.tentative) normalizePeriodList(fbinfo.unavailable) # Now build a new calendar object with the free busy info we have fbcalendar = Component("VCALENDAR") fbcalendar.addProperty(Property("VERSION", "2.0")) fbcalendar.addProperty(Property("PRODID", iCalendarProductID)) if method: fbcalendar.addProperty(Property("METHOD", method)) fb = Component("VFREEBUSY") fbcalendar.addComponent(fb) if self.organizerProp is not None: fb.addProperty(self.organizerProp) if self.attendeeProp is not None: fb.addProperty(self.attendeeProp) fb.addProperty(Property("DTSTART", self.timerange.getStart())) fb.addProperty(Property("DTEND", self.timerange.getEnd())) fb.addProperty(Property("DTSTAMP", DateTime.getNowUTC())) if len(fbinfo.busy) != 0: fb.addProperty(Property("FREEBUSY", fbinfo.busy, {"FBTYPE": "BUSY"})) if len(fbinfo.tentative) != 0: fb.addProperty(Property("FREEBUSY", fbinfo.tentative, {"FBTYPE": "BUSY-TENTATIVE"})) if len(fbinfo.unavailable) != 0: fb.addProperty(Property("FREEBUSY", fbinfo.unavailable, {"FBTYPE": "BUSY-UNAVAILABLE"})) if self.uid is not None: fb.addProperty(Property("UID", self.uid)) else: uid = str(uuid.uuid4()) fb.addProperty(Property("UID", uid)) if self.event_details: for vevent in self.event_details: fbcalendar.addComponent(vevent) return fbcalendar
def buildFreeBusyResult(self, fbinfo, method=None): """ Generate a VCALENDAR object containing a single VFREEBUSY that is the aggregate of the free busy info passed in. @param fbinfo: the array of busy periods to use. @param method: the METHOD property value to insert. @return: the L{Component} containing the calendar data. """ # Merge overlapping time ranges in each fb info section normalizePeriodList(fbinfo.busy) normalizePeriodList(fbinfo.tentative) normalizePeriodList(fbinfo.unavailable) # Now build a new calendar object with the free busy info we have fbcalendar = Component("VCALENDAR") fbcalendar.addProperty(Property("VERSION", "2.0")) fbcalendar.addProperty(Property("PRODID", iCalendarProductID)) if method: fbcalendar.addProperty(Property("METHOD", method)) fb = Component("VFREEBUSY") fbcalendar.addComponent(fb) if self.organizerProp is not None: fb.addProperty(self.organizerProp) if self.attendeeProp is not None: fb.addProperty(self.attendeeProp) fb.addProperty(Property("DTSTART", self.timerange.getStart())) fb.addProperty(Property("DTEND", self.timerange.getEnd())) fb.addProperty(Property("DTSTAMP", DateTime.getNowUTC())) if len(fbinfo.busy) != 0: fb.addProperty(Property("FREEBUSY", fbinfo.busy, {"FBTYPE": "BUSY"})) if len(fbinfo.tentative) != 0: fb.addProperty(Property("FREEBUSY", fbinfo.tentative, {"FBTYPE": "BUSY-TENTATIVE"})) if len(fbinfo.unavailable) != 0: fb.addProperty(Property("FREEBUSY", fbinfo.unavailable, {"FBTYPE": "BUSY-UNAVAILABLE"})) if self.uid is not None: fb.addProperty(Property("UID", self.uid)) else: uid = str(uuid.uuid4()) fb.addProperty(Property("UID", uid)) if self.event_details: for vevent in self.event_details: fbcalendar.addComponent(vevent) return fbcalendar
def ensureEvents(self, session, calendarhref, n): """ Make sure the required number of events are present in the calendar. @param n: number of events @type n: C{int} """ now = DateTime.getNowUTC() for i in range(n - self.currentCount): index = self.currentCount + i + 1 href = joinURL(calendarhref, "%d.ics" % (index, )) session.writeData(URL(path=href), ICAL % ( now.getYear() + 1, index, ), "text/calendar") self.currentCount = n
def action(self): # Don't perform any operations until the client is up and running if not self._client.started: returnValue(None) event = self._getRandomEventOfType('VEVENT', justOwned=True) if not event: returnValue(None) component = event.component vevent = component.mainComponent() label = yield self.modifyEvent(event.url, vevent) if label: vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) event.component = component yield self._newOperation(label, self._client.changeEvent(event.url))
def resetAttendeePartstat(self, component, cuas, partstat, hadRSVP=False): """ Change the PARTSTAT on any ATTENDEE properties that match the list of calendar user addresses on the component passed in. Also adjust the TRANSP property to match the new PARTSTAT value. @param component: an iCalendar component to modify @type attendees: L{Component} @param cuas: a list of calendar user addresses to match @type attendees: C{list} or C{tuple} @param partstat: new PARTSTAT to set @type partstat: C{str} @param hadRSVP: indicates whether RSVP should be added when changing to NEEDS-ACTION @type hadRSVP: C{bool} @return: C{True} if any change was made, C{False} otherwise """ madeChanges = False attendee = component.getAttendeeProperty(cuas) if attendee: if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") != partstat: attendee.setParameter("PARTSTAT", partstat) madeChanges = True # Always remove RSVP when a state other than NEEDS-ACTION is set - this # is only an attendee change so madeChanges does not need to be changed try: if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") != "NEEDS-ACTION": attendee.removeParameter("RSVP") elif hadRSVP: attendee.setParameter("RSVP", "TRUE") except KeyError: pass # Adjust TRANSP to OPAQUE if PARTSTAT is ACCEPTED, otherwise TRANSPARENT component.replaceProperty(Property("TRANSP", "OPAQUE" if partstat == "ACCEPTED" else "TRANSPARENT")) if madeChanges: attendee.setParameter("X-CALENDARSERVER-AUTO", DateTime.getNowUTC().getText()) attendee.removeParameter("X-CALENDARSERVER-DTSTAMP") return madeChanges
def _purgeUID(self, uid): if self.when is None: self.when = DateTime.getNowUTC() cuas = set(( "urn:uuid:{}".format(uid), "urn:x-uid:{}".format(uid) )) # See if calendar home is provisioned txn = self.store.newTransaction() storeCalHome = yield txn.calendarHomeWithUID(uid) calHomeProvisioned = storeCalHome is not None # Always, unshare collections, remove notifications if calHomeProvisioned: yield self._cleanHome(txn, storeCalHome) yield txn.commit() count = 0 if calHomeProvisioned: count = yield self._cancelEvents(txn, uid, cuas) # Remove empty calendar collections (and calendar home if no more # calendars) yield self._removeCalendarHome(uid) # Remove VCards count += (yield self._removeAddressbookHome(uid)) if self.proxies and not self.dryrun: if self.verbose: print("Deleting any proxy assignments") yield self._purgeProxyAssignments(self.store, uid) returnValue(count)
def _purgeUID(self, uid): if self.when is None: self.when = DateTime.getNowUTC() cuas = set(( "urn:uuid:{}".format(uid), "urn:x-uid:{}".format(uid) )) # See if calendar home is provisioned txn = self.store.newTransaction() storeCalHome = yield txn.calendarHomeWithUID(uid) calHomeProvisioned = storeCalHome is not None # Always, unshare collections, remove notifications if calHomeProvisioned: yield self._cleanHome(txn, storeCalHome) yield txn.commit() count = 0 if calHomeProvisioned: count = yield self._cancelEvents(txn, uid, cuas) # Remove empty calendar collections (and calendar home if no more # calendars) yield self._removeCalendarHome(uid) # Remove VCards count += (yield self._removeAddressbookHome(uid)) if self.proxies and not self.dryrun: if self.verbose: print("Deleting any proxy assignments") yield self._purgeProxyAssignments(self.store, uid) returnValue(count)
def _transferAttendeeData(self, serverComponent, clientComponent, declines): # We are skipping this check now - instead we let the server data override the broken client data # First check validity of date-time related properties and get removed components which are declines self._checkInvalidChanges(serverComponent, clientComponent, declines) # Now look for items to transfer from one to the other. # We care about the ATTENDEE's PARTSTAT, TRANSP, VALARMS, X-APPLE-NEEDS-REPLY, # DTSTAMP, LAST-MODIFIED, COMPLETED, and ATTACH's referring to a dropbox replyNeeded = False # ATTENDEE/PARTSTAT/RSVP serverAttendee = serverComponent.getAttendeeProperty((self.attendee, )) clientAttendee = clientComponent.getAttendeeProperty((self.attendee, )) # Possible case where one ATTENDEE prop is missing - this happens with a "fake" master sometimes if serverAttendee is None or clientAttendee is None: log.error( "ATTENDEE for user making an attendee change is missing: {attendee}", attendee=self.attendee) return False, False if serverAttendee.parameterValue( "PARTSTAT", "NEEDS-ACTION") != clientAttendee.parameterValue( "PARTSTAT", "NEEDS-ACTION"): serverAttendee.setParameter( "PARTSTAT", clientAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION")) # If PARTSTAT was changed by the attendee, add a timestamp if needed if config.Scheduling.Options.TimestampAttendeePartStatChanges: serverAttendee.setParameter(DTSTAMP_PARAM, DateTime.getNowUTC().getText()) serverAttendee.removeParameter("X-CALENDARSERVER-AUTO") replyNeeded = True # Apply client fix only if the PARTSTAT was changed if self.forceTRANSP: if clientAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION") in ( "ACCEPTED", "TENTATIVE", ): clientComponent.replaceProperty( Property("TRANSP", "OPAQUE")) else: clientComponent.replaceProperty( Property("TRANSP", "TRANSPARENT")) if serverAttendee.parameterValue( "RSVP", "FALSE") != clientAttendee.parameterValue( "RSVP", "FALSE"): if clientAttendee.parameterValue("RSVP", "FALSE") == "FALSE": try: serverAttendee.removeParameter("RSVP") except KeyError: pass else: serverAttendee.setParameter("RSVP", "TRUE") for pname in config.Scheduling.CalDAV.AttendeePublicParameters: serverValue = serverAttendee.parameterValue(pname) clientValue = clientAttendee.parameterValue(pname) if serverValue != clientValue: if clientValue is None: serverAttendee.removeParameter(pname) else: serverAttendee.setParameter(pname, clientValue) replyNeeded = True # Transfer these properties from the client data self._transferProperty("TRANSP", serverComponent, clientComponent) self._transferProperty("DTSTAMP", serverComponent, clientComponent) self._transferProperty("LAST-MODIFIED", serverComponent, clientComponent) self._transferProperty("COMPLETED", serverComponent, clientComponent) for pname in config.Scheduling.CalDAV.PerAttendeeProperties: self._transferProperty(pname, serverComponent, clientComponent) replyNeeded |= self._transferProperty(PRIVATE_COMMENT, serverComponent, clientComponent) for pname in config.Scheduling.CalDAV.AttendeePublicProperties: replyNeeded |= self._transferProperty(pname, serverComponent, clientComponent) # Dropbox - this now never returns false if config.EnableDropBox: self._transferDropBoxData(serverComponent, clientComponent) # Handle VALARMs serverComponent.removeAlarms() for comp in clientComponent.subcomponents(): if comp.name() == "VALARM": serverComponent.addComponent(comp) # VPOLL if serverComponent.name() == "VPOLL": replyNeeded = self._transferVPOLLData(serverComponent, clientComponent) return True, replyNeeded
def __init__(self, *children): super(DTStamp, self).__init__(*children) if not self.children: self.children = (PCDATAElement(DateTime.getNowUTC().getText()), )
def doCapabilities(self, request): """ Return a list of all timezones known to the server. """ # Determine min/max date-time for iSchedule now = DateTime.getNowUTC() minDateTime = DateTime(now.getYear(), 1, 1, 0, 0, 0, Timezone.UTCTimezone) minDateTime.offsetYear(-1) maxDateTime = DateTime(now.getYear(), 1, 1, 0, 0, 0, Timezone.UTCTimezone) maxDateTime.offsetYear(10) dataTypes = [] dataTypes.append( ischedulexml.CalendarDataType(**{ "content-type": "text/calendar", "version": "2.0", })) if config.EnableJSONData: dataTypes.append( ischedulexml.CalendarDataType( **{ "content-type": "application/calendar+json", "version": "2.0", })) componentTypes = [] from twistedcaldav.ical import allowedSchedulingComponents for name in allowedSchedulingComponents: if name == "VFREEBUSY": componentTypes.append( ischedulexml.Component(ischedulexml.Method(name="REQUEST"), name=name)) else: componentTypes.append( ischedulexml.Component(ischedulexml.Method(name="REQUEST"), ischedulexml.Method(name="CANCEL"), ischedulexml.Method(name="REPLY"), name=name)) result = ischedulexml.QueryResult( ischedulexml.Capabilities( ischedulexml.Version.fromString( config.Scheduling.iSchedule.SerialNumber), ischedulexml.Versions( ischedulexml.Version.fromString("1.0"), ), ischedulexml.SchedulingMessages(*componentTypes), ischedulexml.CalendarDataTypes(*dataTypes), ischedulexml.Attachments(ischedulexml.External(), ), ischedulexml.MaxContentLength.fromString( config.MaxResourceSize), ischedulexml.MinDateTime.fromString(minDateTime.getText()), ischedulexml.MaxDateTime.fromString(maxDateTime.getText()), ischedulexml.MaxInstances.fromString( config.MaxAllowedInstances), ischedulexml.MaxRecipients.fromString( config.MaxAttendeesPerInstance), ischedulexml.Administrator.fromString( request.unparseURL(params="", querystring="", fragment="")), ), ) response = XMLResponse(responsecode.OK, result) response.headers.addRawHeader( ISCHEDULE_CAPABILITIES, str(config.Scheduling.iSchedule.SerialNumber)) return response
def isNow(self): # Check instance start/end against current date-time now = DateTime.getNowUTC() return self.mInstanceStart <= now and self.mInstanceEnd > now
def setupDateTimeValues(self): self.dtsubs = {} # Set of "now" values that are directly accessible self.now = DateTime.getNowUTC() self.now.setHHMMSS(0, 0, 0) self.now12 = DateTime.getNowUTC() self.now12.setHHMMSS(12, 0, 0) self.nowDate = self.now.duplicate() self.nowDate.setDateOnly(True) self.nowFloating = self.now.duplicate() self.nowFloating.setTimezoneID(None) self.dtsubs["now"] = self.now self.dtsubs["now12"] = self.now12 self.dtsubs["nowDate"] = self.nowDate self.dtsubs["nowFloating"] = self.nowFloating # Values going 30 days back from now for i in range(30): attrname = "now_back%s" % (i + 1, ) setattr(self, attrname, self.now.duplicate()) getattr(self, attrname).offsetDay(-(i + 1)) self.dtsubs[attrname] = getattr(self, attrname) attrname_12h = "now_back%s_12h" % (i + 1, ) setattr(self, attrname_12h, getattr(self, attrname).duplicate()) getattr(self, attrname_12h).offsetHours(12) self.dtsubs[attrname_12h] = getattr(self, attrname_12h) attrname_1 = "now_back%s_1" % (i + 1, ) setattr(self, attrname_1, getattr(self, attrname).duplicate()) getattr(self, attrname_1).offsetSeconds(-1) self.dtsubs[attrname_1] = getattr(self, attrname_1) attrname = "nowDate_back%s" % (i + 1, ) setattr(self, attrname, self.nowDate.duplicate()) getattr(self, attrname).offsetDay(-(i + 1)) self.dtsubs[attrname] = getattr(self, attrname) attrname = "nowFloating_back%s" % (i + 1, ) setattr(self, attrname, self.nowFloating.duplicate()) getattr(self, attrname).offsetDay(-(i + 1)) self.dtsubs[attrname] = getattr(self, attrname) attrname_1 = "nowFloating_back%s_1" % (i + 1, ) setattr(self, attrname_1, getattr(self, attrname).duplicate()) getattr(self, attrname_1).offsetSeconds(-1) self.dtsubs[attrname_1] = getattr(self, attrname_1) # Values going 30 days forward from now for i in range(30): attrname = "now_fwd%s" % (i + 1, ) setattr(self, attrname, self.now.duplicate()) getattr(self, attrname).offsetDay(i + 1) self.dtsubs[attrname] = getattr(self, attrname) attrname_12h = "now_fwd%s_12h" % (i + 1, ) setattr(self, attrname_12h, getattr(self, attrname).duplicate()) getattr(self, attrname_12h).offsetHours(12) self.dtsubs[attrname_12h] = getattr(self, attrname_12h) attrname = "nowDate_fwd%s" % (i + 1, ) setattr(self, attrname, self.nowDate.duplicate()) getattr(self, attrname).offsetDay(i + 1) self.dtsubs[attrname] = getattr(self, attrname) attrname = "nowFloating_fwd%s" % (i + 1, ) setattr(self, attrname, self.nowFloating.duplicate()) getattr(self, attrname).offsetDay(i + 1) self.dtsubs[attrname] = getattr(self, attrname)
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)
def updateLastModified(self): self.removeProperties(definitions.cICalProperty_LAST_MODIFIED) prop = Property(definitions.cICalProperty_LAST_MODIFIED, DateTime.getNowUTC()) self.addProperty(prop)
def initDTSTAMP(self): self.removeProperties(definitions.cICalProperty_DTSTAMP) prop = Property(definitions.cICalProperty_DTSTAMP, DateTime.getNowUTC()) self.addProperty(prop)
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)
def sample(self): now = DateTime.getNowUTC() now.offsetSeconds(int(self._offset.sample())) return now
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)
def isNow(self): # Check instance start/end against current date-time now = DateTime.getNowUTC() return self.mInstanceStart <= now and self.mInstanceEnd > now
from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, Deferred from twistedcaldav.config import config from twistedcaldav.test.util import StoreTestCase from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE from txdav.common.datastore.test.util import populateCalendarsFrom from txweb2.http_headers import MimeType import datetime future = DateTime.getNowUTC() future.offsetDay(1) future = future.getText() past = DateTime.getNowUTC() past.offsetDay(-1) past = past.getText() # For test_purgeExistingGUID # No organizer/attendee NON_INVITE_ICS = """BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT UID:151AFC76-6036-40EF-952B-97D1840760BF SUMMARY:Non Invitation
def updateAttendeeData(from_component, to_component): """ Called when processing a REPLY only. Copy the PARTSTAT of the Attendee in the from_component to the matching ATTENDEE in the to_component. Ignore if no match found. Also update the private comments. For VPOLL we need to copy POLL-ITEM-ID response values into the actual matching polled sub-components as VOTER properties. @param from_component: component to copy from @type from_component: L{Component} @param to_component: component to copy to @type to_component: L{Component} """ # Track what changed partstat_changed = False private_comment_changed = False # Get REQUEST-STATUS as we need to write that into the saved ATTENDEE property reqstatus = tuple(from_component.properties("REQUEST-STATUS")) if reqstatus: reqstatus = ",".join(status.value()[0] for status in reqstatus) else: reqstatus = "2.0" # Get attendee in from_component - there MUST be only one attendees = tuple(from_component.properties(from_component.recipientPropertyName())) if len(attendees) != 1: log.error("There must be one and only one ATTENDEE property in a REPLY\n%s" % (str(from_component),)) return None, False, False attendee = attendees[0] partstat = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") # Now find matching ATTENDEE in to_component existing_attendee = to_component.getAttendeeProperty((attendee.value(),)) if existing_attendee: oldpartstat = existing_attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") existing_attendee.setParameter("PARTSTAT", partstat) existing_attendee.setParameter("SCHEDULE-STATUS", reqstatus) partstat_changed = (oldpartstat != partstat) # Always delete RSVP on PARTSTAT change if partstat_changed: try: existing_attendee.removeParameter("RSVP") except KeyError: pass # Handle attendee comments if config.Scheduling.CalDAV.get("EnablePrivateComments", True): # Look for X-CALENDARSERVER-PRIVATE-COMMENT property in iTIP component (State 1 in spec) attendee_comment = tuple(from_component.properties("X-CALENDARSERVER-PRIVATE-COMMENT")) attendee_comment = attendee_comment[0] if len(attendee_comment) else None # Look for matching X-CALENDARSERVER-ATTENDEE-COMMENT property in existing data (State 2 in spec) private_comments = tuple(to_component.properties("X-CALENDARSERVER-ATTENDEE-COMMENT")) for comment in private_comments: attendeeref = comment.parameterValue("X-CALENDARSERVER-ATTENDEE-REF") if attendeeref == attendee.value(): private_comment = comment break else: private_comment = None else: attendee_comment = None private_comment = None # Now do update logic if attendee_comment is None and private_comment is None: # Nothing to do pass elif attendee_comment is None and private_comment is not None: # We now remove the private comment on the organizer's side if the attendee removed it to_component.removeProperty(private_comment) private_comment_changed = True elif attendee_comment is not None and private_comment is None: # Add new property private_comment = Property( "X-CALENDARSERVER-ATTENDEE-COMMENT", attendee_comment.value(), params={ "X-CALENDARSERVER-ATTENDEE-REF": attendee.value(), "X-CALENDARSERVER-DTSTAMP": DateTime.getNowUTC().getText(), } ) to_component.addProperty(private_comment) private_comment_changed = True else: # Only change if different if private_comment.value() != attendee_comment.value(): # Remove all property parameters private_comment.removeAllParameters() # Add default parameters private_comment.setParameter("X-CALENDARSERVER-ATTENDEE-REF", attendee.value()) private_comment.setParameter("X-CALENDARSERVER-DTSTAMP", DateTime.getNowUTC().getText()) # Set new value private_comment.setValue(attendee_comment.value()) private_comment_changed = True # Do VPOLL transfer if from_component.name() == "VPOLL": # TODO: figure out how to report changes back iTipProcessing.updateVPOLLData(from_component, to_component, attendee) return attendee.value(), partstat_changed, private_comment_changed
def doCapabilities(self, request): """ Return a list of all timezones known to the server. """ # Determine min/max date-time for iSchedule now = DateTime.getNowUTC() minDateTime = DateTime(now.getYear(), 1, 1, 0, 0, 0, Timezone.UTCTimezone) minDateTime.offsetYear(-1) maxDateTime = DateTime(now.getYear(), 1, 1, 0, 0, 0, Timezone.UTCTimezone) maxDateTime.offsetYear(10) dataTypes = [] dataTypes.append( ischedulexml.CalendarDataType(**{ "content-type": "text/calendar", "version": "2.0", }) ) if config.EnableJSONData: dataTypes.append( ischedulexml.CalendarDataType(**{ "content-type": "application/calendar+json", "version": "2.0", }) ) componentTypes = [] from twistedcaldav.ical import allowedSchedulingComponents for name in allowedSchedulingComponents: if name == "VFREEBUSY": componentTypes.append( ischedulexml.Component( ischedulexml.Method(name="REQUEST"), name=name ) ) else: componentTypes.append( ischedulexml.Component( ischedulexml.Method(name="REQUEST"), ischedulexml.Method(name="CANCEL"), ischedulexml.Method(name="REPLY"), name=name ) ) result = ischedulexml.QueryResult( ischedulexml.Capabilities( ischedulexml.Version.fromString(config.Scheduling.iSchedule.SerialNumber), ischedulexml.Versions( ischedulexml.Version.fromString("1.0"), ), ischedulexml.SchedulingMessages(*componentTypes), ischedulexml.CalendarDataTypes(*dataTypes), ischedulexml.Attachments( ischedulexml.External(), ), ischedulexml.MaxContentLength.fromString(config.MaxResourceSize), ischedulexml.MinDateTime.fromString(minDateTime.getText()), ischedulexml.MaxDateTime.fromString(maxDateTime.getText()), ischedulexml.MaxInstances.fromString(config.MaxAllowedInstances), ischedulexml.MaxRecipients.fromString(config.MaxAttendeesPerInstance), ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")), ), ) response = XMLResponse(responsecode.OK, result) response.headers.addRawHeader(ISCHEDULE_CAPABILITIES, str(config.Scheduling.iSchedule.SerialNumber)) return response
def generateAttendeeReply(original, attendee, changedRids=None, force_decline=False): # Start with a copy of the original as we may have to modify bits of it itip = original.duplicate() itip.replaceProperty(Property("PRODID", iCalendarProductID)) itip.addProperty(Property("METHOD", "REPLY")) # Now filter out components except the ones specified itip.filterComponents(changedRids) # Force update to DTSTAMP everywhere so reply sequencing will work itip.replacePropertyInAllComponents(Property("DTSTAMP", DateTime.getNowUTC())) # Remove all attendees except the one we want itip.removeAllButOneAttendee(attendee) # Remove all components which are missing the attendee for component in itip.subcomponents(): if component.name() in ignoredComponents: continue if not component.getAttendeeProperty((attendee,)): itip.removeComponent(component) # No alarms itip.removeAlarms() # Remove all but essential properties itip.filterProperties(keep=( "UID", "RECURRENCE-ID", "SEQUENCE", "STATUS", "DTSTAMP", "DTSTART", "DTEND", "DURATION", "RRULE", "RDATE", "EXDATE", "ORGANIZER", "ATTENDEE", "VOTER", "X-CALENDARSERVER-PRIVATE-COMMENT", "SUMMARY", "LOCATION", "DESCRIPTION", )) # Now set each ATTENDEE's PARTSTAT to DECLINED if force_decline: attendeeProps = itip.getAttendeeProperties((attendee,)) assert attendeeProps, "Must have some matching ATTENDEEs" for attendeeProp in attendeeProps: attendeeProp.setParameter("PARTSTAT", "DECLINED") # Add REQUEST-STATUS to each top-level component itip.addPropertyToAllComponents(Property("REQUEST-STATUS", ["2.0", "Success", ])) # Strip out unwanted bits iTipGenerator.prepareSchedulingMessage(itip, reply=True) # Handle VPOLL behavior for component in itip.subcomponents(): if component.name() == "VPOLL": iTipGenerator.generateVPOLLReply(component, attendee) return itip
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", justOwned=True) 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: continue choice = self.random.randint(1, 100) if choice <= self._fileAttachPercentage: attachmentSize = int(self._fileSizeDistribution.sample()) else: attachmentSize = 0 # no attachment href = '%s%s.ics' % (calendar.url, uid) d = self._client.addInvite( href, vcalendar, attachmentSize=attachmentSize, lookupPercentage=self._inviteeLookupPercentage ) return self._newOperation("invite", d)