[CalendarServer-changes] [11579] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon Aug 5 10:55:30 PDT 2013


Revision: 11579
          http://trac.calendarserver.org//changeset/11579
Author:   cdaboo at apple.com
Date:     2013-08-05 10:55:30 -0700 (Mon, 05 Aug 2013)
Log Message:
-----------
Make sure implicit component splitting sends scheduling messages for non-hosted attendees. Also found and fixed
a bunch of iSchedule bugs introduced by scheduling-in-the-store changes.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/delivery.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/dkim.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/resource.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/scheduler.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_dkim.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_localservers.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/trunk/txdav/caldav/icalendarstore.py

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -30,6 +30,7 @@
 
 import cStringIO as StringIO
 import codecs
+from difflib import unified_diff
 import heapq
 import itertools
 import uuid
@@ -950,12 +951,17 @@
         Remove a property from this component.
         @param property: the L{Property} to remove from this component.
         """
-        self._pycalendar.removeProperty(property._pycalendar)
-        self._pycalendar.finalise()
-        property._parent = None
-        self._markAsDirty()
 
+        if isinstance(property, str):
+            for property in self.properties(property):
+                self.removeProperty(property)
+        else:
+            self._pycalendar.removeProperty(property._pycalendar)
+            self._pycalendar.finalise()
+            property._parent = None
+            self._markAsDirty()
 
+
     def removeAllPropertiesWithName(self, pname):
         """
         Remove all properties with the given name from all components.
@@ -3512,3 +3518,23 @@
             break
         else:
             heapq.heappop(heap)
+
+
+
+def normalize_iCalStr(icalstr):
+    """
+    Normalize a string representation of ical data for easy test comparison.
+    """
+
+    icalstr = str(icalstr).replace("\r\n ", "")
+    icalstr = icalstr.replace("\n ", "")
+    icalstr = "\r\n".join([line for line in icalstr.splitlines() if not line.startswith("DTSTAMP")])
+    return icalstr
+
+
+
+def diff_iCalStrs(icalstr1, icalstr2):
+
+    icalstr1 = normalize_iCalStr(icalstr1).splitlines()
+    icalstr2 = normalize_iCalStr(icalstr2).splitlines()
+    return "\n".join(unified_diff(icalstr1, icalstr2))

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -113,7 +113,7 @@
         return rid
 
 
-    def split(self, ical, rid=None, newUID=None):
+    def split(self, ical, rid=None, olderUID=None):
         """
         Split the specified iCalendar object. This assumes that L{willSplit} has already
         been called and returned C{True}. Splitting is done by carving out old instances
@@ -126,11 +126,11 @@
         @param rid: recurrence-id where the split should occur, or C{None} to determine it here
         @type rid: L{PyCalendarDateTime} or C{None}
 
-        @param newUID: UID to use for the split off component, or C{None} to generate one here
-        @type newUID: C{str} or C{None}
+        @param olderUID: UID to use for the split off component, or C{None} to generate one here
+        @type olderUID: C{str} or C{None}
 
-        @return: iCalendar object for the old "carved out" instances
-        @rtype: L{Component}
+        @return: iCalendar objects for the old and new "carved out" instances
+        @rtype: C{tuple} of two L{Component}'s
         """
 
         # Find the instance RECURRENCE-ID where a split is going to happen
@@ -138,16 +138,17 @@
 
         # Create the old one with a new UID value (or the one passed in)
         icalOld = ical.duplicate()
-        oldUID = icalOld.newUID(newUID=newUID)
+        oldUID = icalOld.newUID(newUID=olderUID)
         icalOld.onlyPastInstances(rid)
 
         # Adjust the current one
-        ical.onlyFutureInstances(rid)
+        icalNew = ical.duplicate()
+        icalNew.onlyFutureInstances(rid)
 
         # Relate them - add RELATED-TO;RELTYPE=RECURRENCE-SET if not already present
         if not icalOld.hasPropertyWithParameterMatch("RELATED-TO", "RELTYPE", "X-CALENDARSERVER-RECURRENCE-SET"):
             property = Property("RELATED-TO", oldUID, params={"RELTYPE": "X-CALENDARSERVER-RECURRENCE-SET"})
             icalOld.addPropertyToAllComponents(property)
-            ical.addPropertyToAllComponents(property)
+            icalNew.addPropertyToAllComponents(property)
 
-        return icalOld
+        return (icalOld, icalNew,)

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -63,6 +63,8 @@
         self.allowed_to_schedule = True
         self.suppress_refresh = False
 
+        self.split_details = None
+
     NotAllowedExceptionDetails = collections.namedtuple("NotAllowedExceptionDetails", ("type", "args", "kwargs",))
 
     def setSchedulingNotAllowed(self, ex, *ex_args, **ex_kwargs):
@@ -301,7 +303,7 @@
 
 
     @inlineCallbacks
-    def doImplicitScheduling(self, do_smart_merge=False):
+    def doImplicitScheduling(self, do_smart_merge=False, split_details=None):
         """
         Do implicit scheduling operation based on the data already set by call to checkImplicitScheduling.
 
@@ -315,6 +317,7 @@
         self.do_smart_merge = do_smart_merge
         self.except_attendees = ()
         self.only_refresh_attendees = None
+        self.split_details = split_details
 
         # Determine what type of scheduling this is: Organizer triggered or Attendee triggered
         if self.state == "organizer":
@@ -561,25 +564,29 @@
                     log.debug("Implicit - organizer '%s' is modifying UID: '%s' but change is not significant" % (self.organizer, self.uid))
                     returnValue(None)
             else:
-                log.debug("Implicit - organizer '%s' is modifying UID: '%s'" % (self.organizer, self.uid))
+                # Do not change PARTSTATs for a split operation
+                if self.split_details is None:
+                    log.debug("Implicit - organizer '%s' is modifying UID: '%s'" % (self.organizer, self.uid))
 
-                for rid in self.needs_action_rids:
-                    comp = self.calendar.overriddenComponent(rid)
-                    if comp is None:
-                        comp = self.calendar.deriveInstance(rid)
-                        self.calendar.addComponent(comp)
+                    for rid in self.needs_action_rids:
+                        comp = self.calendar.overriddenComponent(rid)
+                        if comp is None:
+                            comp = self.calendar.deriveInstance(rid)
+                            self.calendar.addComponent(comp)
 
-                    for attendee in comp.getAllAttendeeProperties():
-                        if attendee.hasParameter("PARTSTAT"):
-                            cuaddr = attendee.value()
+                        for attendee in comp.getAllAttendeeProperties():
+                            if attendee.hasParameter("PARTSTAT"):
+                                cuaddr = attendee.value()
 
-                            if cuaddr in self.organizerPrincipal.calendarUserAddresses:
-                                # If the attendee is the organizer then do not update
-                                # the PARTSTAT to NEEDS-ACTION.
-                                # The organizer is automatically ACCEPTED to the event.
-                                continue
+                                if cuaddr in self.organizerPrincipal.calendarUserAddresses:
+                                    # If the attendee is the organizer then do not update
+                                    # the PARTSTAT to NEEDS-ACTION.
+                                    # The organizer is automatically ACCEPTED to the event.
+                                    continue
 
-                            attendee.setParameter("PARTSTAT", "NEEDS-ACTION")
+                                attendee.setParameter("PARTSTAT", "NEEDS-ACTION")
+                else:
+                    log.debug("Implicit - organizer '%s' is splitting UID: '%s'" % (self.organizer, self.uid))
 
                 # Check for removed attendees
                 if not recurrence_reschedule:
@@ -592,8 +599,12 @@
                 self.needs_sequence_change = self.calendar.needsiTIPSequenceChange(self.oldcalendar)
 
         elif self.action == "create":
-            log.debug("Implicit - organizer '%s' is creating UID: '%s'" % (self.organizer, self.uid))
-            self.coerceAttendeesPartstatOnCreate()
+            if self.split_details is None:
+                log.debug("Implicit - organizer '%s' is creating UID: '%s'" % (self.organizer, self.uid))
+                self.coerceAttendeesPartstatOnCreate()
+            else:
+                log.debug("Implicit - organizer '%s' is creating a split UID: '%s'" % (self.organizer, self.uid))
+                self.needs_sequence_change = False
 
         # Always set RSVP=TRUE for any NEEDS-ACTION
         for attendee in self.calendar.getAllAttendeeProperties():
@@ -932,6 +943,13 @@
             if attendee in self.organizerPrincipal.calendarUserAddresses:
                 continue
 
+            # Handle split by not scheduling local attendees
+            if self.split_details is not None:
+                attendeePrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
+                attendeeAddress = (yield addressmapping.mapper.getCalendarUser(attendee, attendeePrincipal))
+                if type(attendeeAddress) is LocalCalendarUser:
+                    continue
+
             # Generate an iTIP CANCEL message for this attendee, cancelling
             # each instance or the whole
 
@@ -944,6 +962,13 @@
 
             # Send scheduling message
             if itipmsg:
+
+                # Add split details if needed
+                if self.split_details is not None:
+                    rid, uid, newer_piece = self.split_details
+                    itipmsg.addProperty(Property("X-CALENDARSERVER-SPLIT-RID", rid))
+                    itipmsg.addProperty(Property("X-CALENDARSERVER-SPLIT-OLDER-UID" if newer_piece else "X-CALENDARSERVER-SPLIT-NEWER-UID", uid))
+
                 # This is a local CALDAV scheduling operation.
                 scheduler = self.makeScheduler()
 
@@ -983,10 +1008,24 @@
             if self.reinvites and attendee not in self.reinvites:
                 continue
 
+            # Handle split by not scheduling local attendees
+            if self.split_details is not None:
+                attendeePrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
+                attendeeAddress = (yield addressmapping.mapper.getCalendarUser(attendee, attendeePrincipal))
+                if type(attendeeAddress) is LocalCalendarUser:
+                    continue
+
             itipmsg = iTipGenerator.generateAttendeeRequest(self.calendar, (attendee,), self.changed_rids)
 
             # Send scheduling message
             if itipmsg is not None:
+
+                # Add split details if needed
+                if self.split_details is not None:
+                    rid, uid, newer_piece = self.split_details
+                    itipmsg.addProperty(Property("X-CALENDARSERVER-SPLIT-RID", rid))
+                    itipmsg.addProperty(Property("X-CALENDARSERVER-SPLIT-OLDER-UID" if newer_piece else "X-CALENDARSERVER-SPLIT-NEWER-UID", uid))
+
                 # This is a local CALDAV scheduling operation.
                 scheduler = self.makeScheduler()
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/delivery.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/delivery.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/delivery.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -38,6 +38,8 @@
 from twistedcaldav.client.pool import _configuredClientContextFactory
 from twistedcaldav.config import config
 from twistedcaldav.ical import normalizeCUAddress, Component
+from twistedcaldav.util import utf8String
+
 from txdav.caldav.datastore.scheduling.cuaddress import PartitionedCalendarUser, RemoteCalendarUser, \
     OtherServerCalendarUser
 from txdav.caldav.datastore.scheduling.delivery import DeliveryService
@@ -49,7 +51,7 @@
     RequestStatus, Recipient, ischedule_namespace, CalendarData, \
     ResponseDescription, Error
 from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.util import utf8String, normalizationLookup
+from txdav.caldav.datastore.util import normalizationLookup
 
 from urlparse import urlsplit
 
@@ -226,11 +228,10 @@
 
         # Generate an HTTP client request
         try:
-            if not hasattr(self.scheduler.request, "extendedLogItems"):
-                self.scheduler.request.extendedLogItems = {}
-            if "itip.ischedule" not in self.scheduler.request.extendedLogItems:
-                self.scheduler.request.extendedLogItems["itip.ischedule"] = 0
-            self.scheduler.request.extendedLogItems["itip.ischedule"] += 1
+            if self.scheduler.logItems is not None:
+                if "itip.ischedule" not in self.scheduler.logItems:
+                    self.scheduler.logItems["itip.ischedule"] = 0
+                self.scheduler.logItems["itip.ischedule"] += 1
 
             # Loop over at most 3 redirects
             ssl, host, port, path = self.server.details()
@@ -363,7 +364,7 @@
 
         # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
         originator = self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee
-        originator = normalizeCUAddress(originator, normalizationLookup, self.scheduler.resource.principalForCalendarUserAddress, toUUID=False)
+        originator = normalizeCUAddress(originator, normalizationLookup, self.scheduler.txn.directoryService().recordWithCalendarUserAddress, toUUID=False)
         self.headers.addRawHeader("Originator", utf8String(originator))
         self.sign_headers.append("Originator")
 
@@ -417,7 +418,7 @@
                 normalizedCalendar = self.scheduler.calendar.duplicate()
                 normalizedCalendar.normalizeCalendarUserAddresses(
                     normalizationLookup,
-                    self.scheduler.resource.principalForCalendarUserAddress,
+                    self.scheduler.txn.directoryService().recordWithCalendarUserAddress,
                     toUUID=False)
             else:
                 normalizedCalendar = self.scheduler.calendar

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/dkim.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/dkim.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/dkim.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -513,12 +513,15 @@
     Class used to verify an DKIM-signed HTTP request.
     """
 
-    def __init__(self, request, key_lookup=None, protocol_debug=False):
+    def __init__(self, headers, body, key_lookup=None, protocol_debug=False):
         """
-        @param request: The HTTP request to process
-        @type request: L{twext.server.Request}
+        @param headers: The HTTP request headers to process
+        @type headers: L{twext.web2.http_headers.Headers}
+        @param body: The HTTP request body to process
+        @type body: C{str}
         """
-        self.request = request
+        self.headers = headers
+        self.body = body
         self._debug = protocol_debug
         self.dkim_tags = {}
 
@@ -563,17 +566,14 @@
 
 Public key used:
 %s
-""" % (self.request.headers.getRawHeaders(DKIM_SIGNATURE)[0], headers, pubkey._original_data,)
+""" % (self.headers.getRawHeaders(DKIM_SIGNATURE)[0], headers, pubkey._original_data,)
             log.debug("DKIM: %s:%s" % (msg, _debug_msg,))
             if self._debug:
                 msg = "%s:%s" % (msg, _debug_msg,)
             raise DKIMVerificationError(msg)
 
         # Do body validation
-        data = (yield allDataFromStream(self.request.stream))
-        self.request.stream = MemoryStream(data if data is not None else "")
-        self.request.stream.doStartReading = None
-        body = DKIMUtils.canonicalizeBody(data)
+        body = DKIMUtils.canonicalizeBody(self.body)
         bh = base64.b64encode(self.hash_method(body).digest())
         if bh != self.dkim_tags["_bh"]:
             msg = "Could not verify the DKIM body hash"
@@ -584,7 +584,7 @@
 
 Base64 encoded body:
 %s
-""" % (self.request.headers.getRawHeaders(DKIM_SIGNATURE), self.hash_method.__name__, base64.b64encode(body),)
+""" % (self.headers.getRawHeaders(DKIM_SIGNATURE), self.hash_method.__name__, base64.b64encode(body),)
             log.debug("DKIM: %s:%s" % (msg, _debug_msg,))
             if self._debug:
                 msg = "%s:%s" % (msg, _debug_msg,)
@@ -599,7 +599,7 @@
         """
 
         # Check presence of header
-        dkim = self.request.headers.getRawHeaders(DKIM_SIGNATURE)
+        dkim = self.headers.getRawHeaders(DKIM_SIGNATURE)
         if dkim is None:
             msg = "No DKIM-Signature header present in the request"
             log.debug("DKIM: " + msg)
@@ -683,12 +683,12 @@
 
         headers = []
         for header in header_list:
-            actual_headers = self.request.headers.getRawHeaders(header)
+            actual_headers = self.headers.getRawHeaders(header)
             if actual_headers:
                 headers.append((header, ",".join(actual_headers),))
 
         # DKIM-Signature is always included at the end
-        headers.append((DKIM_SIGNATURE, self.request.headers.getRawHeaders(DKIM_SIGNATURE)[0],))
+        headers.append((DKIM_SIGNATURE, self.headers.getRawHeaders(DKIM_SIGNATURE)[0],))
 
         # Now canonicalize the values
         return "".join([DKIMUtils.canonicalizeHeader(name, value, dkim_tags=self.dkim_tags) for name, value in headers])

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -193,10 +193,10 @@
         return ip in self.allowed_from_ips
 
 
-    def checkSharedSecret(self, request):
+    def checkSharedSecret(self, headers):
 
         # Get header from the request
-        request_secret = request.headers.getRawHeaders(SERVER_SECRET_HEADER)
+        request_secret = headers.getRawHeaders(SERVER_SECRET_HEADER)
 
         if request_secret is not None and self.shared_secret is None:
             log.error("iSchedule request included unexpected %s header" % (SERVER_SECRET_HEADER,))

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/resource.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/resource.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -20,6 +20,7 @@
 from twext.web2 import responsecode
 from twext.web2.dav.http import ErrorResponse
 from twext.web2.dav.noneprops import NonePropertyStore
+from twext.web2.dav.util import allDataFromStream
 from twext.web2.http import Response, HTTPError, StatusResponse, XMLResponse
 from twext.web2.http_headers import MimeType
 
@@ -223,11 +224,11 @@
 
         originator = self.loadOriginatorFromRequestHeaders(request)
         recipients = self.loadRecipientsFromRequestHeaders(request)
-        calendar = (yield self.loadCalendarFromRequest(request))
+        body = (yield allDataFromStream(request.stream))
 
         # Do the POST processing treating this as a non-local schedule
         try:
-            result = (yield scheduler.doSchedulingViaPOST(request, originator, recipients, calendar))
+            result = (yield scheduler.doSchedulingViaPOST(request.remoteAddr, request.headers, body, originator, recipients))
         except Exception:
             ex = Failure()
             yield txn.abort()

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/scheduler.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/scheduler.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/scheduler.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -23,7 +23,7 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 from twistedcaldav.config import config
-from twistedcaldav.ical import normalizeCUAddress
+from twistedcaldav.ical import normalizeCUAddress, Component
 
 from txdav.caldav.datastore.scheduling import addressmapping
 from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser
@@ -139,15 +139,17 @@
     }
 
     @inlineCallbacks
-    def doSchedulingViaPOST(self, request, originator, recipients, calendar):
+    def doSchedulingViaPOST(self, remoteAddr, headers, body, originator, recipients):
         """
         Carry out iSchedule specific processing.
         """
 
-        self.request = request
+        self.remoteAddr = remoteAddr
+        self.headers = headers
         self.verified = False
+
         if config.Scheduling.iSchedule.DKIM.Enabled:
-            verifier = DKIMVerifier(self.request, protocol_debug=config.Scheduling.iSchedule.DKIM.ProtocolDebug)
+            verifier = DKIMVerifier(self.headers, body, protocol_debug=config.Scheduling.iSchedule.DKIM.ProtocolDebug)
             try:
                 yield verifier.verify()
                 self.verified = True
@@ -170,7 +172,9 @@
                     msg,
                 ))
 
-        if self.request.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
+        calendar = Component.fromString(body)
+
+        if self.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
             self.txn.doing_attendee_refresh = 1
 
         # Normalize recipient addresses
@@ -193,15 +197,6 @@
             self.calendar.normalizeCalendarUserAddresses(normalizationLookup, self.txn.directoryService().recordWithCalendarUserAddress)
 
 
-    def loadRecipientsFromRequestHeaders(self):
-        """
-        Need to normalize the calendar data and recipient values to keep those in sync,
-        as we might later try to match them
-        """
-        super(IScheduleScheduler, self).loadRecipientsFromRequestHeaders()
-        self.recipients = [normalizeCUAddress(recipient, normalizationLookup, self.txn.directoryService().recordWithCalendarUserAddress) for recipient in self.recipients]
-
-
     def checkAuthorization(self):
         # Must have an unauthenticated user
         if self.originator_uid is not None:
@@ -259,7 +254,7 @@
             ))
         else:
             # Get the request IP and map to hostname.
-            clientip = self.request.remoteAddr.host
+            clientip = self.remoteAddr.host
 
             # First compare as dotted IP
             matched = False
@@ -312,7 +307,7 @@
         expected_uri = urlparse.urlparse(expected_uri)
 
         # Get the request IP and map to hostname.
-        clientip = self.request.remoteAddr.host
+        clientip = self.remoteAddr.host
 
         # Check against this server (or any of its partitions). We need this because an external iTIP message
         # may be addressed to users on different partitions, and the node receiving the iTIP message will need to
@@ -345,7 +340,7 @@
                 log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
 
         # Check possible shared secret
-        if matched and not Servers.getThisServer().checkSharedSecret(self.request):
+        if matched and not Servers.getThisServer().checkSharedSecret(self.headers):
             log.error("Invalid iSchedule shared secret")
             matched = False
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_dkim.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_dkim.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_dkim.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -16,6 +16,7 @@
 
 from Crypto.PublicKey import RSA
 
+from twext.web2.dav.util import allDataFromStream
 from twext.web2.http_headers import Headers, MimeType
 from twext.web2.stream import MemoryStream
 
@@ -244,6 +245,13 @@
             self.stream = MemoryStream(body)
 
 
+    def _makeHeaders(self, headers_pairs):
+        headers = Headers()
+        for name, value in headers_pairs:
+            headers.addRawHeader(name, value)
+        return headers
+
+
     def test_valid_dkim_headers(self):
         """
         L{DKIMVerifier.processDKIMHeader} correctly validates DKIM-Signature headers.
@@ -276,8 +284,7 @@
         )
 
         for headers, result in data:
-            request = self.StubRequest("POST", "/", headers, "")
-            verifier = DKIMVerifier(request)
+            verifier = DKIMVerifier(self._makeHeaders(headers), "")
             if result:
                 verifier.processDKIMHeader()
             else:
@@ -307,8 +314,7 @@
         )
 
         for name, value, result in data:
-            request = self.StubRequest("POST", "/", ((name, value,),), "")
-            verifier = DKIMVerifier(request)
+            verifier = DKIMVerifier(self._makeHeaders(((name, value,),)), "")
             if name == "DKIM-Signature":
                 verifier.processDKIMHeader()
             canonicalized = DKIMUtils.canonicalizeHeader(name, value, verifier.dkim_tags if name == "DKIM-Signature" else None)
@@ -374,8 +380,7 @@
 
         for hdrs, result in data:
             headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
-            request = self.StubRequest("POST", "/", headers, "")
-            verifier = DKIMVerifier(request)
+            verifier = DKIMVerifier(self._makeHeaders(headers), "")
             verifier.processDKIMHeader()
             extracted = verifier.extractSignedHeaders()
             self.assertEqual(extracted, result.replace("\n", "\r\n"))
@@ -427,9 +432,8 @@
 
         for hdrs, keys, result in data:
             headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
-            request = self.StubRequest("POST", "/", headers, "")
             TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
-            verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
+            verifier = DKIMVerifier(self._makeHeaders(headers), "", key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
             verifier.processDKIMHeader()
             pkey = (yield verifier.locatePublicKey())
             if result:
@@ -461,7 +465,8 @@
 
                 # Verify signature
                 TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
-                verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
+                data = (yield allDataFromStream(request.stream))
+                verifier = DKIMVerifier(request.headers, data, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
                 TestPublicKeyLookup.PublicKeyLookup_Testing({}).flushCache()
                 try:
                     yield verifier.verify()

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_localservers.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_localservers.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_localservers.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -167,22 +167,22 @@
 
         request = SimpleRequest(None, "POST", "/ischedule")
         request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
-        self.assertTrue(servers.getServerById("00001").checkSharedSecret(request))
+        self.assertTrue(servers.getServerById("00001").checkSharedSecret(request.headers))
 
         request = SimpleRequest(None, "POST", "/ischedule")
         request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
-        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
+        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request.headers))
 
         request = SimpleRequest(None, "POST", "/ischedule")
-        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
+        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request.headers))
 
         request = SimpleRequest(None, "POST", "/ischedule")
         request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
-        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
+        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request.headers))
 
         request = SimpleRequest(None, "POST", "/ischedule")
         request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
-        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
+        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request.headers))
 
         request = SimpleRequest(None, "POST", "/ischedule")
-        self.assertTrue(servers.getServerById("00002").checkSharedSecret(request))
+        self.assertTrue(servers.getServerById("00002").checkSharedSecret(request.headers))

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -462,6 +462,26 @@
                 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"):
+            if config.Scheduling.Options.Splitting.Enabled:
+                # Tell the existing resource to split
+                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' splitting UID: '%s'" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+                split = (yield self.doImplicitAttendeeSplit())
+                if split:
+                    returnValue((True, False, False, None,))
+            else:
+                self.message.removeProperty("X-CALENDARSERVER-SPLIT-OLDER-UID")
+                self.message.removeProperty("X-CALENDARSERVER-SPLIT-RID")
+
+        elif self.message.hasProperty("X-CALENDARSERVER-SPLIT-NEWER-UID"):
+            if config.Scheduling.Options.Splitting.Enabled:
+                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - split already done" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+                returnValue((True, False, False, None,))
+            else:
+                self.message.removeProperty("X-CALENDARSERVER-SPLIT-OLDER-UID")
+                self.message.removeProperty("X-CALENDARSERVER-SPLIT-RID")
+
         # Different based on method
         if self.method == "REQUEST":
             result = (yield self.doImplicitAttendeeRequest())
@@ -478,6 +498,22 @@
 
 
     @inlineCallbacks
+    def doImplicitAttendeeSplit(self):
+        """
+        Handle splitting of the existing calendar data.
+        """
+        olderUID = self.message.propertyValue("X-CALENDARSERVER-SPLIT-OLDER-UID")
+        split_rid = self.message.propertyValue("X-CALENDARSERVER-SPLIT-RID")
+        if olderUID is None or split_rid is None:
+            returnValue(False)
+
+        # Split the resource
+        yield self.recipient_calendar_resource.splitForAttendee(rid=split_rid, olderUID=olderUID)
+
+        returnValue(True)
+
+
+    @inlineCallbacks
     def doImplicitAttendeeRequest(self):
         """
         @return: C{tuple} of (processed, auto-processed, store inbox item, changes)

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -2009,12 +2009,12 @@
             splitter = iCalSplitter(1024, 14)
             if title[0] == "1":
                 self.assertTrue(splitter.willSplit(ical), "Failed will split: %s" % (title,))
-            icalOld = splitter.split(ical)
+            icalOld, icalNew = splitter.split(ical)
             relsubs = dict(self.subs)
             relsubs["relID"] = icalOld.resourceUID()
-            self.assertEqual(str(ical).replace("\r\n ", ""), split_future.replace("\n", "\r\n") % relsubs, "Failed future: %s" % (title,))
+            self.assertEqual(str(icalNew).replace("\r\n ", ""), split_future.replace("\n", "\r\n") % relsubs, "Failed future: %s" % (title,))
             self.assertEqual(str(icalOld).replace("\r\n ", ""), split_past.replace("\n", "\r\n") % relsubs, "Failed past: %s" % (title,))
 
             # Make sure new items won't split again
-            self.assertFalse(splitter.willSplit(ical), "Failed future will split: %s" % (title,))
+            self.assertFalse(splitter.willSplit(icalNew), "Failed future will split: %s" % (title,))
             self.assertFalse(splitter.willSplit(icalOld), "Failed past will split: %s" % (title,))

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -1047,7 +1047,7 @@
 
 
     @inlineCallbacks
-    def _createCalendarObjectWithNameInternal(self, name, component, internal_state, options=None):
+    def _createCalendarObjectWithNameInternal(self, name, component, internal_state, options=None, split_details=None):
 
         # Create => a new resource name
         if name in self._objects and self._objects[name]:
@@ -1060,7 +1060,7 @@
                 raise TooManyObjectResourcesError()
 
         objectResource = (
-            yield self._objectResourceClass._createInternal(self, name, component, internal_state, options)
+            yield self._objectResourceClass._createInternal(self, name, component, internal_state, options, split_details)
         )
         self._objects[objectResource.name()] = objectResource
         self._objects[objectResource.uid()] = objectResource
@@ -1414,7 +1414,7 @@
 
 
     @classproperty
-    def _moveTimeRangeUpdateQuery(cls):  #@NoSelf
+    def _moveTimeRangeUpdateQuery(cls): #@NoSelf
         """
         DAL query to update a child to be in a new parent.
         """
@@ -1509,7 +1509,7 @@
 
     @classmethod
     @inlineCallbacks
-    def _createInternal(cls, parent, name, component, internal_state, options=None):
+    def _createInternal(cls, parent, name, component, internal_state, options=None, split_details=None):
 
         child = (yield cls.objectWithName(parent, name, None))
         if child:
@@ -1519,7 +1519,7 @@
             raise ObjectResourceNameNotAllowedError(name)
 
         objectResource = cls(parent, name, None, None, options=options)
-        yield objectResource._setComponentInternal(component, inserting=True, internal_state=internal_state)
+        yield objectResource._setComponentInternal(component, inserting=True, internal_state=internal_state, split_details=split_details)
         yield objectResource._loadPropertyStore(created=True)
 
         # Note: setComponent triggers a notification, so we don't need to
@@ -1931,17 +1931,28 @@
 
 
     @inlineCallbacks
-    def doImplicitScheduling(self, component, inserting, internal_state):
+    def doImplicitScheduling(self, component, inserting, internal_state, split_details=None):
 
         new_component = None
         did_implicit_action = False
         is_scheduling_resource = False
         schedule_state = None
 
-        is_internal = internal_state not in (ComponentUpdateState.NORMAL, ComponentUpdateState.ATTACHMENT_UPDATE,)
+        is_internal = internal_state not in (
+            ComponentUpdateState.NORMAL,
+            ComponentUpdateState.ATTACHMENT_UPDATE,
+            ComponentUpdateState.SPLIT_OWNER,
+        )
 
         # Do scheduling
         if not self.calendar().isInbox():
+            # For splitting we are passed a "raw" component - one with the per-user data pieces in it.
+            # We need to filter that down just to the owner's view to do scheduling, but still ensure the
+            # raw component is written out.
+            if split_details is not None:
+                user_uuid = self._parentCollection.viewerHome().uid()
+                component = PerUserDataFilter(user_uuid).filter(component.duplicate())
+
             scheduler = ImplicitScheduler()
 
             # PUT
@@ -1961,7 +1972,7 @@
                         "Sharee's cannot schedule",
                     )
 
-                new_calendar = (yield scheduler.doImplicitScheduling(self.schedule_tag_match))
+                new_calendar = (yield scheduler.doImplicitScheduling(self.schedule_tag_match, split_details))
                 if new_calendar:
                     if isinstance(new_calendar, int):
                         returnValue(new_calendar)
@@ -2076,7 +2087,7 @@
 
 
     @inlineCallbacks
-    def _setComponentInternal(self, component, inserting=False, internal_state=ComponentUpdateState.NORMAL, smart_merge=False):
+    def _setComponentInternal(self, component, inserting=False, internal_state=ComponentUpdateState.NORMAL, smart_merge=False, split_details=None):
         """
         Setting the component internally to the store itself. This will bypass a whole bunch of data consistency checks
         on the assumption that those have been done prior to the component data being provided, provided the flag is set.
@@ -2087,9 +2098,9 @@
         self.schedule_tag_match = not self.calendar().isInbox() and internal_state == ComponentUpdateState.NORMAL and smart_merge
         schedule_state = None
 
-        if internal_state == ComponentUpdateState.SPLIT:
+        if internal_state in (ComponentUpdateState.SPLIT_OWNER, ComponentUpdateState.SPLIT_ATTENDEE,):
             # When splitting, some state from the previous resource needs to be properly
-            # preserved in thus new one when storing the component. Since we don't do the "full"
+            # preserved in the new one when storing the component. Since we don't do the "full"
             # store here, we need to add the explicit pieces we need for state preservation.
 
             # Check access
@@ -2101,6 +2112,10 @@
 
             managed_copied, managed_removed = (yield self.resourceCheckAttachments(component, inserting))
 
+            # Do scheduling only for owner split
+            if internal_state == ComponentUpdateState.SPLIT_OWNER:
+                yield self.doImplicitScheduling(component, inserting, internal_state, split_details)
+
             self.isScheduleObject = True
             self.processScheduleTags(component, inserting, internal_state)
 
@@ -2165,7 +2180,11 @@
         yield self.updateDatabase(component, inserting=inserting)
 
         # Post process managed attachments
-        if internal_state in (ComponentUpdateState.NORMAL, ComponentUpdateState.SPLIT):
+        if internal_state in (
+            ComponentUpdateState.NORMAL,
+            ComponentUpdateState.SPLIT_OWNER,
+            ComponentUpdateState.SPLIT_ATTENDEE,
+        ):
             if managed_copied:
                 yield self.copyResourceAttachments(managed_copied)
             if managed_removed:
@@ -2179,7 +2198,7 @@
         yield self._calendar.notifyChanged()
 
         # Finally check if a split is needed
-        if internal_state != ComponentUpdateState.SPLIT and schedule_state == "organizer":
+        if internal_state not in (ComponentUpdateState.SPLIT_OWNER, ComponentUpdateState.SPLIT_ATTENDEE,) and schedule_state == "organizer":
             yield self.checkSplit()
 
         returnValue(self._componentChanged)
@@ -2613,7 +2632,7 @@
 
 
     @classproperty
-    def _recurrenceMinMaxByIDQuery(cls):  #@NoSelf
+    def _recurrenceMinMaxByIDQuery(cls): #@NoSelf
         """
         DAL query to load RECURRANCE_MIN, RECURRANCE_MAX via an object's resource ID.
         """
@@ -2647,7 +2666,7 @@
 
 
     @classproperty
-    def _instanceQuery(cls):  #@NoSelf
+    def _instanceQuery(cls): #@NoSelf
         """
         DAL query to load TIME_RANGE data via an object's resource ID.
         """
@@ -3282,59 +3301,92 @@
 
 
     @inlineCallbacks
-    def split(self):
+    def split(self, onlyThis=False, rid=None, olderUID=None):
         """
         Split this and all matching UID calendar objects as per L{iCalSplitter}.
+
+        We need to handle scheduling with non-hosted users here. Here is what we will do:
+
+        1) Send an iTIP message for the original event (in its now future-truncated state) and
+        include a special X- parameter in the iTIP message to indicate a split was done and
+        what the RECURRENCE-ID was where the split was made. This will allow "smart" clients/servers
+        to spot the split action and apply that locally upon receipt and processing of the iTIP
+        message. That way they get to preserve the existing per-user data for the old instances. Other
+        clients/servers will just apply the change via normal iTIP processing.
+
+        2) Send an iTIP message for the new event (which will be for the old instances). "Smart"
+        clients that already got and processed the message from #1 will simply apply this on top
+        of their split copy - it should be identical, part from per-user data, so it will apply
+        cleanly. We can include an X- headers to indicate the split R-ID so "smart" clients/servers
+        can simply ignore this message.
         """
 
         # First job is to grab a UID lock on this entire series of events
         yield NamedLock.acquire(self._txn, "ImplicitUIDLock:%s" % (hashlib.md5(self._uid).hexdigest(),))
 
         # Find all other calendar objects on this server with the same UID
-        resources = (yield CalendarStoreFeatures(self._txn._store).calendarObjectsWithUID(self._txn, self._uid))
+        if onlyThis:
+            resources = ()
+        else:
+            resources = (yield CalendarStoreFeatures(self._txn._store).calendarObjectsWithUID(self._txn, self._uid))
 
         splitter = iCalSplitter(config.Scheduling.Options.Splitting.Size, config.Scheduling.Options.Splitting.PastDays)
 
         # Determine the recurrence-id of the split and create a new UID for it
         calendar = (yield self.component())
-        rid = splitter.whereSplit(calendar)
-        newUID = str(uuid.uuid4())
+        if rid is None:
+            rid = splitter.whereSplit(calendar)
+        newerUID = calendar.resourceUID()
+        if olderUID is None:
+            olderUID = str(uuid.uuid4())
 
         # Now process this resource, but do implicit scheduling for attendees not hosted on this server.
         # We need to do this before processing attendee copies.
-        calendar_old = splitter.split(calendar, rid=rid, newUID=newUID)
+        calendar_old, calendar_new = splitter.split(calendar, rid=rid, olderUID=olderUID)
+        calendar_new.bumpiTIPInfo(oldcalendar=calendar, doSequence=True)
+        calendar_old.bumpiTIPInfo(oldcalendar=None, doSequence=True)
 
+        # If the split results in nothing either resource, then there is really nothing
+        # to actually split
+        if calendar_new.mainType() is None or calendar_old.mainType() is None:
+            returnValue(None)
+
         # Store changed data
-        if calendar.mainType() is not None:
-            yield self._setComponentInternal(calendar, internal_state=ComponentUpdateState.SPLIT)
-        else:
-            yield self._removeInternal(internal_state=ComponentUpdateState.SPLIT)
-        if calendar_old.mainType() is not None:
-            yield self.calendar()._createCalendarObjectWithNameInternal("%s.ics" % (newUID,), calendar_old, ComponentUpdateState.SPLIT)
+        yield self._setComponentInternal(calendar_new, internal_state=ComponentUpdateState.SPLIT_OWNER, split_details=(rid, olderUID, True,))
+        yield self.calendar()._createCalendarObjectWithNameInternal("%s.ics" % (olderUID,), calendar_old, ComponentUpdateState.SPLIT_OWNER, split_details=(rid, newerUID, False,))
 
         # Split each one - but not this resource
         for resource in resources:
             if resource._resourceID == self._resourceID:
                 continue
-            ical = (yield resource.component())
-            ical_old = splitter.split(ical, rid=rid, newUID=newUID)
+            yield resource.splitForAttendee(rid, olderUID)
 
-            # Store changed data
-            if ical.mainType() is not None:
-                yield resource._setComponentInternal(ical, internal_state=ComponentUpdateState.SPLIT)
-            else:
-                # The split removed all components from this object - remove it
-                yield resource._removeInternal(internal_state=ComponentUpdateState.SPLIT)
+        returnValue(olderUID)
 
-            # Create a new resource and store its data (but not if the parent is "inbox", or if it is empty)
-            if not resource.calendar().isInbox() and ical_old.mainType() is not None:
-                yield resource.calendar()._createCalendarObjectWithNameInternal("%s.ics" % (newUID,), ical_old, ComponentUpdateState.SPLIT)
 
-        # TODO: scheduling currently turned off until we figure out how to properly do that
+    @inlineCallbacks
+    def splitForAttendee(self, rid=None, olderUID=None):
+        """
+        Split this attendee resource as per L{split}.
+        """
+        splitter = iCalSplitter(config.Scheduling.Options.Splitting.Size, config.Scheduling.Options.Splitting.PastDays)
+        ical = (yield self.component())
+        ical_old, ical_new = splitter.split(ical, rid=rid, olderUID=olderUID)
+        ical_new.bumpiTIPInfo(oldcalendar=ical, doSequence=True)
+        ical_old.bumpiTIPInfo(oldcalendar=None, doSequence=True)
 
-        returnValue(newUID)
+        # Store changed data
+        if ical_new.mainType() is not None:
+            yield self._setComponentInternal(ical_new, internal_state=ComponentUpdateState.SPLIT_ATTENDEE)
+        else:
+            # The split removed all components from this object - remove it
+            yield self._removeInternal(internal_state=ComponentRemoveState.INTERNAL)
 
+        # Create a new resource and store its data (but not if the parent is "inbox", or if it is empty)
+        if not self.calendar().isInbox() and ical_old.mainType() is not None:
+            yield self.calendar()._createCalendarObjectWithNameInternal("%s.ics" % (olderUID,), ical_old, ComponentUpdateState.SPLIT_ATTENDEE)
 
+
     class CalendarObjectSplitterWork(WorkItem, fromTable(schema.CALENDAR_OBJECT_SPLITTER_WORK)):
 
         group = property(lambda self: "CalendarObjectSplitterWork:%s" % (self.resourceID,))

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -13,6 +13,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
+from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser, \
+    LocalCalendarUser
+from txdav.caldav.datastore.scheduling.caldav.scheduler import CalDAVScheduler
+from txdav.caldav.datastore.scheduling.scheduler import ScheduleResponseQueue
+from twext.web2 import responsecode
+from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
 
 """
 Tests for txdav.caldav.datastore.postgres, mostly based on
@@ -28,7 +35,8 @@
 from twext.web2.stream import MemoryStream
 
 from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, DeferredList
+from twisted.internet.defer import inlineCallbacks, returnValue, DeferredList, \
+    succeed
 from twisted.internet.task import deferLater
 from twisted.trial import unittest
 
@@ -36,7 +44,7 @@
 from twistedcaldav.caldavxml import CalendarDescription
 from twistedcaldav.config import config
 from twistedcaldav.dateops import datetimeMktime
-from twistedcaldav.ical import Component
+from twistedcaldav.ical import Component, normalize_iCalStr, diff_iCalStrs
 from twistedcaldav.query import calendarqueryfilter
 
 from txdav.base.propertystore.base import PropertyName
@@ -2117,6 +2125,7 @@
 ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """
@@ -2157,6 +2166,7 @@
 ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
 END:VEVENT
 BEGIN:VEVENT
 UID:%(relID)s
@@ -2168,6 +2178,7 @@
 DTSTAMP:20051222T210507Z
 ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
 END:VEVENT
 BEGIN:VEVENT
 UID:%(relID)s
@@ -2179,6 +2190,7 @@
 DTSTAMP:20051222T210507Z
 ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """
@@ -2214,8 +2226,8 @@
         title = "temp"
         relsubs = dict(self.subs)
         relsubs["relID"] = newUID
-        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future.replace("\n", "\r\n") % relsubs, "Failed future: %s" % (title,))
-        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past.replace("\n", "\r\n") % relsubs, "Failed past: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % relsubs, "Failed past: %s" % (title,))
 
 
     @inlineCallbacks
@@ -2297,6 +2309,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY
+SEQUENCE:1
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -2312,6 +2325,7 @@
 DTSTAMP:20051222T210507Z
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """
@@ -2330,6 +2344,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -2346,6 +2361,7 @@
 DTSTAMP:20051222T210507Z
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
 END:VEVENT
 BEGIN:VEVENT
 UID:%(relID)s
@@ -2357,6 +2373,7 @@
 DTSTAMP:20051222T210507Z
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """
@@ -2376,6 +2393,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY
+SEQUENCE:1
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -2405,6 +2423,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -2421,6 +2440,7 @@
 DTSTAMP:20051222T210507Z
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
 END:VEVENT
 BEGIN:VEVENT
 UID:%(relID)s
@@ -2432,6 +2452,7 @@
 DTSTAMP:20051222T210507Z
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
 UID:%(relID)s
@@ -2459,6 +2480,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY
+SEQUENCE:1
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -2482,6 +2504,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY
+SEQUENCE:1
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -2513,6 +2536,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -2544,6 +2568,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY
+SEQUENCE:1
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -2566,6 +2591,7 @@
 DTSTAMP:20051222T210507Z
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
 UID:%(relID)s
@@ -2591,6 +2617,7 @@
 DTSTAMP:20051222T210507Z
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
 UID:12345-67890
@@ -2617,6 +2644,7 @@
 DTSTAMP:20051222T210507Z
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
 END:VEVENT
 END:VCALENDAR
 """
@@ -2663,8 +2691,8 @@
         title = "user01"
         relsubs = dict(self.subs)
         relsubs["relID"] = newUID
-        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
-        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % relsubs, "Failed past: %s" % (title,))
 
         # Get user02 data
         cal = yield self.calendarUnderTest(name="calendar", home="user02")
@@ -2684,9 +2712,9 @@
 
         # Verify user02 data
         title = "user02"
-        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future2.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
-        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past2.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
-        self.assertEqual(str(ical_inbox).replace("\r\n ", ""), data_inbox2.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed inbox: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future2) % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past2) % relsubs, "Failed past: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_inbox), normalize_iCalStr(data_inbox2) % relsubs, "Failed inbox: %s" % (title,))
 
         # Get user03 data
         cal = yield self.calendarUnderTest(name="calendar", home="user03")
@@ -2707,9 +2735,9 @@
 
         # Verify user03 data
         title = "user03"
-        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future3.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
-        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past3.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
-        self.assertEqual(str(ical_inbox).replace("\r\n ", ""), data_inbox3.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed inbox: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future3) % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past3) % relsubs, "Failed past: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_inbox), normalize_iCalStr(data_inbox3) % relsubs, "Failed inbox: %s" % (title,))
 
         # Get user04 data
         cal = yield self.calendarUnderTest(name="calendar", home="user04")
@@ -2724,7 +2752,7 @@
 
         # Verify user04 data
         title = "user04"
-        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past4.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past4) % relsubs, "Failed past: %s" % (title,))
 
         # Get user05 data
         cal = yield self.calendarUnderTest(name="calendar", home="user05")
@@ -2740,8 +2768,8 @@
 
         # Verify user05 data
         title = "user05"
-        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future5.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
-        self.assertEqual(str(ical_inbox).replace("\r\n ", ""), data_inbox5.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed inbox: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future5) % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_inbox), normalize_iCalStr(data_inbox5) % relsubs, "Failed inbox: %s" % (title,))
 
 
     @inlineCallbacks
@@ -3045,16 +3073,16 @@
         cobj = cobjs[0]
         cname2 = cobj.name()
         ical = yield cobj.component()
-        self.assertEqual(str(ical).replace("\r\n ", ""), data_2.replace("\n", "\r\n").replace("\r\n ", "") % self.subs, "Failed 2")
+        self.assertEqual(normalize_iCalStr(ical), normalize_iCalStr(data_2) % self.subs, "Failed 2")
         yield cobj.setComponent(Component.fromString(data_2_update % self.subs))
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         ical = yield cobj.component()
-        self.assertEqual(str(ical).replace("\r\n ", ""), data_1.replace("\n", "\r\n").replace("\r\n ", "") % self.subs, "Failed 2")
+        self.assertEqual(normalize_iCalStr(ical), normalize_iCalStr(data_1) % self.subs, "Failed 2")
         cobj = yield self.calendarObjectUnderTest(name=cname2, calendar_name="calendar", home="user02")
         ical = yield cobj.component()
-        self.assertEqual(str(ical).replace("\r\n ", ""), data_2_changed.replace("\n", "\r\n").replace("\r\n ", "") % self.subs, "Failed 2")
+        self.assertEqual(normalize_iCalStr(ical), normalize_iCalStr(data_2_changed) % self.subs, "Failed 2")
         yield self.commit()
 
 
@@ -3244,7 +3272,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY
-SEQUENCE:2
+SEQUENCE:3
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -3261,7 +3289,7 @@
 DTSTAMP:%(dtstamp)s
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
-SEQUENCE:2
+SEQUENCE:3
 END:VEVENT
 END:VCALENDAR
 """
@@ -3280,7 +3308,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
-SEQUENCE:2
+SEQUENCE:3
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -3297,7 +3325,7 @@
 DTSTAMP:%(dtstamp)s
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
-SEQUENCE:2
+SEQUENCE:3
 END:VEVENT
 BEGIN:VEVENT
 UID:%(relID)s
@@ -3310,7 +3338,7 @@
 DTSTAMP:%(dtstamp)s
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
-SEQUENCE:2
+SEQUENCE:3
 END:VEVENT
 END:VCALENDAR
 """
@@ -3329,7 +3357,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY
-SEQUENCE:2
+SEQUENCE:3
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -3346,7 +3374,7 @@
 DTSTAMP:%(dtstamp)s
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
-SEQUENCE:2
+SEQUENCE:3
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
 UID:12345-67890
@@ -3372,7 +3400,7 @@
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
-SEQUENCE:2
+SEQUENCE:3
 SUMMARY:1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
  1234567890123456789012345678901234567890
@@ -3389,7 +3417,7 @@
 DTSTAMP:%(dtstamp)s
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
-SEQUENCE:2
+SEQUENCE:3
 END:VEVENT
 BEGIN:VEVENT
 UID:%(relID)s
@@ -3402,7 +3430,7 @@
 DTSTAMP:%(dtstamp)s
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
-SEQUENCE:2
+SEQUENCE:3
 END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
 UID:%(relID)s
@@ -3436,7 +3464,7 @@
         relsubs["mid"] = mid
         relsubs["att_uri"] = location
         relsubs["dtstamp"] = str(ical.masterComponent().propertyValue("DTSTAMP"))
-        self.assertEqual(str(ical).replace("\r\n ", ""), data_attach_1.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed attachment user01")
+        self.assertEqual(normalize_iCalStr(ical), normalize_iCalStr(data_attach_1) % relsubs, "Failed attachment user01")
         yield self.commit()
 
         # Add overrides to cause a split
@@ -3468,8 +3496,8 @@
 
         # Verify user01 data
         title = "user01"
-        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
-        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % relsubs, "Failed past: %s" % (title,))
 
         # Get user02 data
         cal = yield self.calendarUnderTest(name="calendar", home="user02")
@@ -3484,5 +3512,1433 @@
 
         # Verify user02 data
         title = "user02"
-        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future2.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
-        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past2.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future2) % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past2) % relsubs, "Failed past: %s" % (title,))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_processing_simple(self):
+        """
+        Test that splitting of calendar objects works when outside invites are processed.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", True)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 2)
+
+        # Create one event from outside organizer that will not split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back25
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=DECLINED:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=DECLINED:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back24
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_fwd10
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+
+        itip1 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-CALENDARSERVER-SPLIT-OLDER-UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+X-CALENDARSERVER-SPLIT-RID;VALUE=DATE-TIME:%(now_back14)s
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:cuser01 at example.org
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:cuser01 at example.org
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=TENTATIVE:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_fwd10)s
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_fwd10
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_past = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=NEEDS-ACTION:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=DECLINED:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=DECLINED:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_back25)s
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back25
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_back24)s
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back24
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        itip2 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-CALENDARSERVER-SPLIT-NEWER-UID:12345-67890
+X-CALENDARSERVER-SPLIT-RID;VALUE=DATE-TIME:%(now_back14)s
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=NEEDS-ACTION:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=DECLINED:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=DECLINED:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data.ics", component)
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+        # Now inject an iTIP with split
+        processor = ImplicitProcessor()
+        processor.getRecipientsCopy = lambda : succeed(None)
+
+        cobj = yield self.calendarObjectUnderTest(name="data.ics", calendar_name="calendar", home="user01")
+        processor.recipient_calendar_resource = cobj
+        processor.recipient_calendar = (yield cobj.componentForUser("user01"))
+        processor.message = Component.fromString(itip1 % self.subs)
+        processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
+        processor.recipient = LocalCalendarUser("urn:uuid:user01", None)
+        processor.method = "REQUEST"
+        processor.uid = "12345-67890"
+
+        result = yield processor.doImplicitAttendee()
+        self.assertEqual(result, (True, False, False, None,))
+        yield self.commit()
+
+        new_name = []
+
+        @inlineCallbacks
+        def _verify_state():
+            # Get user01 data
+            cal = yield self.calendarUnderTest(name="calendar", home="user01")
+            cobjs = yield cal.calendarObjects()
+            self.assertEqual(len(cobjs), 2)
+            for cobj in cobjs:
+                ical = yield cobj.component()
+                if ical.resourceUID() == "12345-67890":
+                    ical_future = ical
+                else:
+                    ical_past = ical
+                    new_name.append(cobj.name())
+
+            # Verify user01 data
+            title = "user01"
+            self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % self.subs, "Failed future: %s\n%s" % (title, diff_iCalStrs(ical_future, data_future % self.subs),))
+            self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % self.subs, "Failed past: %s\n%s" % (title, diff_iCalStrs(ical_past, data_past % self.subs),))
+
+            # No inbox
+            cal = yield self.calendarUnderTest(name="inbox", home="user01")
+            cobjs = yield cal.calendarObjects()
+            self.assertEqual(len(cobjs), 0)
+            yield self.commit()
+
+        yield _verify_state()
+
+        # Now inject an iTIP with split
+        processor = ImplicitProcessor()
+        processor.getRecipientsCopy = lambda : succeed(None)
+
+        cobj = yield self.calendarObjectUnderTest(name=new_name[0], calendar_name="calendar", home="user01")
+        self.assertTrue(cobj is not None)
+        processor.recipient_calendar_resource = cobj
+        processor.recipient_calendar = (yield cobj.componentForUser("user01"))
+        processor.message = Component.fromString(itip2 % self.subs)
+        processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
+        processor.recipient = LocalCalendarUser("urn:uuid:user01", None)
+        processor.method = "REQUEST"
+        processor.uid = "C4526F4C-4324-4893-B769-BD766E4A4E7C"
+
+        result = yield processor.doImplicitAttendee()
+        self.assertEqual(result, (True, False, False, None,))
+        yield self.commit()
+
+        yield _verify_state()
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_processing_one_past_instance(self):
+        """
+        Test that splitting of calendar objects works when outside invites are processed.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", True)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 2)
+
+        # Create one event from outside organizer that will not split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back25
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+
+        itip1 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:CANCEL
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-CALENDARSERVER-SPLIT-OLDER-UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+X-CALENDARSERVER-SPLIT-RID;VALUE=DATE-TIME:%(now_back14)s
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_past = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=NEEDS-ACTION:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_back25)s
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back25
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data.ics", component)
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+        # Now inject an iTIP with split
+        processor = ImplicitProcessor()
+        processor.getRecipientsCopy = lambda : succeed(None)
+
+        cobj = yield self.calendarObjectUnderTest(name="data.ics", calendar_name="calendar", home="user01")
+        processor.recipient_calendar_resource = cobj
+        processor.recipient_calendar = (yield cobj.componentForUser("user01"))
+        processor.message = Component.fromString(itip1 % self.subs)
+        processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
+        processor.recipient = LocalCalendarUser("urn:uuid:user01", None)
+        processor.method = "CANCEL"
+        processor.uid = "12345-67890"
+
+        result = yield processor.doImplicitAttendee()
+        self.assertEqual(result, (True, False, False, None,))
+        yield self.commit()
+
+        # Get user01 data
+        cal = yield self.calendarUnderTest(name="calendar", home="user01")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        ical = yield cobjs[0].component()
+        ical_past = ical
+
+        # Verify user01 data
+        title = "user01"
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % self.subs, "Failed past: %s\n%s" % (title, diff_iCalStrs(ical_past, data_past % self.subs),))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_processing_one_future_instance(self):
+        """
+        Test that splitting of calendar objects works when outside invites are processed.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", True)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 2)
+
+        # Create one event from outside organizer that will not split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_fwd10
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+
+        itip1 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-CALENDARSERVER-SPLIT-OLDER-UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+X-CALENDARSERVER-SPLIT-RID;VALUE=DATE-TIME:%(now_back14)s
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:cuser01 at example.org
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=TENTATIVE:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_fwd10)s
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_fwd10
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data.ics", component)
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+        # Now inject an iTIP with split
+        processor = ImplicitProcessor()
+        processor.getRecipientsCopy = lambda : succeed(None)
+
+        cobj = yield self.calendarObjectUnderTest(name="data.ics", calendar_name="calendar", home="user01")
+        processor.recipient_calendar_resource = cobj
+        processor.recipient_calendar = (yield cobj.componentForUser("user01"))
+        processor.message = Component.fromString(itip1 % self.subs)
+        processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
+        processor.recipient = LocalCalendarUser("urn:uuid:user01", None)
+        processor.method = "REQUEST"
+        processor.uid = "12345-67890"
+
+        result = yield processor.doImplicitAttendee()
+        self.assertEqual(result, (True, False, False, None,))
+        yield self.commit()
+
+        # Get user01 data
+        cal = yield self.calendarUnderTest(name="calendar", home="user01")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        ical = yield cobjs[0].component()
+        ical_future = ical
+
+        # Verify user01 data
+        title = "user01"
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % self.subs, "Failed future: %s\n%s" % (title, diff_iCalStrs(ical_future, data_future % self.subs),))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_processing_one_past_and_one_future(self):
+        """
+        Test that splitting of calendar objects works when outside invites are processed.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", True)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 2)
+
+        # Create one event from outside organizer that will not split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back25
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_fwd10
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+
+        itip1 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:CANCEL
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-CALENDARSERVER-SPLIT-OLDER-UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+X-CALENDARSERVER-SPLIT-RID;VALUE=DATE-TIME:%(now_back14)s
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back25
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=TENTATIVE:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_fwd10)s
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_fwd10
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_past = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=NEEDS-ACTION:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_back25)s
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back25
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data.ics", component)
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+        # Now inject an iTIP with split
+        processor = ImplicitProcessor()
+        processor.getRecipientsCopy = lambda : succeed(None)
+
+        cobj = yield self.calendarObjectUnderTest(name="data.ics", calendar_name="calendar", home="user01")
+        processor.recipient_calendar_resource = cobj
+        processor.recipient_calendar = (yield cobj.componentForUser("user01"))
+        processor.message = Component.fromString(itip1 % self.subs)
+        processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
+        processor.recipient = LocalCalendarUser("urn:uuid:user01", None)
+        processor.method = "REQUEST"
+        processor.uid = "12345-67890"
+
+        result = yield processor.doImplicitAttendee()
+        self.assertEqual(result, (True, False, False, None,))
+        yield self.commit()
+
+        # Get user01 data
+        cal = yield self.calendarUnderTest(name="calendar", home="user01")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 2)
+        for cobj in cobjs:
+            ical = yield cobj.component()
+            if ical.resourceUID() == "12345-67890":
+                ical_future = ical
+            else:
+                ical_past = ical
+
+        # Verify user01 data
+        title = "user01"
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % self.subs, "Failed future: %s\n%s" % (title, diff_iCalStrs(ical_future, data_future % self.subs),))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % self.subs, "Failed past: %s\n%s" % (title, diff_iCalStrs(ical_past, data_past % self.subs),))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_processing_disabled(self):
+        """
+        Test that splitting of calendar objects works when outside invites are processed.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", False)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 2)
+
+        # Create one event from outside organizer that will not split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back25
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=DECLINED:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=DECLINED:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_back24
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:now_fwd10
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+
+        itip1 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-CALENDARSERVER-SPLIT-OLDER-UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+X-CALENDARSERVER-SPLIT-RID;VALUE=DATE-TIME:%(now_back14)s
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:cuser01 at example.org
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user01 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:cuser01 at example.org
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        itip2 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-CALENDARSERVER-SPLIT-NEWER-UID:12345-67890
+X-CALENDARSERVER-SPLIT-RID;VALUE=DATE-TIME:%(now_back14)s
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=NEEDS-ACTION:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:C4526F4C-4324-4893-B769-BD766E4A4E7C
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=DECLINED:mailto:cuser01 at example.org
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=DECLINED:urn:uuid:user01
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE:mailto:cuser01 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:C4526F4C-4324-4893-B769-BD766E4A4E7C
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data.ics", component)
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+        # Now inject an iTIP with split
+        processor_action = [False, False, ]
+        def _doImplicitAttendeeRequest():
+            processor_action[0] = True
+            return succeed(True)
+        def _doImplicitAttendeeCancel():
+            processor_action[1] = True
+            return succeed(True)
+        processor = ImplicitProcessor()
+        processor.getRecipientsCopy = lambda : succeed(None)
+        processor.doImplicitAttendeeRequest = _doImplicitAttendeeRequest
+        processor.doImplicitAttendeeCancel = _doImplicitAttendeeCancel
+
+        cobj = yield self.calendarObjectUnderTest(name="data.ics", calendar_name="calendar", home="user01")
+        processor.recipient_calendar_resource = cobj
+        processor.recipient_calendar = (yield cobj.componentForUser("user01"))
+        processor.message = Component.fromString(itip1 % self.subs)
+        processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
+        processor.recipient = LocalCalendarUser("urn:uuid:user01", None)
+        processor.method = "REQUEST"
+        processor.uid = "12345-67890"
+
+        yield processor.doImplicitAttendee()
+        self.assertTrue(processor_action[0])
+        self.assertFalse(processor_action[1])
+        yield self.commit()
+
+        # Now inject an iTIP with split
+        processor_action = [False, False, ]
+        processor.getRecipientsCopy = lambda : succeed(None)
+        processor.doImplicitAttendeeRequest = _doImplicitAttendeeRequest
+        processor.doImplicitAttendeeCancel = _doImplicitAttendeeCancel
+
+        processor.recipient_calendar_resource = None
+        processor.recipient_calendar = None
+        processor.message = Component.fromString(itip2 % self.subs)
+        processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
+        processor.recipient = LocalCalendarUser("urn:uuid:user01", None)
+        processor.method = "REQUEST"
+        processor.uid = "C4526F4C-4324-4893-B769-BD766E4A4E7C"
+
+        yield processor.doImplicitAttendee()
+        self.assertTrue(processor_action[0])
+        self.assertFalse(processor_action[1])
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_external(self):
+        """
+        Test that splitting of calendar objects works.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", True)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 2)
+
+        # Create one event that will split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ATTENDEE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ATTENDEE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+ATTENDEE;RSVP=TRUE;SCHEDULE-STATUS=3.7:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;RSVP=TRUE;SCHEDULE-STATUS=3.7:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_past = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+ATTENDEE;RSVP=TRUE;SCHEDULE-STATUS=3.7:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+ATTENDEE;RSVP=TRUE;SCHEDULE-STATUS=3.7:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;RSVP=TRUE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+EXDATE:%(now_fwd10)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_past2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;RSVP=TRUE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;RSVP=TRUE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:%(relID)s
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_inbox2 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;RSVP=TRUE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+EXDATE:%(now_fwd10)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future_external = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-CALENDARSERVER-SPLIT-OLDER-UID:%(relID)s
+X-CALENDARSERVER-SPLIT-RID;VALUE=DATE-TIME:%(now_back14)s
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;RSVP=TRUE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;RSVP=TRUE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_past_external = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-CALENDARSERVER-SPLIT-NEWER-UID:12345-67890
+X-CALENDARSERVER-SPLIT-RID;VALUE=DATE-TIME:%(now_back14)s
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;RSVP=TRUE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+EXDATE:%(now_back24)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;RSVP=TRUE:mailto:cuser01 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        # Patch CalDAVScheduler to trap external schedules
+        details = []
+        def _doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False, suppress_refresh=False):
+            details.append((originator, recipients, calendar,))
+
+            responses = ScheduleResponseQueue("REQUEST", responsecode.OK)
+            for recipient in recipients:
+                responses.add(recipient, responsecode.OK, reqstatus=iTIPRequestStatus.MESSAGE_DELIVERED)
+            return succeed(responses)
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
+        self.assertTrue(hasattr(cobj, "_workItems"))
+        work = cobj._workItems[0]
+        yield self.commit()
+
+        self.patch(CalDAVScheduler, "doSchedulingViaPUT", _doSchedulingViaPUT)
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 1)
+        self.assertEqual(rows[0][0], cobj._resourceID)
+        yield self.abort()
+
+        # Wait for it to complete
+        yield work.whenExecuted()
+
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        # Get the existing and new object data
+        cobj1 = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        self.assertTrue(cobj1.isScheduleObject)
+        ical1 = yield cobj1.component()
+        newUID = ical1.masterComponent().propertyValue("RELATED-TO")
+
+        cobj2 = yield self.calendarObjectUnderTest(name="%s.ics" % (newUID,), calendar_name="calendar", home="user01")
+        self.assertTrue(cobj2 is not None)
+        self.assertTrue(cobj2.isScheduleObject)
+
+        ical_future = yield cobj1.component()
+        ical_past = yield cobj2.component()
+
+        # Verify user01 data
+        title = "user01"
+        relsubs = dict(self.subs)
+        relsubs["relID"] = newUID
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s\n%s" % (title, diff_iCalStrs(ical_future, data_future % relsubs),))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % relsubs, "Failed past: %s\n%s" % (title, diff_iCalStrs(ical_past, data_past % relsubs),))
+
+        # Get user02 data
+        cal = yield self.calendarUnderTest(name="calendar", home="user02")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 2)
+        for cobj in cobjs:
+            ical = yield cobj.component()
+            if ical.resourceUID() == "12345-67890":
+                ical_future = ical
+            else:
+                ical_past = ical
+
+        cal = yield self.calendarUnderTest(name="inbox", home="user02")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        ical_inbox = yield cobjs[0].component()
+
+        # Verify user02 data
+        title = "user02"
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future2) % relsubs, "Failed future: %s\n%s" % (title, diff_iCalStrs(ical_future, data_future2 % relsubs),))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past2) % relsubs, "Failed past: %s\n%s" % (title, diff_iCalStrs(ical_past, data_past2 % relsubs),))
+        self.assertEqual(normalize_iCalStr(ical_inbox), normalize_iCalStr(data_inbox2) % relsubs, "Failed past: %s\n%s" % (title, diff_iCalStrs(ical_inbox, data_inbox2 % relsubs),))
+
+        # Verify cuser02 data
+        self.assertEqual(len(details), 2)
+        self.assertEqual(details[0][0], "urn:uuid:user01")
+        self.assertEqual(details[0][1], ("mailto:cuser01 at example.org",))
+        self.assertEqual(normalize_iCalStr(details[0][2]), normalize_iCalStr(data_future_external) % relsubs, "Failed future: %s\n%s" % (title, diff_iCalStrs(details[0][2], data_future_external % relsubs),))
+
+        self.assertEqual(details[1][0], "urn:uuid:user01")
+        self.assertEqual(details[1][1], ("mailto:cuser01 at example.org",))
+        self.assertEqual(normalize_iCalStr(details[1][2]), normalize_iCalStr(data_past_external) % relsubs, "Failed past: %s\n%s" % (title, diff_iCalStrs(details[1][2], data_past_external % relsubs),))

Modified: CalendarServer/trunk/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/icalendarstore.py	2013-08-05 17:51:24 UTC (rev 11578)
+++ CalendarServer/trunk/txdav/caldav/icalendarstore.py	2013-08-05 17:55:30 UTC (rev 11579)
@@ -860,9 +860,11 @@
 
     ATTACHMENT_UPDATE     - change to a managed attachment that is re-writing calendar data.
 
-    SPLIT                 - calendar data is being split. Some validation and implicit scheduling is not done.
-                            Schedule-Tag is changed.
+    SPLIT_OWNER           - owner calendar data is being split. Implicit is done with non-hosted attendees.
 
+    SPLIT_ATTENDEE        - attendee calendar data is being split. No implicit done, but some extra processing
+                            is done (more than RAW).
+
     RAW                   - store the supplied data as-is without any processing or validation. This is used
                             for unit testing purposes only.
     """
@@ -872,7 +874,8 @@
     ORGANIZER_ITIP_UPDATE = NamedConstant()
     ATTENDEE_ITIP_UPDATE = NamedConstant()
     ATTACHMENT_UPDATE = NamedConstant()
-    SPLIT = NamedConstant()
+    SPLIT_OWNER = NamedConstant()
+    SPLIT_ATTENDEE = NamedConstant()
     RAW = NamedConstant()
 
     NORMAL.description = "normal"
@@ -880,7 +883,8 @@
     ORGANIZER_ITIP_UPDATE.description = "organizer-update"
     ATTENDEE_ITIP_UPDATE.description = "attendee-update"
     ATTACHMENT_UPDATE.description = "attachment-update"
-    SPLIT.description = "split"
+    SPLIT_OWNER.description = "split-owner"
+    SPLIT_ATTENDEE.description = "split-attendee"
     RAW.description = "raw"
 
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130805/bae8a24c/attachment-0001.html>


More information about the calendarserver-changes mailing list