[CalendarServer-changes] [13591] CalendarServer/branches/release/CalendarServer-5.3-dev
source_changes at macosforge.org
source_changes at macosforge.org
Sun Jun 1 19:47:48 PDT 2014
Revision: 13591
http://trac.calendarserver.org//changeset/13591
Author: cdaboo at apple.com
Date: 2014-06-01 19:47:48 -0700 (Sun, 01 Jun 2014)
Log Message:
-----------
Merge schedule-agent fix from trunk.
Modified Paths:
--------------
CalendarServer/branches/release/CalendarServer-5.3-dev/twistedcaldav/ical.py
CalendarServer/branches/release/CalendarServer-5.3-dev/twistedcaldav/test/test_icalendar.py
CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/cuaddress.py
CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/implicit.py
CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/processing.py
CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_implicit.py
Added Paths:
-----------
CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_processing.py
Removed Paths:
-------------
CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_pocessing.py
Modified: CalendarServer/branches/release/CalendarServer-5.3-dev/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.3-dev/twistedcaldav/ical.py 2014-06-02 02:10:46 UTC (rev 13590)
+++ CalendarServer/branches/release/CalendarServer-5.3-dev/twistedcaldav/ical.py 2014-06-02 02:47:48 UTC (rev 13591)
@@ -2156,6 +2156,26 @@
return is_server
+ def cleanOrganizerScheduleAgent(self):
+ """
+ Remove components whose ORGANIZER property does not have
+ SCHEDULE-AGENT=SERVER.
+ """
+
+ changed = False
+ for component in tuple(self.subcomponents()):
+ if component.name() in ignoredComponents:
+ continue
+
+ organizerProp = component.getOrganizerProperty()
+ if organizerProp is not None:
+ if organizerProp.parameterValue("SCHEDULE-AGENT", "SERVER") != "SERVER":
+ self.removeComponent(component)
+ changed = True
+
+ return changed
+
+
def getAttendees(self):
"""
Get the attendee value. Works on either a VCALENDAR or on a component.
Modified: CalendarServer/branches/release/CalendarServer-5.3-dev/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.3-dev/twistedcaldav/test/test_icalendar.py 2014-06-02 02:10:46 UTC (rev 13590)
+++ CalendarServer/branches/release/CalendarServer-5.3-dev/twistedcaldav/test/test_icalendar.py 2014-06-02 02:47:48 UTC (rev 13591)
@@ -10429,3 +10429,163 @@
changed = component.hasDuplicatePrivateComments(doFix=True)
self.assertEqual(sorted(normalize_iCalStr(component).splitlines()), sorted(normalize_iCalStr(result).splitlines()), msg=title)
self.assertEqual(changed, result_changed, msg=title)
+
+
+ def test_cleanOrganizerScheduleAgent(self):
+ """
+ Test that L{Component.cleanOrganizerScheduleAgent} correctly removes components.
+ """
+
+ data = (
+ (
+ "All SERVER - master only",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ False,
+ ),
+ (
+ "All SERVER - master and overrides",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ False,
+ ),
+ (
+ "Master CLIENT and override SERVER",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER;SCHEDULE-AGENT=CLIENT:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ True,
+ ),
+ )
+
+ for title, txt, result, result_changed in data:
+ component = Component.fromString(txt)
+ changed = component.cleanOrganizerScheduleAgent()
+ self.assertEqual(sorted(normalize_iCalStr(component).splitlines()), sorted(normalize_iCalStr(result).splitlines()), msg=title)
+ self.assertEqual(changed, result_changed, msg=title)
Modified: CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/cuaddress.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/cuaddress.py 2014-06-02 02:10:46 UTC (rev 13590)
+++ CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/cuaddress.py 2014-06-02 02:47:48 UTC (rev 13591)
@@ -38,7 +38,14 @@
self.serviceType = None
+ def hosted(self):
+ """
+ Is this user hosted on this service (this pod or any other)
+ """
+ return False
+
+
class LocalCalendarUser(CalendarUser):
def __init__(self, cuaddr, principal, inbox=None):
@@ -52,7 +59,14 @@
return "Local calendar user: %s" % (self.cuaddr,)
+ def hosted(self):
+ """
+ Is this user hosted on this service (this pod or any other)
+ """
+ return True
+
+
class PartitionedCalendarUser(CalendarUser):
def __init__(self, cuaddr, principal):
@@ -65,7 +79,14 @@
return "Partitioned calendar user: %s" % (self.cuaddr,)
+ def hosted(self):
+ """
+ Is this user hosted on this service (this pod or any other)
+ """
+ return True
+
+
class OtherServerCalendarUser(CalendarUser):
def __init__(self, cuaddr, principal):
@@ -78,7 +99,14 @@
return "Other server calendar user: %s" % (self.cuaddr,)
+ def hosted(self):
+ """
+ Is this user hosted on this service (this pod or any other)
+ """
+ return True
+
+
class RemoteCalendarUser(CalendarUser):
def __init__(self, cuaddr):
Modified: CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/implicit.py 2014-06-02 02:10:46 UTC (rev 13590)
+++ CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/implicit.py 2014-06-02 02:47:48 UTC (rev 13591)
@@ -141,6 +141,7 @@
# If the new data has no organizer, then there must also be no attendees
if self.organizer is None and self.attendees:
+ log.error("organizer-allowed: Organizer removal also requires attendees to be removed for UID: %s" % (self.uid,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "organizer-allowed"),
@@ -163,6 +164,11 @@
(existing_type != new_type) and
existing_resource
):
+ log.error("valid-attendee-change: Cannot change scheduling object mode for %s to %s for UID: %s" % (
+ existing_type,
+ new_type,
+ self.uid
+ ))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -171,6 +177,7 @@
# Organizer events must have a master component
if self.state == "organizer" and self.calendar.masterComponent() is None:
+ log.error("organizer-allowed: Organizer cannot schedule without a master component for UID: %s" % (self.uid,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "organizer-allowed"),
@@ -251,7 +258,7 @@
# to create new scheduling resources.
if self.action == "create":
if self.organizerPrincipal and not self.organizerPrincipal.enabledAsOrganizer():
- log.error("ORGANIZER not allowed to be an Organizer: %s" % (self.organizer,))
+ log.error("organizer-allowed: ORGANIZER not allowed to be an Organizer: %s" % (self.organizer,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "organizer-allowed"),
@@ -427,7 +434,7 @@
self.organizer = self.calendar.validOrganizerForScheduling()
except ValueError:
# We have different ORGANIZERs in the same iCalendar object - this is an error
- log.error("Only one ORGANIZER is allowed in an iCalendar object:\n%s" % (self.calendar,))
+ log.error("single-organizer: Only one ORGANIZER is allowed in an iCalendar object:\n%s" % (self.calendar,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "single-organizer"),
@@ -715,7 +722,7 @@
oldOrganizer = _normalizeCUAddress(self.oldcalendar.getOrganizer())
newOrganizer = _normalizeCUAddress(self.calendar.getOrganizer())
if oldOrganizer != newOrganizer:
- log.error("Cannot change ORGANIZER: UID:%s" % (self.uid,))
+ log.error("valid-organizer-change: Cannot change ORGANIZER: UID:%s" % (self.uid,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-organizer-change"),
@@ -1118,7 +1125,7 @@
oldOrganizer = self.oldcalendar.getOrganizer()
newOrganizer = self.calendar.getOrganizer()
if oldOrganizer != newOrganizer:
- log.error("Cannot change ORGANIZER: UID:%s" % (self.uid,))
+ log.error("valid-attendee-change: Cannot change ORGANIZER: UID:%s" % (self.uid,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -1133,13 +1140,24 @@
# If Organizer copy exists we cannot allow SCHEDULE-AGENT=CLIENT or NONE
if not doScheduling:
- log.error("Attendee '%s' is not allowed to change SCHEDULE-AGENT on organizer: UID:%s" % (self.attendeePrincipal, self.uid,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-attendee-change"),
- "Cannot alter organizer",
- ))
+ # If an existing resource is present and it does not have SCHEDULE-AGENT=SERVER, then
+ # try and fix the situation by using the organizer's copy of the event and stripping
+ # the incoming attendee copy of any SCHEDULE-AGENT=CLIENT components. That should allow
+ # a fixed version of the data to be stored and proper scheduling to occur.
+ if self.oldcalendar is not None and not self.oldcalendar.getOrganizerScheduleAgent():
+ self.oldcalendar = self.organizer_calendar.duplicate()
+ self.oldcalendar.attendeesView((self.attendee,), onlyScheduleAgentServer=True)
+ self.calendar.cleanOrganizerScheduleAgent()
+ doScheduling = True
+ if not doScheduling:
+ log.error("valid-attendee-change: Attendee '%s' is not allowed to change SCHEDULE-AGENT on organizer: UID:%s" % (self.attendeePrincipal, self.uid,))
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "valid-attendee-change"),
+ "Cannot alter organizer",
+ ))
+
# Determine whether the current change is allowed
changeAllowed, doITipReply, changedRids, newCalendar = self.isAttendeeChangeInsignificant()
if changeAllowed:
@@ -1151,7 +1169,7 @@
self.return_status = ImplicitScheduler.STATUS_ORPHANED_EVENT
returnValue(None)
else:
- log.error("Attendee '%s' is not allowed to make an unauthorized change to an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
+ log.error("valid-attendee-change: Attendee '%s' is not allowed to make an unauthorized change to an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -1183,7 +1201,7 @@
if self.oldcalendar:
oldScheduling = self.oldcalendar.getOrganizerScheduleAgent()
if not oldScheduling:
- log.error("Attendee '%s' is not allowed to set SCHEDULE-AGENT=SERVER on organizer: UID:%s" % (self.attendeePrincipal, self.uid,))
+ log.error("valid-attendee-change: Attendee '%s' is not allowed to set SCHEDULE-AGENT=SERVER on organizer: UID:%s" % (self.attendeePrincipal, self.uid,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -1240,7 +1258,7 @@
oldOrganizer = self.oldcalendar.getOrganizer()
newOrganizer = self.calendar.getOrganizer()
if oldOrganizer != newOrganizer and self.oldcalendar.getOrganizerScheduleAgent():
- log.error("Cannot change ORGANIZER: UID:%s" % (self.uid,))
+ log.error("valid-attendee-change: Cannot change ORGANIZER: UID:%s" % (self.uid,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -1260,7 +1278,7 @@
break
if found_old:
- log.error("Cannot remove ATTENDEE: UID:%s" % (self.uid,))
+ log.error("valid-attendee-change: Cannot remove ATTENDEE: UID:%s" % (self.uid,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
Modified: CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/processing.py 2014-06-02 02:10:46 UTC (rev 13590)
+++ CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/processing.py 2014-06-02 02:47:48 UTC (rev 13591)
@@ -460,8 +460,11 @@
new_organizer = normalizeCUAddr(self.message.getOrganizer())
new_organizer = normalizeCUAddr(new_organizer) if new_organizer else ""
if existing_organizer != new_organizer:
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - organizer has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- raise ImplicitProcessorException("5.3;Organizer change not allowed")
+ # Additional check - if the existing organizer is missing and the originator
+ # is local to the server - then allow the change
+ if not (existing_organizer == "" and self.originator.hosted()):
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - organizer has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ raise ImplicitProcessorException("5.3;Organizer change not allowed")
# Handle splitting of data early so we can preserve per-attendee data
if self.message.hasProperty("X-CALENDARSERVER-SPLIT-OLDER-UID"):
Modified: CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_implicit.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_implicit.py 2014-06-02 02:10:46 UTC (rev 13590)
+++ CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_implicit.py 2014-06-02 02:47:48 UTC (rev 13591)
@@ -1624,3 +1624,324 @@
self.assertEqual(len(list2), 2)
self.assertTrue(list2[0].startswith(hashlib.md5("12345-67890").hexdigest()))
self.assertTrue(list2[1].startswith(hashlib.md5("12345-67890").hexdigest()))
+
+
+
+class ScheduleAgentFixBase(CommonCommonTests, TestCase):
+ """
+ Test txdav.caldav.datastore.scheduling.implicit.
+ """
+
+ @inlineCallbacks
+ def setUp(self):
+ yield super(ScheduleAgentFixBase, self).setUp()
+ self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+ yield self.populate()
+ self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 0)
+
+
+ @inlineCallbacks
+ def populate(self):
+ yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+ self.notifierFactory.reset()
+
+
+ def storeUnderTest(self):
+ """
+ Create and return a L{CalendarStore} for testing.
+ """
+ return self._sqlCalendarStore
+
+ metadata = {
+ "accessMode": "PUBLIC",
+ "isScheduleObject": True,
+ "scheduleTag": "abc",
+ "scheduleEtags": (),
+ "hasPrivateComment": False,
+ }
+
+ @classproperty(cache=False)
+ def requirements(cls): #@NoSelf
+ return {
+ "user01": {
+ "calendar_1": {
+ "organizer.ics": (cls.organizer_data, cls.metadata),
+ },
+ "inbox": {
+ },
+ },
+ "user02": {
+ "calendar_1": {
+ "attendee2.ics": (cls.attendee2_data, cls.metadata),
+ },
+ "inbox": {
+ },
+ },
+ "user03": {
+ "calendar_1": {
+ "attendee3.ics": (cls.attendee3_data, cls.metadata),
+ },
+ "inbox": {
+ },
+ },
+ }
+
+
+
+class ScheduleAgentFix(ScheduleAgentFixBase):
+ """
+ Test that implicit scheduling where an attendee has S-A=CLIENT and S-A=SERVER is
+ corrected when the attendee updates.
+ """
+
+ organizer_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user03
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+RECURRENCE-ID:20140102T100000Z
+DTSTART:20140102T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+ attendee2_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER;SCHEDULE-AGENT=CLIENT:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user03
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+RECURRENCE-ID:20140102T100000Z
+DTSTART:20140102T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER;SCHEDULE-AGENT=SERVER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+ attendee2_update_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER;SCHEDULE-AGENT=CLIENT:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user03
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+RECURRENCE-ID:20140102T100000Z
+DTSTART:20140102T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER;SCHEDULE-AGENT=SERVER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+ attendee3_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user03
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+RECURRENCE-ID:20140102T100000Z
+DTSTART:20140102T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+
+ @inlineCallbacks
+ def test_doImplicitScheduling(self):
+ """
+ Test that doImplicitScheduling fixes an inconsistent schedule-agent state when an
+ attendee stores their data.
+ """
+
+ cobj = yield self.calendarObjectUnderTest(home="user02", name="attendee2.ics")
+ yield cobj.setComponent(Component.fromString(self.attendee2_update_data))
+ yield self.commit()
+
+ cobj = yield self.calendarObjectUnderTest(home="user02", name="attendee2.ics")
+ comp = yield cobj.component()
+ self.assertTrue(comp.masterComponent() is None)
+ self.assertTrue(comp.getOrganizerScheduleAgent())
+
+ inbox = yield self.calendarUnderTest(home="user01", name="inbox")
+ cobjs = yield inbox.calendarObjects()
+ self.assertTrue(len(cobjs) == 1)
+
+
+
+class MissingOrganizerFix(ScheduleAgentFixBase):
+ """
+ Test that an attendee with a copy of an event without any organizer or attendee
+ properties is corrected when the organizer updates.
+ """
+
+ organizer_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+ organizer_update_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+ attendee2_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+END:VEVENT
+END:VCALENDAR
+"""
+
+ attendee3_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+
+ @inlineCallbacks
+ def test_doImplicitScheduling(self):
+ """
+ Test that doImplicitScheduling fixes an inconsistent schedule-agent state when an
+ attendee stores their data.
+ """
+
+ cobj = yield self.calendarObjectUnderTest(home="user02", name="attendee2.ics")
+ comp = yield cobj.component()
+ self.assertTrue(comp.getOrganizer() is None)
+ yield self.commit()
+
+ cobj = yield self.calendarObjectUnderTest(home="user01", name="organizer.ics")
+ yield cobj.setComponent(Component.fromString(self.organizer_update_data))
+ yield self.commit()
+
+ cobj = yield self.calendarObjectUnderTest(home="user02", name="attendee2.ics")
+ comp = yield cobj.component()
+ self.assertTrue(comp.getOrganizer() is not None)
+
+ inbox = yield self.calendarUnderTest(home="user02", name="inbox")
+ cobjs = yield inbox.calendarObjects()
+ self.assertTrue(len(cobjs) == 1)
Deleted: CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_pocessing.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_pocessing.py 2014-06-02 02:10:46 UTC (rev 13590)
+++ CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_pocessing.py 2014-06-02 02:47:48 UTC (rev 13591)
@@ -1,232 +0,0 @@
-##
-# Copyright (c) 2005-2014 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks, succeed
-from twisted.trial import unittest
-
-from twistedcaldav import memcacher
-from twistedcaldav.ical import Component
-from twistedcaldav.stdconfig import config
-
-from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
-from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
-
-
-class FakeImplicitProcessor(ImplicitProcessor):
- """
- A fake ImplicitProcessor that tracks batch refreshes.
- """
-
- def __init__(self):
- self.batches = 0
-
-
- def _enqueueBatchRefresh(self):
- self.batches += 1
-
-
- def writeCalendarResource(self, collection, resource, calendar):
- return succeed(FakeResource())
-
-
-
-class FakePrincipal(object):
-
- def __init__(self, cuaddr):
- self.cuaddr = cuaddr
-
-
- def calendarUserAddresses(self):
- return (self.cuaddr,)
-
-
-
-class FakeResource(object):
-
- def parentCollection(self):
- return self
-
-
- def ownerHome(self):
- return self
-
-
- def uid(self):
- return None
-
-
- def id(self):
- return None
-
-
-
-class BatchRefresh (unittest.TestCase):
- """
- iCalendar support tests
- """
-
- def setUp(self):
- super(BatchRefresh, self).setUp()
- config.Memcached.Pools.Default.ClientEnabled = False
- config.Memcached.Pools.Default.ServerEnabled = False
- memcacher.Memcacher.allowTestCache = True
- memcacher.Memcacher.memoryCacheInstance = None
-
-
- @inlineCallbacks
- def test_queueAttendeeUpdate_no_refresh(self):
-
- self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
-
- calendar = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE:urn:uuid:user01
-ATTENDEE:urn:uuid:user02
-END:VEVENT
-END:VCALENDAR
-""")
- processor = FakeImplicitProcessor()
- processor.txn = ""
- processor.uid = "12345-67890"
- processor.recipient_calendar = calendar
- yield processor.queueAttendeeUpdate(("urn:uuid:user02", "urn:uuid:user01",))
- self.assertEqual(processor.batches, 0)
-
-
- @inlineCallbacks
- def test_queueAttendeeUpdate_with_refresh(self):
-
- self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
-
- calendar = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE:urn:uuid:user01
-ATTENDEE:urn:uuid:user02
-ATTENDEE:urn:uuid:user03
-END:VEVENT
-END:VCALENDAR
-""")
- processor = FakeImplicitProcessor()
- processor.txn = ""
- processor.uid = "12345-67890"
- processor.recipient_calendar = calendar
- yield processor.queueAttendeeUpdate(("urn:uuid:user02", "urn:uuid:user01",))
- self.assertEqual(processor.batches, 1)
-
-
- @inlineCallbacks
- def test_queueAttendeeUpdate_count_suppressed(self):
-
- self.patch(config.Scheduling.Options, "AttendeeRefreshCountLimit", 5)
- self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
-
- calendar_small = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE:urn:uuid:user01
-ATTENDEE:urn:uuid:user02
-ATTENDEE:urn:uuid:user03
-ATTENDEE:urn:uuid:user04
-END:VEVENT
-END:VCALENDAR
-""")
- itip_small = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-METHOD:REPLY
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE;PARTSTAT="ACCEPTED":urn:uuid:user02
-END:VEVENT
-END:VCALENDAR
-""")
- calendar_large = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE:urn:uuid:user01
-ATTENDEE:urn:uuid:user02
-ATTENDEE:urn:uuid:user03
-ATTENDEE:urn:uuid:user04
-ATTENDEE:urn:uuid:user05
-ATTENDEE:urn:uuid:user06
-ATTENDEE:urn:uuid:user07
-ATTENDEE:urn:uuid:user08
-ATTENDEE:urn:uuid:user09
-ATTENDEE:urn:uuid:user10
-END:VEVENT
-END:VCALENDAR
-""")
- itip_large = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-METHOD:REPLY
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE;PARTSTAT="ACCEPTED":urn:uuid:user02
-END:VEVENT
-END:VCALENDAR
-""")
-
- for count, calendar, itip, result, msg in (
- (5, calendar_small, itip_small, 1, "Small, count=5"),
- (5, calendar_large, itip_large, 0, "Large, count=5"),
- (0, calendar_small, itip_small, 1, "Small, count=0"),
- (0, calendar_large, itip_large, 1, "Large, count=0"),
- ):
- config.Scheduling.Options.AttendeeRefreshCountLimit = count
- processor = FakeImplicitProcessor()
- processor.txn = ""
- processor.recipient_calendar = calendar.duplicate()
- processor.uid = processor.recipient_calendar.newUID()
- processor.recipient_calendar_resource = None
- processor.message = itip.duplicate()
- processor.message.newUID(processor.uid)
- processor.originator = LocalCalendarUser(None, None)
- processor.recipient = LocalCalendarUser(None, None)
- processor.uid = calendar.resourceUID()
- processor.noAttendeeRefresh = False
-
- processed = yield processor.doImplicitOrganizerUpdate()
- self.assertTrue(processed[3] is not None, msg=msg)
- self.assertEqual(processor.batches, result, msg=msg)
Copied: CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_processing.py (from rev 13299, CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_pocessing.py)
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_processing.py (rev 0)
+++ CalendarServer/branches/release/CalendarServer-5.3-dev/txdav/caldav/datastore/scheduling/test/test_processing.py 2014-06-02 02:47:48 UTC (rev 13591)
@@ -0,0 +1,232 @@
+##
+# Copyright (c) 2005-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, succeed
+from twisted.trial import unittest
+
+from twistedcaldav import memcacher
+from twistedcaldav.ical import Component
+from twistedcaldav.stdconfig import config
+
+from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
+from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
+
+
+class FakeImplicitProcessor(ImplicitProcessor):
+ """
+ A fake ImplicitProcessor that tracks batch refreshes.
+ """
+
+ def __init__(self):
+ self.batches = 0
+
+
+ def _enqueueBatchRefresh(self):
+ self.batches += 1
+
+
+ def writeCalendarResource(self, collection, resource, calendar):
+ return succeed(FakeResource())
+
+
+
+class FakePrincipal(object):
+
+ def __init__(self, cuaddr):
+ self.cuaddr = cuaddr
+
+
+ def calendarUserAddresses(self):
+ return (self.cuaddr,)
+
+
+
+class FakeResource(object):
+
+ def parentCollection(self):
+ return self
+
+
+ def ownerHome(self):
+ return self
+
+
+ def uid(self):
+ return None
+
+
+ def id(self):
+ return None
+
+
+
+class BatchRefresh (unittest.TestCase):
+ """
+ iCalendar support tests
+ """
+
+ def setUp(self):
+ super(BatchRefresh, self).setUp()
+ config.Memcached.Pools.Default.ClientEnabled = False
+ config.Memcached.Pools.Default.ServerEnabled = False
+ memcacher.Memcacher.allowTestCache = True
+ memcacher.Memcacher.memoryCacheInstance = None
+
+
+ @inlineCallbacks
+ def test_queueAttendeeUpdate_no_refresh(self):
+
+ self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
+
+ calendar = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+ processor = FakeImplicitProcessor()
+ processor.txn = ""
+ processor.uid = "12345-67890"
+ processor.recipient_calendar = calendar
+ yield processor.queueAttendeeUpdate(("urn:uuid:user02", "urn:uuid:user01",))
+ self.assertEqual(processor.batches, 0)
+
+
+ @inlineCallbacks
+ def test_queueAttendeeUpdate_with_refresh(self):
+
+ self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
+
+ calendar = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+END:VEVENT
+END:VCALENDAR
+""")
+ processor = FakeImplicitProcessor()
+ processor.txn = ""
+ processor.uid = "12345-67890"
+ processor.recipient_calendar = calendar
+ yield processor.queueAttendeeUpdate(("urn:uuid:user02", "urn:uuid:user01",))
+ self.assertEqual(processor.batches, 1)
+
+
+ @inlineCallbacks
+ def test_queueAttendeeUpdate_count_suppressed(self):
+
+ self.patch(config.Scheduling.Options, "AttendeeRefreshCountLimit", 5)
+ self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
+
+ calendar_small = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+ATTENDEE:urn:uuid:user04
+END:VEVENT
+END:VCALENDAR
+""")
+ itip_small = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE;PARTSTAT="ACCEPTED":urn:uuid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+ calendar_large = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+ATTENDEE:urn:uuid:user04
+ATTENDEE:urn:uuid:user05
+ATTENDEE:urn:uuid:user06
+ATTENDEE:urn:uuid:user07
+ATTENDEE:urn:uuid:user08
+ATTENDEE:urn:uuid:user09
+ATTENDEE:urn:uuid:user10
+END:VEVENT
+END:VCALENDAR
+""")
+ itip_large = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE;PARTSTAT="ACCEPTED":urn:uuid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+ for count, calendar, itip, result, msg in (
+ (5, calendar_small, itip_small, 1, "Small, count=5"),
+ (5, calendar_large, itip_large, 0, "Large, count=5"),
+ (0, calendar_small, itip_small, 1, "Small, count=0"),
+ (0, calendar_large, itip_large, 1, "Large, count=0"),
+ ):
+ config.Scheduling.Options.AttendeeRefreshCountLimit = count
+ processor = FakeImplicitProcessor()
+ processor.txn = ""
+ processor.recipient_calendar = calendar.duplicate()
+ processor.uid = processor.recipient_calendar.newUID()
+ processor.recipient_calendar_resource = None
+ processor.message = itip.duplicate()
+ processor.message.newUID(processor.uid)
+ processor.originator = LocalCalendarUser(None, None)
+ processor.recipient = LocalCalendarUser(None, None)
+ processor.uid = calendar.resourceUID()
+ processor.noAttendeeRefresh = False
+
+ processed = yield processor.doImplicitOrganizerUpdate()
+ self.assertTrue(processed[3] is not None, msg=msg)
+ self.assertEqual(processor.batches, result, msg=msg)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140601/f4d48f00/attachment-0001.html>
More information about the calendarserver-changes
mailing list