<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[13408] CalendarServer/trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/13408">13408</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2014-05-02 13:07:26 -0700 (Fri, 02 May 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Implement background automatic purge of missing users (not enabled by default). Required some api clean-ups
and support for scheduling without a valid directory record.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertoolspurgepy">CalendarServer/trunk/calendarserver/tools/purge.py</a></li>
<li><a href="#CalendarServertrunkcalendarservertoolstesttest_purgepy">CalendarServer/trunk/calendarserver/tools/test/test_purge.py</a></li>
<li><a href="#CalendarServertrunkcalendarservertoolstesttest_purge_old_eventspy">CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavdirectoryprincipalpy">CalendarServer/trunk/twistedcaldav/directory/principal.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavicalpy">CalendarServer/trunk/twistedcaldav/ical.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavstdconfigpy">CalendarServer/trunk/twistedcaldav/stdconfig.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingaddressmappingpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/addressmapping.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingcaldavdeliverypy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingcaldavschedulerpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/scheduler.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingcuaddresspy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/cuaddress.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingicaldiffpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingimipoutboundpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/outbound.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingimplicitpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingischeduledeliverypy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/delivery.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingischeduleschedulerpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/scheduler.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingprocessingpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingschedulerpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_implicitpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_implicit.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_utilspy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_utils.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingutilspy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/utils.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingworkpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoresqlpy">CalendarServer/trunk/txdav/caldav/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoretesttest_sqlpy">CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoretestutilpy">CalendarServer/trunk/txdav/caldav/datastore/test/util.py</a></li>
<li><a href="#CalendarServertrunktxdavcarddavdatastoresqlpy">CalendarServer/trunk/txdav/carddav/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresqlpy">CalendarServer/trunk/txdav/common/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresql_schemacurrentoracledialectsql">CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresql_schemacurrentsql">CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresql_tablespy">CalendarServer/trunk/txdav/common/datastore/sql_tables.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoretestutilpy">CalendarServer/trunk/txdav/common/datastore/test/util.py</a></li>
<li><a href="#CalendarServertrunktxdavwhodirectorypy">CalendarServer/trunk/txdav/who/directory.py</a></li>
<li><a href="#CalendarServertrunktxdavwhoutilpy">CalendarServer/trunk/txdav/who/util.py</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_cuaddresspy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_cuaddress.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresql_schemaoldoracledialectv40sql">CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v40.sql</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresql_schemaoldpostgresdialectv40sql">CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v40.sql</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresql_schemaupgradesoracledialectupgrade_from_40_to_41sql">CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_40_to_41.sql</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresql_schemaupgradespostgresdialectupgrade_from_40_to_41sql">CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_40_to_41.sql</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertoolspurgepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/purge.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/purge.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/calendarserver/tools/purge.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -18,22 +18,28 @@
</span><span class="cx"> from __future__ import print_function
</span><span class="cx"> 
</span><span class="cx"> import collections
</span><ins>+import datetime
</ins><span class="cx"> from getopt import getopt, GetoptError
</span><span class="cx"> import os
</span><span class="cx"> import sys
</span><span class="cx"> 
</span><span class="cx"> from calendarserver.tools import tables
</span><span class="cx"> from calendarserver.tools.cmdline import utilityMain, WorkerService
</span><ins>+
</ins><span class="cx"> from pycalendar.datetime import DateTime
</span><ins>+
+from twext.enterprise.dal.record import fromTable
+from twext.enterprise.dal.syntax import Delete, Select, Union
+from twext.enterprise.jobqueue import WorkItem
</ins><span class="cx"> from twext.python.log import Logger
</span><ins>+
</ins><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><ins>+
</ins><span class="cx"> from twistedcaldav import caldavxml
</span><ins>+from twistedcaldav.config import config
+
</ins><span class="cx"> from txdav.caldav.datastore.query.filter import Filter
</span><del>-from twext.enterprise.dal.record import fromTable
-from twext.enterprise.dal.syntax import Delete, Select
-from twext.enterprise.jobqueue import WorkItem
-from txdav.common.datastore.sql_tables import schema
-import datetime
</del><ins>+from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_NORMAL
</ins><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="lines">@@ -47,6 +53,11 @@
</span><span class="cx">     WorkItem,
</span><span class="cx">     fromTable(schema.PRINCIPAL_PURGE_POLLING_WORK)
</span><span class="cx"> ):
</span><ins>+    &quot;&quot;&quot;
+    A work item that scans the existing set of provisioned homes in the
+    store and creates a work item for each to be checked against the
+    directory to see if they need purging.
+    &quot;&quot;&quot;
</ins><span class="cx"> 
</span><span class="cx">     group = &quot;principal_purge_polling&quot;
</span><span class="cx"> 
</span><span class="lines">@@ -56,36 +67,44 @@
</span><span class="cx">         # Delete all other work items
</span><span class="cx">         yield Delete(From=self.table, Where=None).on(self.transaction)
</span><span class="cx"> 
</span><del>-        # Schedule next update, 7 days out
-        notBefore = (
-            datetime.datetime.utcnow() +
-            datetime.timedelta(days=7)
-        )
-        log.info(
-            &quot;Scheduling next principal purge scan update: {when}&quot;, when=notBefore
-        )
-        yield self.transaction.enqueue(
-            PrincipalPurgePollingWork,
-            notBefore=notBefore
-        )
</del><ins>+        # If not enabled, punt here
+        if not config.AutomaticPurging.Enabled:
+            returnValue(None)
</ins><span class="cx"> 
</span><ins>+        # Schedule next update, 7 days out (default)
+        # Special - for testing it is handy to have this work item not regenerate, so
+        # we use an interval of -1 to signify a one-shot operation
+        if config.AutomaticPurging.PollingIntervalSeconds != -1:
+            notBefore = (
+                datetime.datetime.utcnow() +
+                datetime.timedelta(seconds=config.AutomaticPurging.PollingIntervalSeconds)
+            )
+            log.info(
+                &quot;Scheduling next principal purge scan update: {when}&quot;, when=notBefore
+            )
+            yield self.transaction.enqueue(
+                PrincipalPurgePollingWork,
+                notBefore=notBefore
+            )
+
</ins><span class="cx">         # Do the scan
</span><span class="cx">         allUIDs = set()
</span><span class="cx">         for home in (schema.CALENDAR_HOME, schema.ADDRESSBOOK_HOME):
</span><span class="cx">             for [uid] in (
</span><span class="cx">                 yield Select(
</span><span class="cx">                     [home.OWNER_UID],
</span><del>-                    From=home
</del><ins>+                    From=home,
+                    Where=(home.STATUS == _HOME_STATUS_NORMAL),
</ins><span class="cx">                 ).on(self.transaction)
</span><span class="cx">             ):
</span><span class="cx">                 allUIDs.add(uid)
</span><span class="cx"> 
</span><del>-        # Spread out the per-uid checks 1 second apart
</del><ins>+        # Spread out the per-uid checks 0 second apart
</ins><span class="cx">         seconds = 0
</span><span class="cx">         for uid in allUIDs:
</span><span class="cx">             notBefore = (
</span><span class="cx">                 datetime.datetime.utcnow() +
</span><del>-                datetime.timedelta(seconds=seconds)
</del><ins>+                datetime.timedelta(seconds=config.AutomaticPurging.CheckStaggerSeconds)
</ins><span class="cx">             )
</span><span class="cx">             seconds += 1
</span><span class="cx">             yield self.transaction.enqueue(
</span><span class="lines">@@ -96,13 +115,14 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-
</del><span class="cx"> class PrincipalPurgeCheckWork(
</span><span class="cx">     WorkItem,
</span><span class="cx">     fromTable(schema.PRINCIPAL_PURGE_CHECK_WORK)
</span><span class="cx"> ):
</span><span class="cx">     &quot;&quot;&quot;
</span><del>-    Work item for checking for the existence of a UID in the directory
</del><ins>+    Work item for checking for the existence of a UID in the directory. This
+    work item is created by L{PrincipalPurgePollingWork} - one for each
+    unique user UID to check.
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">     group = property(lambda self: self.uid)
</span><span class="lines">@@ -116,6 +136,10 @@
</span><span class="cx">             Where=self.table.UID == self.uid
</span><span class="cx">         ).on(self.transaction)
</span><span class="cx"> 
</span><ins>+        # If not enabled, punt here
+        if not config.AutomaticPurging.Enabled:
+            returnValue(None)
+
</ins><span class="cx">         log.debug(&quot;Checking for existence of {uid} in directory&quot;, uid=self.uid)
</span><span class="cx">         directory = self.transaction.store().directoryService()
</span><span class="cx">         record = yield directory.recordWithUID(self.uid)
</span><span class="lines">@@ -124,7 +148,7 @@
</span><span class="cx">             # Schedule purge of this UID a week from now
</span><span class="cx">             notBefore = (
</span><span class="cx">                 datetime.datetime.utcnow() +
</span><del>-                datetime.timedelta(minutes=1)
</del><ins>+                datetime.timedelta(seconds=config.AutomaticPurging.PurgeIntervalSeconds)
</ins><span class="cx">             )
</span><span class="cx">             log.warn(
</span><span class="cx">                 &quot;Principal {uid} is no longer in the directory; scheduling clean-up at {when}&quot;,
</span><span class="lines">@@ -159,6 +183,10 @@
</span><span class="cx">             Where=self.table.UID == self.uid
</span><span class="cx">         ).on(self.transaction)
</span><span class="cx"> 
</span><ins>+        # If not enabled, punt here
+        if not config.AutomaticPurging.Enabled:
+            returnValue(None)
+
</ins><span class="cx">         # Check for UID in directory again
</span><span class="cx">         log.debug(&quot;One last existence check for {uid}&quot;, uid=self.uid)
</span><span class="cx">         directory = self.transaction.store().directoryService()
</span><span class="lines">@@ -166,17 +194,15 @@
</span><span class="cx"> 
</span><span class="cx">         if record is None:
</span><span class="cx">             # Time to go
</span><del>-            service = PurgePrincipalService(self.transaction.store)
</del><ins>+            service = PurgePrincipalService(self.transaction.store())
</ins><span class="cx">             log.warn(
</span><span class="cx">                 &quot;Cleaning up future events for principal {uid} since they are no longer in directory&quot;,
</span><span class="cx">                 uid=self.uid
</span><span class="cx">             )
</span><span class="cx">             yield service.purgeUIDs(
</span><del>-                self.transaction.store,
</del><ins>+                self.transaction.store(),
</ins><span class="cx">                 directory,
</span><span class="cx">                 [self.uid],
</span><del>-                completely=False,
-                doimplicit=True,
</del><span class="cx">                 proxies=True,
</span><span class="cx">                 when=None
</span><span class="cx">             )
</span><span class="lines">@@ -184,6 +210,72 @@
</span><span class="cx">             log.debug(&quot;{uid} has re-appeared in the directory&quot;, uid=self.uid)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
+class PrincipalPurgeHomeWork(
+    WorkItem,
+    fromTable(schema.PRINCIPAL_PURGE_HOME_WORK)
+):
+    &quot;&quot;&quot;
+    Work item for removing a UID's home
+    &quot;&quot;&quot;
+
+    group = property(lambda self: str(self.homeResourceID))
+
+    @inlineCallbacks
+    def doWork(self):
+
+        # Delete any other work items for this UID
+        yield Delete(
+            From=self.table,
+            Where=self.table.HOME_RESOURCE_ID == self.homeResourceID
+        ).on(self.transaction)
+
+        # NB We do not check config.AutomaticPurging.Enabled here because if this work
+        # item was enqueued we always need to complete it
+
+        # Check for pending scheduling operations
+        sow = schema.SCHEDULE_ORGANIZER_WORK
+        srw = schema.SCHEDULE_REPLY_WORK
+        srcw = schema.SCHEDULE_REPLY_CANCEL_WORK
+        rows = yield Select(
+            [sow.HOME_RESOURCE_ID],
+            From=sow,
+            Where=(sow.HOME_RESOURCE_ID == self.homeResourceID),
+            SetExpression=Union(
+                Select(
+                    [srw.HOME_RESOURCE_ID],
+                    From=srw,
+                    Where=(srw.HOME_RESOURCE_ID == self.homeResourceID),
+                    SetExpression=Union(
+                        Select(
+                            [srcw.HOME_RESOURCE_ID],
+                            From=srcw,
+                            Where=(srcw.HOME_RESOURCE_ID == self.homeResourceID),
+                        )
+                    ),
+                )
+            ),
+        ).on(self.transaction)
+
+        if rows and len(rows):
+            # Regenerate this job
+            notBefore = (
+                datetime.datetime.utcnow() +
+                datetime.timedelta(seconds=config.AutomaticPurging.HomePurgeDelaySeconds)
+            )
+            yield self.transaction.enqueue(
+                PrincipalPurgeHomeWork,
+                homeResourceID=self.homeResourceID,
+                notBefore=notBefore
+            )
+        else:
+            # Get the home and remove it - only if properly marked as being purged
+            home = yield self.transaction.calendarHomeWithResourceID(self.homeResourceID)
+            if home.purging():
+                yield home.remove()
+
+
+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def scheduleNextPrincipalPurgeUpdate(store, seconds):
</span><span class="cx"> 
</span><span class="lines">@@ -743,8 +835,6 @@
</span><span class="cx">     uids = None
</span><span class="cx">     dryrun = False
</span><span class="cx">     verbose = False
</span><del>-    completely = False
-    doimplicit = True
</del><span class="cx">     proxies = True
</span><span class="cx">     when = None
</span><span class="cx"> 
</span><span class="lines">@@ -755,9 +845,9 @@
</span><span class="cx">         print(&quot;usage: %s [options]&quot; % (name,))
</span><span class="cx">         print(&quot;&quot;)
</span><span class="cx">         print(&quot;  Remove a principal's events and contacts from the calendar server&quot;)
</span><ins>+        print(&quot;  Future events are declined or cancelled&quot;)
</ins><span class="cx">         print(&quot;&quot;)
</span><span class="cx">         print(&quot;options:&quot;)
</span><del>-        print(&quot;  -c --completely: By default, only future events are canceled; this option cancels all events&quot;)
</del><span class="cx">         print(&quot;  -h --help: print this help and exit&quot;)
</span><span class="cx">         print(&quot;  -f --config &lt;path&gt;: Specify caldavd.plist configuration path&quot;)
</span><span class="cx">         print(&quot;  -n --dry-run: calculate how many events and contacts to purge, but do not purge data&quot;)
</span><span class="lines">@@ -777,14 +867,12 @@
</span><span class="cx"> 
</span><span class="cx">         try:
</span><span class="cx">             (optargs, args) = getopt(
</span><del>-                sys.argv[1:], &quot;cDf:hnv&quot;, [
-                    &quot;completely&quot;,
</del><ins>+                sys.argv[1:], &quot;Df:hnv&quot;, [
</ins><span class="cx">                     &quot;dry-run&quot;,
</span><span class="cx">                     &quot;config=&quot;,
</span><span class="cx">                     &quot;help&quot;,
</span><span class="cx">                     &quot;verbose&quot;,
</span><span class="cx">                     &quot;debug&quot;,
</span><del>-                    &quot;noimplicit&quot;,
</del><span class="cx">                 ],
</span><span class="cx">             )
</span><span class="cx">         except GetoptError, e:
</span><span class="lines">@@ -797,16 +885,11 @@
</span><span class="cx">         dryrun = False
</span><span class="cx">         verbose = False
</span><span class="cx">         debug = False
</span><del>-        completely = False
-        doimplicit = True
</del><span class="cx"> 
</span><span class="cx">         for opt, arg in optargs:
</span><span class="cx">             if opt in (&quot;-h&quot;, &quot;--help&quot;):
</span><span class="cx">                 cls.usage()
</span><span class="cx"> 
</span><del>-            elif opt in (&quot;-c&quot;, &quot;--completely&quot;):
-                completely = True
-
</del><span class="cx">             elif opt in (&quot;-v&quot;, &quot;--verbose&quot;):
</span><span class="cx">                 verbose = True
</span><span class="cx"> 
</span><span class="lines">@@ -819,18 +902,13 @@
</span><span class="cx">             elif opt in (&quot;-f&quot;, &quot;--config&quot;):
</span><span class="cx">                 configFileName = arg
</span><span class="cx"> 
</span><del>-            elif opt in (&quot;--noimplicit&quot;):
-                doimplicit = False
-
</del><span class="cx">             else:
</span><span class="cx">                 raise NotImplementedError(opt)
</span><span class="cx"> 
</span><span class="cx">         # args is a list of uids
</span><span class="cx">         cls.uids = args
</span><del>-        cls.completely = completely
</del><span class="cx">         cls.dryrun = dryrun
</span><span class="cx">         cls.verbose = verbose
</span><del>-        cls.doimplicit = doimplicit
</del><span class="cx"> 
</span><span class="cx">         utilityMain(
</span><span class="cx">             configFileName,
</span><span class="lines">@@ -842,15 +920,13 @@
</span><span class="cx">     @classmethod
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def purgeUIDs(cls, store, directory, uids, verbose=False, dryrun=False,
</span><del>-                  completely=False, doimplicit=True, proxies=True, when=None):
</del><ins>+                  proxies=True, when=None):
</ins><span class="cx"> 
</span><span class="cx">         service = cls(store)
</span><span class="cx">         service.directory = directory
</span><span class="cx">         service.uids = uids
</span><span class="cx">         service.verbose = verbose
</span><span class="cx">         service.dryrun = dryrun
</span><del>-        service.completely = completely
-        service.doimplicit = doimplicit
</del><span class="cx">         service.proxies = proxies
</span><span class="cx">         service.when = when
</span><span class="cx">         result = yield service.doWork()
</span><span class="lines">@@ -885,32 +961,18 @@
</span><span class="cx">         if self.when is None:
</span><span class="cx">             self.when = DateTime.getNowUTC()
</span><span class="cx"> 
</span><del>-        # Does the record exist?
-        record = yield self.directory.recordWithUID(uid)
-        # MOVE2WHO
-        # if record is None:
-            # The user has already been removed from the directory service.  We
-            # need to fashion a temporary, fake record
</del><ins>+        cuas = set((
+            &quot;urn:uuid:{}&quot;.format(uid),
+            &quot;urn:x-uid:{}&quot;.format(uid)
+        ))
</ins><span class="cx"> 
</span><del>-            # FIXME: probably want a more elegant way to accomplish this,
-            # since it requires the aggregate directory to examine these first:
-            # record = DirectoryRecord(self.directory, &quot;users&quot;, uid, shortNames=(uid,), enabledForCalendaring=True)
-            # self.directory._tmpRecords[&quot;shortNames&quot;][uid] = record
-            # self.directory._tmpRecords[&quot;uids&quot;][uid] = record
-
-        # Override augments settings for this record
-        record.hasCalendars = True
-        record.hasContacts = True
-
-        cua = record.canonicalCalendarUserAddress()
-
</del><span class="cx">         # See if calendar home is provisioned
</span><span class="cx">         txn = self.store.newTransaction()
</span><span class="cx">         storeCalHome = yield txn.calendarHomeWithUID(uid)
</span><span class="cx">         calHomeProvisioned = storeCalHome is not None
</span><span class="cx"> 
</span><del>-        # If in &quot;completely&quot; mode, unshare collections, remove notifications
-        if calHomeProvisioned and self.completely:
</del><ins>+        # Always, unshare collections, remove notifications
+        if calHomeProvisioned:
</ins><span class="cx">             yield self._cleanHome(txn, storeCalHome)
</span><span class="cx"> 
</span><span class="cx">         yield txn.commit()
</span><span class="lines">@@ -918,7 +980,7 @@
</span><span class="cx">         count = 0
</span><span class="cx"> 
</span><span class="cx">         if calHomeProvisioned:
</span><del>-            count = yield self._cancelEvents(txn, uid, cua)
</del><ins>+            count = yield self._cancelEvents(txn, uid, cuas)
</ins><span class="cx"> 
</span><span class="cx">         # Remove empty calendar collections (and calendar home if no more
</span><span class="cx">         # calendars)
</span><span class="lines">@@ -930,7 +992,7 @@
</span><span class="cx">         if self.proxies and not self.dryrun:
</span><span class="cx">             if self.verbose:
</span><span class="cx">                 print(&quot;Deleting any proxy assignments&quot;)
</span><del>-            yield self._purgeProxyAssignments(self.store, record)
</del><ins>+            yield self._purgeProxyAssignments(self.store, uid)
</ins><span class="cx"> 
</span><span class="cx">         returnValue(count)
</span><span class="cx"> 
</span><span class="lines">@@ -951,13 +1013,13 @@
</span><span class="cx"> 
</span><span class="cx">         if not self.dryrun:
</span><span class="cx">             yield storeCalHome.removeUnacceptedShares()
</span><del>-            notificationHome = yield txn.notificationsWithUID(storeCalHome.uid())
</del><ins>+            notificationHome = yield txn.notificationsWithUID(storeCalHome.uid(), create=False)
</ins><span class="cx">             if notificationHome is not None:
</span><span class="cx">                 yield notificationHome.remove()
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def _cancelEvents(self, txn, uid, cua):
</del><ins>+    def _cancelEvents(self, txn, uid, cuas):
</ins><span class="cx"> 
</span><span class="cx">         # Anything in the past is left alone
</span><span class="cx">         whenString = self.when.getText()
</span><span class="lines">@@ -983,73 +1045,59 @@
</span><span class="cx">             txn = self.store.newTransaction(authz_uid=uid)
</span><span class="cx">             storeCalHome = yield txn.calendarHomeWithUID(uid)
</span><span class="cx">             calendar = yield storeCalHome.calendarWithName(calendarName)
</span><del>-            childNames = []
</del><ins>+            allChildNames = []
+            futureChildNames = set()
</ins><span class="cx"> 
</span><del>-            if self.completely:
</del><ins>+            # Only purge owned calendars
+            if calendar.owned():
</ins><span class="cx">                 # all events
</span><span class="cx">                 for childName in (yield calendar.listCalendarObjects()):
</span><del>-                    childNames.append(childName)
-            else:
</del><ins>+                    allChildNames.append(childName)
+
</ins><span class="cx">                 # events matching filter
</span><span class="cx">                 for childName, _ignore_childUid, _ignore_childType in (yield calendar.search(query_filter)):
</span><del>-                    childNames.append(childName)
</del><ins>+                    futureChildNames.add(childName)
+
</ins><span class="cx">             yield txn.commit()
</span><span class="cx"> 
</span><del>-            for childName in childNames:
</del><ins>+            for childName in allChildNames:
</ins><span class="cx"> 
</span><span class="cx">                 txn = self.store.newTransaction(authz_uid=uid)
</span><span class="cx">                 storeCalHome = yield txn.calendarHomeWithUID(uid)
</span><span class="cx">                 calendar = yield storeCalHome.calendarWithName(calendarName)
</span><ins>+                doScheduling = childName in futureChildNames
</ins><span class="cx"> 
</span><span class="cx">                 try:
</span><span class="cx">                     childResource = yield calendar.calendarObjectWithName(childName)
</span><span class="cx"> 
</span><del>-                    # Always delete inbox items
-                    if self.completely or calendar.isInbox():
-                        action = self.CANCELEVENT_SHOULD_DELETE
-                    else:
-                        event = yield childResource.componentForUser()
-                        action = self._cancelEvent(event, self.when, cua)
-
</del><span class="cx">                     uri = &quot;/calendars/__uids__/%s/%s/%s&quot; % (storeCalHome.uid(), calendar.name(), childName)
</span><del>-                    if action == self.CANCELEVENT_MODIFIED:
-                        if self.verbose:
-                            if self.dryrun:
-                                print(&quot;Would modify: %s&quot; % (uri,))
-                            else:
-                                print(&quot;Modifying: %s&quot; % (uri,))
-                        if not self.dryrun:
-                            yield childResource.setComponent(event)
-                        count += 1
</del><ins>+                    incrementCount = self.dryrun
+                    if self.verbose:
+                        if self.dryrun:
+                            print(&quot;Would delete%s: %s&quot; % (&quot; with scheduling&quot; if doScheduling else &quot;&quot;, uri,))
+                        else:
+                            print(&quot;Deleting%s: %s&quot; % (&quot; with scheduling&quot; if doScheduling else &quot;&quot;, uri,))
+                    if not self.dryrun:
+                        retry = False
+                        try:
+                            yield childResource.remove(implicitly=doScheduling)
+                            incrementCount = True
+                        except Exception, e:
+                            print(&quot;Exception deleting %s: %s&quot; % (uri, str(e)))
+                            retry = True
</ins><span class="cx"> 
</span><del>-                    elif action == self.CANCELEVENT_SHOULD_DELETE:
-                        incrementCount = self.dryrun
-                        if self.verbose:
-                            if self.dryrun:
-                                print(&quot;Would delete: %s&quot; % (uri,))
-                            else:
-                                print(&quot;Deleting: %s&quot; % (uri,))
-                        if not self.dryrun:
-                            retry = False
</del><ins>+                        if retry and doScheduling:
+                            # Try again with implicit scheduling off
+                            print(&quot;Retrying deletion of %s with scheduling turned off&quot; % (uri,))
</ins><span class="cx">                             try:
</span><del>-                                yield childResource.remove(implicitly=self.doimplicit)
</del><ins>+                                yield childResource.remove(implicitly=False)
</ins><span class="cx">                                 incrementCount = True
</span><span class="cx">                             except Exception, e:
</span><del>-                                print(&quot;Exception deleting %s: %s&quot; % (uri, str(e)))
-                                retry = True
</del><ins>+                                print(&quot;Still couldn't delete %s even with scheduling turned off: %s&quot; % (uri, str(e)))
</ins><span class="cx"> 
</span><del>-                            if retry and self.doimplicit:
-                                # Try again with implicit scheduling off
-                                print(&quot;Retrying deletion of %s with implicit scheduling turned off&quot; % (uri, childName))
-                                try:
-                                    yield childResource.remove(implicitly=False)
-                                    incrementCount = True
-                                except Exception, e:
-                                    print(&quot;Still couldn't delete %s even with implicit scheduling turned off: %s&quot; % (uri, str(e)))
</del><ins>+                    if incrementCount:
+                        count += 1
</ins><span class="cx"> 
</span><del>-                        if incrementCount:
-                            count += 1
-
</del><span class="cx">                     # Commit
</span><span class="cx">                     yield txn.commit()
</span><span class="cx"> 
</span><span class="lines">@@ -1095,8 +1143,22 @@
</span><span class="cx">                         else:
</span><span class="cx">                             print(&quot;Deleting calendar home&quot;)
</span><span class="cx">                     if not self.dryrun:
</span><del>-                        yield storeCalHome.remove()
</del><ins>+                        # Queue a job to delete the calendar home after any scheduling operations
+                        # are complete
+                        notBefore = (
+                            datetime.datetime.utcnow() +
+                            datetime.timedelta(seconds=config.AutomaticPurging.HomePurgeDelaySeconds)
+                        )
+                        yield txn.enqueue(
+                            PrincipalPurgeHomeWork,
+                            homeResourceID=storeCalHome.id(),
+                            notBefore=notBefore
+                        )
</ins><span class="cx"> 
</span><ins>+                        # Also mark the home as purging so it won't be looked at again during
+                        # purge polling
+                        yield storeCalHome.purge()
+
</ins><span class="cx">             # Commit
</span><span class="cx">             yield txn.commit()
</span><span class="cx"> 
</span><span class="lines">@@ -1159,112 +1221,12 @@
</span><span class="cx"> 
</span><span class="cx">         returnValue(count)
</span><span class="cx"> 
</span><del>-    CANCELEVENT_SKIPPED = 1
-    CANCELEVENT_MODIFIED = 2
-    CANCELEVENT_NOT_MODIFIED = 3
-    CANCELEVENT_SHOULD_DELETE = 4
</del><span class="cx"> 
</span><del>-    @classmethod
-    def _cancelEvent(cls, event, when, cua):
-        &quot;&quot;&quot;
-        Modify a VEVENT such that all future occurrences are removed
-
-        @param event: the event to modify
-        @type event: L{twistedcaldav.ical.Component}
-
-        @param when: the cutoff date (anything after which is removed)
-        @type when: DateTime
-
-        @param cua: Calendar User Address of principal being purged, to compare
-            to see if it's the organizer of the event or just an attendee
-        @type cua: string
-
-        Assumes that event does not occur entirely in the past.
-
-        @return: one of the 4 constants above to indicate what action to take
-        &quot;&quot;&quot;
-
-        whenDate = when.duplicate()
-        whenDate.setDateOnly(True)
-
-        # Only process VEVENT
-        if event.mainType() != &quot;VEVENT&quot;:
-            return cls.CANCELEVENT_SKIPPED
-
-        main = event.mainComponent()
-
-        # Anything completely in the future is deleted
-        dtstart = main.getStartDateUTC()
-        isDateTime = not dtstart.isDateOnly()
-        if dtstart &gt; when:
-            return cls.CANCELEVENT_SHOULD_DELETE
-
-        organizer = main.getOrganizer()
-
-        # Non-meetings are deleted
-        if organizer is None:
-            return cls.CANCELEVENT_SHOULD_DELETE
-
-        # Meetings which cua is merely an attendee are deleted (thus implicitly
-        # declined)
-        # FIXME: I think we want to decline anything after the cut-off, not delete
-        # the whole event.
-        if organizer != cua:
-            return cls.CANCELEVENT_SHOULD_DELETE
-
-        dirty = False
-
-        # Set the UNTIL on RRULE to cease at the cutoff
-        if main.hasProperty(&quot;RRULE&quot;):
-            for rrule in main.properties(&quot;RRULE&quot;):
-                rrule = rrule.value()
-                if rrule.getUseCount():
-                    rrule.setUseCount(False)
-
-                rrule.setUseUntil(True)
-                if isDateTime:
-                    rrule.setUntil(when)
-                else:
-                    rrule.setUntil(whenDate)
-                dirty = True
-
-        # Remove any EXDATEs and RDATEs beyond the cutoff
-        for dateType in (&quot;EXDATE&quot;, &quot;RDATE&quot;):
-            if main.hasProperty(dateType):
-                for exdate_rdate in main.properties(dateType):
-                    newValues = []
-                    for value in exdate_rdate.value():
-                        if value.getValue() &lt; when:
-                            newValues.append(value)
-                        else:
-                            exdate_rdate.value().remove(value)
-                            dirty = True
-                    if not newValues:
-                        main.removeProperty(exdate_rdate)
-                        dirty = True
-
-        # Remove any overridden components beyond the cutoff
-        for component in tuple(event.subcomponents()):
-            if component.name() == &quot;VEVENT&quot;:
-                dtstart = component.getStartDateUTC()
-                remove = False
-                if dtstart &gt; when:
-                    remove = True
-                if remove:
-                    event.removeComponent(component)
-                    dirty = True
-
-        if dirty:
-            return cls.CANCELEVENT_MODIFIED
-        else:
-            return cls.CANCELEVENT_NOT_MODIFIED
-
-
</del><span class="cx">     @inlineCallbacks
</span><del>-    def _purgeProxyAssignments(self, store, record):
</del><ins>+    def _purgeProxyAssignments(self, store, uid):
</ins><span class="cx"> 
</span><span class="cx">         txn = store.newTransaction()
</span><span class="cx">         for readWrite in (True, False):
</span><del>-            yield txn.removeDelegates(record.uid, readWrite)
-            yield txn.removeDelegateGroupss(record.uid, readWrite)
</del><ins>+            yield txn.removeDelegates(uid, readWrite)
+            yield txn.removeDelegateGroups(uid, readWrite)
</ins><span class="cx">         yield txn.commit()
</span></span></pre></div>
<a id="CalendarServertrunkcalendarservertoolstesttest_purgepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/test/test_purge.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/test/test_purge.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/calendarserver/tools/test/test_purge.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -15,22 +15,27 @@
</span><span class="cx"> ##
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-from calendarserver.tools.purge import PurgePrincipalService
-from twistedcaldav.config import config
-from twistedcaldav.ical import Component
-from twistedcaldav.test.util import StoreTestCase
</del><ins>+from calendarserver.tools.purge import PurgePrincipalService, \
+    PrincipalPurgeHomeWork, PrincipalPurgePollingWork, PrincipalPurgeCheckWork, \
+    PrincipalPurgeWork
</ins><span class="cx"> 
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><del>-from pycalendar.timezone import Timezone
</del><span class="cx"> 
</span><del>-from twisted.internet.defer import inlineCallbacks
-from txdav.common.datastore.test.util import populateCalendarsFrom
</del><ins>+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, Deferred
+
+from twistedcaldav.config import config
+from twistedcaldav.test.util import StoreTestCase
+
</ins><span class="cx"> from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
</span><ins>+from txdav.common.datastore.test.util import populateCalendarsFrom
</ins><span class="cx"> 
</span><span class="cx"> from txweb2.http_headers import MimeType
</span><span class="cx"> 
</span><ins>+import datetime
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx"> future = DateTime.getNowUTC()
</span><span class="cx"> future.offsetDay(1)
</span><span class="cx"> future = future.getText()
</span><span class="lines">@@ -61,9 +66,9 @@
</span><span class="cx"> SUMMARY:Organizer
</span><span class="cx"> DTSTART:%s
</span><span class="cx"> DURATION:PT1H
</span><del>-ORGANIZER:urn:uuid:E9E78C86-4829-4520-A35D-70DDADAB2092
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:E9E78C86-4829-4520-A35D-70DDADAB2092
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
</del><ins>+ORGANIZER:urn:uuid:10000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:10000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:10000000-0000-0000-0000-000000000002
</ins><span class="cx"> END:VEVENT
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> &quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;) % (future,)
</span><span class="lines">@@ -76,9 +81,9 @@
</span><span class="cx"> SUMMARY:Attendee
</span><span class="cx"> DTSTART:%s
</span><span class="cx"> DURATION:PT1H
</span><del>-ORGANIZER:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:E9E78C86-4829-4520-A35D-70DDADAB2092
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
</del><ins>+ORGANIZER:urn:uuid:10000000-0000-0000-0000-000000000002
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:10000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:10000000-0000-0000-0000-000000000002
</ins><span class="cx"> END:VEVENT
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> &quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;) % (future,)
</span><span class="lines">@@ -119,9 +124,9 @@
</span><span class="cx"> SUMMARY:Organizer
</span><span class="cx"> DTSTART:%s
</span><span class="cx"> DURATION:PT1H
</span><del>-ORGANIZER:urn:uuid:1CB4378B-DD76-462D-B4D4-BD131FE89243
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:1CB4378B-DD76-462D-B4D4-BD131FE89243
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
</del><ins>+ORGANIZER:urn:uuid:F0000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:F0000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:10000000-0000-0000-0000-000000000002
</ins><span class="cx"> END:VEVENT
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> &quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;) % (future,)
</span><span class="lines">@@ -134,9 +139,9 @@
</span><span class="cx"> SUMMARY:Attendee
</span><span class="cx"> DTSTART:%s
</span><span class="cx"> DURATION:PT1H
</span><del>-ORGANIZER:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:1CB4378B-DD76-462D-B4D4-BD131FE89243
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
</del><ins>+ORGANIZER:urn:uuid:10000000-0000-0000-0000-000000000002
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:F0000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:10000000-0000-0000-0000-000000000002
</ins><span class="cx"> END:VEVENT
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> &quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;) % (future,)
</span><span class="lines">@@ -150,9 +155,9 @@
</span><span class="cx"> DTSTART:%s
</span><span class="cx"> DURATION:PT1H
</span><span class="cx"> RRULE:FREQ=DAILY;COUNT=400
</span><del>-ORGANIZER:urn:uuid:1CB4378B-DD76-462D-B4D4-BD131FE89243
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:1CB4378B-DD76-462D-B4D4-BD131FE89243
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
</del><ins>+ORGANIZER:urn:uuid:F0000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:F0000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:10000000-0000-0000-0000-000000000002
</ins><span class="cx"> END:VEVENT
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> &quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;) % (past,)
</span><span class="lines">@@ -180,10 +185,10 @@
</span><span class="cx"> SUMMARY:Organizer
</span><span class="cx"> DTSTART:%s
</span><span class="cx"> DURATION:PT1H
</span><del>-ORGANIZER:urn:uuid:767F9EB0-8A58-4F61-8163-4BE0BB72B873
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:767F9EB0-8A58-4F61-8163-4BE0BB72B873
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:42EB074A-F859-4E8F-A4D0-7F0ADCB73D87
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
</del><ins>+ORGANIZER:urn:uuid:F0000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:F0000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:F0000000-0000-0000-0000-000000000002
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:10000000-0000-0000-0000-000000000002
</ins><span class="cx"> END:VEVENT
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> &quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;) % (future,)
</span><span class="lines">@@ -197,10 +202,10 @@
</span><span class="cx"> SUMMARY:Attendee
</span><span class="cx"> DTSTART:%s
</span><span class="cx"> DURATION:PT1H
</span><del>-ORGANIZER:urn:uuid:42EB074A-F859-4E8F-A4D0-7F0ADCB73D87
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:767F9EB0-8A58-4F61-8163-4BE0BB72B873
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:42EB074A-F859-4E8F-A4D0-7F0ADCB73D87
</del><ins>+ORGANIZER:urn:uuid:F0000000-0000-0000-0000-000000000002
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:F0000000-0000-0000-0000-000000000002
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:10000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:F0000000-0000-0000-0000-000000000001
</ins><span class="cx"> END:VEVENT
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> &quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;) % (future,)
</span><span class="lines">@@ -213,531 +218,16 @@
</span><span class="cx"> SUMMARY:2 non-existent attendees
</span><span class="cx"> DTSTART:%s
</span><span class="cx"> DURATION:PT1H
</span><del>-ORGANIZER:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:767F9EB0-8A58-4F61-8163-4BE0BB72B873
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:42EB074A-F859-4E8F-A4D0-7F0ADCB73D87
</del><ins>+ORGANIZER:urn:uuid:10000000-0000-0000-0000-000000000002
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:10000000-0000-0000-0000-000000000002
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:F0000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:F0000000-0000-0000-0000-000000000002
</ins><span class="cx"> END:VEVENT
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> &quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;) % (future,)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-class CancelEventTestCase(StoreTestCase):
-
-    def test_cancelRepeating(self):
-        # A repeating event where purged CUA is organizer
-        event = Component.fromString(REPEATING_1_ICS_BEFORE)
-        action = PurgePrincipalService._cancelEvent(event, DateTime(2010, 12, 6, 12, 0, 0, Timezone(utc=True)),
-            &quot;urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1&quot;)
-        self.assertEquals(action, PurgePrincipalService.CANCELEVENT_MODIFIED)
-        self.assertEquals(str(event), REPEATING_1_ICS_AFTER)
-
-
-    def test_cancelAllDayRepeating(self):
-        # A repeating All Day event where purged CUA is organizer
-        event = Component.fromString(REPEATING_2_ICS_BEFORE)
-        action = PurgePrincipalService._cancelEvent(event, DateTime(2010, 12, 6, 12, 0, 0, Timezone(utc=True)),
-            &quot;urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1&quot;)
-        self.assertEquals(action, PurgePrincipalService.CANCELEVENT_MODIFIED)
-        self.assertEquals(str(event), REPEATING_2_ICS_AFTER)
-
-
-    def test_cancelFutureEvent(self):
-        # A future event
-        event = Component.fromString(FUTURE_EVENT_ICS)
-        action = PurgePrincipalService._cancelEvent(event, DateTime(2010, 12, 6, 12, 0, 0, Timezone(utc=True)),
-            &quot;urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1&quot;)
-        self.assertEquals(action, PurgePrincipalService.CANCELEVENT_SHOULD_DELETE)
-
-
-    def test_cancelNonMeeting(self):
-        # A repeating non-meeting event
-        event = Component.fromString(REPEATING_NON_MEETING_ICS)
-        action = PurgePrincipalService._cancelEvent(event, DateTime(2010, 12, 6, 12, 0, 0, Timezone(utc=True)),
-            &quot;urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1&quot;)
-        self.assertEquals(action, PurgePrincipalService.CANCELEVENT_SHOULD_DELETE)
-
-
-    def test_cancelAsAttendee(self):
-        # A repeating meeting event where purged CUA is an attendee
-        event = Component.fromString(REPEATING_ATTENDEE_MEETING_ICS)
-        action = PurgePrincipalService._cancelEvent(event, DateTime(2010, 12, 6, 12, 0, 0, Timezone(utc=True)),
-            &quot;urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1&quot;)
-        self.assertEquals(action, PurgePrincipalService.CANCELEVENT_SHOULD_DELETE)
-
-
-    def test_cancelAsAttendeeOccurrence(self):
-        # A repeating meeting occurrence with no master, where purged CUA is
-        # an attendee
-        event = Component.fromString(INVITED_TO_OCCURRENCE_ICS)
-        action = PurgePrincipalService._cancelEvent(event, DateTime(2010, 12, 6, 12, 0, 0, Timezone(utc=True)),
-            &quot;urn:uuid:9DC04A71-E6DD-11DF-9492-0800200C9A66&quot;)
-        self.assertEquals(action, PurgePrincipalService.CANCELEVENT_SHOULD_DELETE)
-
-
-    def test_cancelAsAttendeeMultipleOccurrences(self):
-        # Multiple meeting occurrences with no master, where purged CUA is
-        # an attendee
-        event = Component.fromString(INVITED_TO_MULTIPLE_OCCURRENCES_ICS)
-        action = PurgePrincipalService._cancelEvent(event, DateTime(2010, 12, 6, 12, 0, 0, Timezone(utc=True)),
-            &quot;urn:uuid:9DC04A71-E6DD-11DF-9492-0800200C9A66&quot;)
-        self.assertEquals(action, PurgePrincipalService.CANCELEVENT_SHOULD_DELETE)
-
-# This event begins on Nov 30, 2010, has two EXDATES (Dec 3 and 9), and has two
-# overridden instances (Dec 4 and 11).  The Dec 11 one will be removed since
-# the cutoff date for this test is Dec 6.
-
-REPEATING_1_ICS_BEFORE = &quot;&quot;&quot;BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.4//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:59E260E3-1644-4BDF-BBC6-6130B0C3A520
-DTSTART;TZID=US/Pacific:20101130T100000
-DTEND;TZID=US/Pacific:20101130T110000
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICI
- PANT;SCHEDULE-STATUS=1.2:urn:uuid:3FF02D2B-07A3-4420-8570-7B7C7D07F08A
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:0F1684
- 77-CF3D-45D3-AE60-9875EA02C4D1
-CREATED:20101203T184815Z
-DTSTAMP:20101203T185019Z
-EXDATE;TZID=US/Pacific:20101203T100000
-EXDATE;TZID=US/Pacific:20101209T100000
-ORGANIZER;CN=Purge Test:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-RRULE:FREQ=DAILY;COUNT=400
-SEQUENCE:4
-SUMMARY:Repeating 1
-TRANSP:OPAQUE
-END:VEVENT
-BEGIN:VEVENT
-UID:59E260E3-1644-4BDF-BBC6-6130B0C3A520
-RECURRENCE-ID;TZID=US/Pacific:20101204T100000
-DTSTART;TZID=US/Pacific:20101204T120000
-DTEND;TZID=US/Pacific:20101204T130000
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICI
- PANT;SCHEDULE-STATUS=2.0:urn:uuid:3FF02D2B-07A3-4420-8570-7B7C7D07F08A
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:0F1684
- 77-CF3D-45D3-AE60-9875EA02C4D1
-CREATED:20101203T184815Z
-DTSTAMP:20101203T185027Z
-ORGANIZER;CN=Purge Test:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-SEQUENCE:6
-SUMMARY:Repeating 1
-TRANSP:OPAQUE
-END:VEVENT
-BEGIN:VEVENT
-UID:59E260E3-1644-4BDF-BBC6-6130B0C3A520
-RECURRENCE-ID;TZID=US/Pacific:20101211T100000
-DTSTART;TZID=US/Pacific:20101211T120000
-DTEND;TZID=US/Pacific:20101211T130000
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICI
- PANT;SCHEDULE-STATUS=2.0:urn:uuid:3FF02D2B-07A3-4420-8570-7B7C7D07F08A
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:0F1684
- 77-CF3D-45D3-AE60-9875EA02C4D1
-CREATED:20101203T184815Z
-DTSTAMP:20101203T185038Z
-ORGANIZER;CN=Purge Test:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-SEQUENCE:6
-SUMMARY:Repeating 1
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;)
-
-REPEATING_1_ICS_AFTER = &quot;&quot;&quot;BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.4//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:59E260E3-1644-4BDF-BBC6-6130B0C3A520
-DTSTART;TZID=US/Pacific:20101130T100000
-DTEND;TZID=US/Pacific:20101130T110000
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICI
- PANT;SCHEDULE-STATUS=1.2:urn:uuid:3FF02D2B-07A3-4420-8570-7B7C7D07F08A
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:0F1684
- 77-CF3D-45D3-AE60-9875EA02C4D1
-CREATED:20101203T184815Z
-DTSTAMP:20101203T185019Z
-EXDATE;TZID=US/Pacific:20101203T100000
-ORGANIZER;CN=Purge Test:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-RRULE:FREQ=DAILY;UNTIL=20101206T120000Z
-SEQUENCE:4
-SUMMARY:Repeating 1
-TRANSP:OPAQUE
-END:VEVENT
-BEGIN:VEVENT
-UID:59E260E3-1644-4BDF-BBC6-6130B0C3A520
-RECURRENCE-ID;TZID=US/Pacific:20101204T100000
-DTSTART;TZID=US/Pacific:20101204T120000
-DTEND;TZID=US/Pacific:20101204T130000
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICI
- PANT;SCHEDULE-STATUS=2.0:urn:uuid:3FF02D2B-07A3-4420-8570-7B7C7D07F08A
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:0F1684
- 77-CF3D-45D3-AE60-9875EA02C4D1
-CREATED:20101203T184815Z
-DTSTAMP:20101203T185027Z
-ORGANIZER;CN=Purge Test:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-SEQUENCE:6
-SUMMARY:Repeating 1
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;)
-
-# This event is similar to the &quot;Repeating 1&quot; event above except this one is an
-# all-day event.
-
-REPEATING_2_ICS_BEFORE = &quot;&quot;&quot;BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.4//EN
-BEGIN:VEVENT
-UID:53BA0EA4-05B1-4E89-BD1E-8397F071FD6A
-DTSTART;VALUE=DATE:20101130
-DTEND;VALUE=DATE:20101201
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICI
- PANT;SCHEDULE-STATUS=1.2:urn:uuid:3FF02D2B-07A3-4420-8570-7B7C7D07F08A
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:0F1684
- 77-CF3D-45D3-AE60-9875EA02C4D1
-CREATED:20101203T203510Z
-DTSTAMP:20101203T203603Z
-EXDATE;VALUE=DATE:20101203
-EXDATE;VALUE=DATE:20101209
-ORGANIZER;CN=Purge Test:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-RRULE:FREQ=DAILY;COUNT=400
-SEQUENCE:5
-SUMMARY:All Day
-TRANSP:TRANSPARENT
-END:VEVENT
-BEGIN:VEVENT
-UID:53BA0EA4-05B1-4E89-BD1E-8397F071FD6A
-RECURRENCE-ID;VALUE=DATE:20101211
-DTSTART;VALUE=DATE:20101211
-DTEND;VALUE=DATE:20101212
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:0F1684
- 77-CF3D-45D3-AE60-9875EA02C4D1
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICI
- PANT;SCHEDULE-STATUS=1.2:urn:uuid:3FF02D2B-07A3-4420-8570-7B7C7D07F08A
-CREATED:20101203T203510Z
-DTSTAMP:20101203T203631Z
-ORGANIZER;CN=Purge Test:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-SEQUENCE:6
-SUMMARY:Modified Title
-TRANSP:TRANSPARENT
-END:VEVENT
-BEGIN:VEVENT
-UID:53BA0EA4-05B1-4E89-BD1E-8397F071FD6A
-RECURRENCE-ID;VALUE=DATE:20101204
-DTSTART;VALUE=DATE:20101204
-DTEND;VALUE=DATE:20101205
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICI
- PANT;SCHEDULE-STATUS=1.2:urn:uuid:3FF02D2B-07A3-4420-8570-7B7C7D07F08A
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:0F1684
- 77-CF3D-45D3-AE60-9875EA02C4D1
-CREATED:20101203T203510Z
-DTSTAMP:20101203T203618Z
-ORGANIZER;CN=Purge Test:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-SEQUENCE:7
-SUMMARY:Modified Title
-TRANSP:TRANSPARENT
-END:VEVENT
-END:VCALENDAR
-&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;)
-
-REPEATING_2_ICS_AFTER = &quot;&quot;&quot;BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.4//EN
-BEGIN:VEVENT
-UID:53BA0EA4-05B1-4E89-BD1E-8397F071FD6A
-DTSTART;VALUE=DATE:20101130
-DTEND;VALUE=DATE:20101201
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICI
- PANT;SCHEDULE-STATUS=1.2:urn:uuid:3FF02D2B-07A3-4420-8570-7B7C7D07F08A
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:0F1684
- 77-CF3D-45D3-AE60-9875EA02C4D1
-CREATED:20101203T203510Z
-DTSTAMP:20101203T203603Z
-EXDATE;VALUE=DATE:20101203
-ORGANIZER;CN=Purge Test:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-RRULE:FREQ=DAILY;UNTIL=20101206
-SEQUENCE:5
-SUMMARY:All Day
-TRANSP:TRANSPARENT
-END:VEVENT
-BEGIN:VEVENT
-UID:53BA0EA4-05B1-4E89-BD1E-8397F071FD6A
-RECURRENCE-ID;VALUE=DATE:20101204
-DTSTART;VALUE=DATE:20101204
-DTEND;VALUE=DATE:20101205
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICI
- PANT;SCHEDULE-STATUS=1.2:urn:uuid:3FF02D2B-07A3-4420-8570-7B7C7D07F08A
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:0F1684
- 77-CF3D-45D3-AE60-9875EA02C4D1
-CREATED:20101203T203510Z
-DTSTAMP:20101203T203618Z
-ORGANIZER;CN=Purge Test:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-SEQUENCE:7
-SUMMARY:Modified Title
-TRANSP:TRANSPARENT
-END:VEVENT
-END:VCALENDAR
-&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;)
-
-
-# This event is on Dec 8 (in the future compared to Dec 6) and should be flagged
-# as needing to be deleted
-
-FUTURE_EVENT_ICS = &quot;&quot;&quot;BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.4//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:97B243D3-D252-4034-AA6D-9AE34E063991
-DTSTART;TZID=US/Pacific:20101208T091500
-DTEND;TZID=US/Pacific:20101208T101500
-CREATED:20101203T172929Z
-DTSTAMP:20101203T172932Z
-SEQUENCE:2
-SUMMARY:Future event single
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;)
-
-REPEATING_NON_MEETING_ICS = &quot;&quot;&quot;BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.4//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:4E4D0C8C-6546-4777-9BF5-AD629C05E7D5
-DTSTART;TZID=US/Pacific:20101130T110000
-DTEND;TZID=US/Pacific:20101130T120000
-CREATED:20101203T204353Z
-DTSTAMP:20101203T204409Z
-RRULE:FREQ=DAILY;COUNT=400
-SEQUENCE:3
-SUMMARY:Repeating non meeting
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;)
-
-REPEATING_ATTENDEE_MEETING_ICS = &quot;&quot;&quot;BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:111A679F-EF8E-4CA5-9262-7C805E2C184D
-DTSTART;TZID=US/Pacific:20101130T120000
-DTEND;TZID=US/Pacific:20101130T130000
-ATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:3FF02D2
- B-07A3-4420-8570-7B7C7D07F08A
-ATTENDEE;CN=Purge Test;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTIC
- IPANT:urn:uuid:0F168477-CF3D-45D3-AE60-9875EA02C4D1
-CREATED:20101203T204908Z
-DTSTAMP:20101203T204927Z
-ORGANIZER;CN=Test User;SCHEDULE-STATUS=1.2:urn:uuid:3FF02D2B-07A3-4420-857
- 0-7B7C7D07F08A
-RRULE:FREQ=DAILY;COUNT=400
-SEQUENCE:4
-SUMMARY:As an attendee
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;)
-
-INVITED_TO_OCCURRENCE_ICS = &quot;&quot;&quot;BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VTIMEZONE
-TZID:America/Los_Angeles
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:44A391CF-52F5-46B4-B35A-E000E3002084
-RECURRENCE-ID;TZID=America/Los_Angeles:20111103T150000
-DTSTART;TZID=America/Los_Angeles:20111103T150000
-DTEND;TZID=America/Los_Angeles:20111103T170000
-ATTENDEE;CN=Betty Test;CUTYPE=INDIVIDUAL;EMAIL=betty@example.com;PARTSTAT=
- NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:urn:uuid:9DC04A71-E6DD-11DF-94
- 92-0800200C9A66
-ATTENDEE;CN=Amanda Test;CUTYPE=INDIVIDUAL;EMAIL=amanda@example.com;PARTSTA
- T=ACCEPTED:urn:uuid:9DC04A70-E6DD-11DF-9492-0800200C9A66
-CREATED:20111101T205355Z
-DTSTAMP:20111101T205506Z
-ORGANIZER;CN=Amanda Test;EMAIL=amanda@example.com:urn:uuid:9DC04A70-E6DD-1
- 1DF-9492-0800200C9A66
-SEQUENCE:5
-SUMMARY:Repeating
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;)
-
-
-INVITED_TO_MULTIPLE_OCCURRENCES_ICS = &quot;&quot;&quot;BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VTIMEZONE
-TZID:America/Los_Angeles
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:44A391CF-52F5-46B4-B35A-E000E3002084
-RECURRENCE-ID;TZID=America/Los_Angeles:20111103T150000
-DTSTART;TZID=America/Los_Angeles:20111103T150000
-DTEND;TZID=America/Los_Angeles:20111103T170000
-ATTENDEE;CN=Betty Test;CUTYPE=INDIVIDUAL;EMAIL=betty@example.com;PARTSTAT=
- NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:urn:uuid:9DC04A71-E6DD-11DF-94
- 92-0800200C9A66
-ATTENDEE;CN=Amanda Test;CUTYPE=INDIVIDUAL;EMAIL=amanda@example.com;PARTSTA
- T=ACCEPTED:urn:uuid:9DC04A70-E6DD-11DF-9492-0800200C9A66
-CREATED:20111101T205355Z
-DTSTAMP:20111101T205506Z
-ORGANIZER;CN=Amanda Test;EMAIL=amanda@example.com:urn:uuid:9DC04A70-E6DD-1
- 1DF-9492-0800200C9A66
-SEQUENCE:5
-SUMMARY:Repeating
-TRANSP:OPAQUE
-END:VEVENT
-BEGIN:VEVENT
-ATTENDEE;CN=&quot;Amanda Test&quot;;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:9
- DC04A70-E6DD-11DF-9492-0800200C9A66
-ATTENDEE;CN=&quot;Betty Test&quot;;CUTYPE=INDIVIDUAL;EMAIL=&quot;betty@example.com&quot;;PAR
- TSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:betty@example.c
- om
-DTEND;TZID=America/Los_Angeles:20111105T170000
-TRANSP:OPAQUE
-ORGANIZER;CN=&quot;Amanda Test&quot;:urn:uuid:9DC04A70-E6DD-11DF-9492-0800200C9A66
-UID:44A391CF-52F5-46B4-B35A-E000E3002084
-DTSTAMP:20111102T162426Z
-SEQUENCE:5
-RECURRENCE-ID;TZID=America/Los_Angeles:20111105T150000
-SUMMARY:Repeating
-DTSTART;TZID=America/Los_Angeles:20111105T150000
-CREATED:20111101T205355Z
-END:VEVENT
-END:VCALENDAR
-&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;)
-
-
</del><span class="cx"> ATTACHMENT_ICS = &quot;&quot;&quot;BEGIN:VCALENDAR
</span><span class="cx"> VERSION:2.0
</span><span class="cx"> PRODID:-//Apple Inc.//iCal 4.0.1//EN
</span><span class="lines">@@ -785,21 +275,22 @@
</span><span class="cx"> DTSTART:%s
</span><span class="cx"> DURATION:PT1H
</span><span class="cx"> RRULE:FREQ=DAILY;COUNT=400
</span><del>-ORGANIZER:urn:uuid:6423F94A-6B76-4A3A-815B-D52CFD77935D
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:6423F94A-6B76-4A3A-815B-D52CFD77935D
-ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
</del><ins>+ORGANIZER:urn:x-uid:user01
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:x-uid:user02
</ins><span class="cx"> DTSTAMP:20100303T195203Z
</span><span class="cx"> END:VEVENT
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> &quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;) % (past,)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx"> class PurgePrincipalTests(StoreTestCase):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Tests for purging the data belonging to a given principal
</span><span class="cx">     &quot;&quot;&quot;
</span><del>-    uid = &quot;6423F94A-6B76-4A3A-815B-D52CFD77935D&quot;
-    uid2 = &quot;37DB0C90-4DB1-4932-BC69-3DAB66F374F5&quot;
</del><ins>+    uid = &quot;user01&quot;
+    uid2 = &quot;user02&quot;
</ins><span class="cx"> 
</span><span class="cx">     metadata = {
</span><span class="cx">         &quot;accessMode&quot;: &quot;PUBLIC&quot;,
</span><span class="lines">@@ -814,11 +305,14 @@
</span><span class="cx">             &quot;calendar1&quot; : {
</span><span class="cx">                 &quot;attachment.ics&quot; : (ATTACHMENT_ICS, metadata,),
</span><span class="cx">                 &quot;organizer.ics&quot; : (REPEATING_PUBLIC_EVENT_ORGANIZER_ICS, metadata,),
</span><del>-            }
</del><ins>+            },
+            &quot;inbox&quot;: {},
</ins><span class="cx">         },
</span><span class="cx">         uid2 : {
</span><span class="cx">             &quot;calendar2&quot; : {
</span><del>-            }
</del><ins>+                &quot;attendee.ics&quot; : (REPEATING_PUBLIC_EVENT_ORGANIZER_ICS, metadata,),
+            },
+            &quot;inbox&quot;: {},
</ins><span class="cx">         },
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -857,7 +351,15 @@
</span><span class="cx">         self.assertNotEquals(calendar1, None)
</span><span class="cx">         yield txn.commit()
</span><span class="cx"> 
</span><ins>+        # Now remove user01
+        yield self.directory.removeRecords((self.uid,))
+        self.patch(config.Scheduling.Options.WorkQueues, &quot;Enabled&quot;, False)
+        self.patch(config.AutomaticPurging, &quot;PollingIntervalSeconds&quot;, -1)
+        self.patch(config.AutomaticPurging, &quot;CheckStaggerSeconds&quot;, 1)
+        self.patch(config.AutomaticPurging, &quot;PurgeIntervalSeconds&quot;, 3)
+        self.patch(config.AutomaticPurging, &quot;HomePurgeDelaySeconds&quot;, 1)
</ins><span class="cx"> 
</span><ins>+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def populate(self):
</span><span class="cx">         yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
</span><span class="lines">@@ -871,23 +373,37 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Now you see it
</span><del>-        txn = self._sqlCalendarStore.newTransaction()
-        home = yield txn.calendarHomeWithUID(self.uid)
</del><ins>+        home = yield self.homeUnderTest(name=self.uid)
</ins><span class="cx">         self.assertNotEquals(home, None)
</span><del>-        yield txn.commit()
</del><span class="cx"> 
</span><ins>+        calobj2 = yield self.calendarObjectUnderTest(name=&quot;attendee.ics&quot;, calendar_name=&quot;calendar2&quot;, home=self.uid2)
+        comp = yield calobj2.componentForUser()
+        self.assertTrue(&quot;STATUS:CANCELLED&quot; not in str(comp))
+        self.assertTrue(&quot;;UNTIL=&quot; not in str(comp))
+        yield self.commit()
+
</ins><span class="cx">         count = (yield PurgePrincipalService.purgeUIDs(self.storeUnderTest(), self.directory,
</span><del>-            (self.uid,), verbose=False, proxies=False, completely=True))
</del><ins>+            (self.uid,), verbose=False, proxies=False))
</ins><span class="cx">         self.assertEquals(count, 2) # 2 events
</span><span class="cx"> 
</span><ins>+        # Wait for queue to process
+        while(True):
+            txn = self.transactionUnderTest()
+            work = yield PrincipalPurgeHomeWork.all(txn)
+            yield self.commit()
+            if len(work) == 0:
+                break
+            d = Deferred()
+            reactor.callLater(1, lambda : d.callback(None))
+            yield d
+
</ins><span class="cx">         # Now you don't
</span><del>-        txn = self._sqlCalendarStore.newTransaction()
-        home = yield txn.calendarHomeWithUID(self.uid)
</del><ins>+        home = yield self.homeUnderTest(name=self.uid)
</ins><span class="cx">         self.assertEquals(home, None)
</span><span class="cx">         # Verify calendar1 was unshared to uid2
</span><del>-        home2 = yield txn.calendarHomeWithUID(self.uid2)
</del><ins>+        home2 = yield self.homeUnderTest(name=self.uid2)
</ins><span class="cx">         self.assertEquals((yield home2.childWithName(self.sharedName)), None)
</span><del>-        yield txn.commit()
</del><ins>+        yield self.commit()
</ins><span class="cx"> 
</span><span class="cx">         count = yield PurgePrincipalService.purgeUIDs(
</span><span class="cx">             self.storeUnderTest(),
</span><span class="lines">@@ -895,50 +411,89 @@
</span><span class="cx">             (self.uid,),
</span><span class="cx">             verbose=False,
</span><span class="cx">             proxies=False,
</span><del>-            completely=True
</del><span class="cx">         )
</span><span class="cx">         self.assertEquals(count, 0)
</span><span class="cx"> 
</span><span class="cx">         # And you still don't (making sure it's not provisioned)
</span><del>-        txn = self._sqlCalendarStore.newTransaction()
-        home = yield txn.calendarHomeWithUID(self.uid)
</del><ins>+        home = yield self.homeUnderTest(name=self.uid)
</ins><span class="cx">         self.assertEquals(home, None)
</span><del>-        yield txn.commit()
</del><ins>+        yield self.commit()
</ins><span class="cx"> 
</span><ins>+        calobj2 = yield self.calendarObjectUnderTest(name=&quot;attendee.ics&quot;, calendar_name=&quot;calendar2&quot;, home=self.uid2)
+        comp = yield calobj2.componentForUser()
+        self.assertTrue(&quot;STATUS:CANCELLED&quot; in str(comp))
+        self.assertTrue(&quot;;UNTIL=&quot; not in str(comp))
+        yield self.commit()
</ins><span class="cx"> 
</span><ins>+
+
+class PurgePrincipalTestsWithWorkQueue(PurgePrincipalTests):
+    &quot;&quot;&quot;
+    Same as L{PurgePrincipalTests} but with the work queue enabled.
+    &quot;&quot;&quot;
+
</ins><span class="cx">     @inlineCallbacks
</span><del>-    def test_purgeUIDsNotCompletely(self):
</del><ins>+    def setUp(self):
+        yield super(PurgePrincipalTestsWithWorkQueue, self).setUp()
+        self.patch(config.Scheduling.Options.WorkQueues, &quot;Enabled&quot;, True)
+        self.patch(config.AutomaticPurging, &quot;PollingIntervalSeconds&quot;, -1)
+        self.patch(config.AutomaticPurging, &quot;CheckStaggerSeconds&quot;, 1)
+        self.patch(config.AutomaticPurging, &quot;PurgeIntervalSeconds&quot;, 3)
+        self.patch(config.AutomaticPurging, &quot;HomePurgeDelaySeconds&quot;, 1)
+
+
+    @inlineCallbacks
+    def test_purgeUIDService(self):
</ins><span class="cx">         &quot;&quot;&quot;
</span><del>-        Verify purgeUIDs removes some events, but leaves others and the home behind
</del><ins>+        Test that the full sequence of work items are processed via automatic polling.
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        self.patch(config, &quot;EnablePrivateEvents&quot;, True)
-
</del><span class="cx">         # Now you see it
</span><del>-        txn = self._sqlCalendarStore.newTransaction()
-        home = (yield txn.calendarHomeWithUID(self.uid))
</del><ins>+        home = yield self.homeUnderTest(name=self.uid)
</ins><span class="cx">         self.assertNotEquals(home, None)
</span><del>-        yield txn.commit()
</del><span class="cx"> 
</span><del>-        count = (yield PurgePrincipalService.purgeUIDs(self.storeUnderTest(), self.directory,
-            (self.uid,), verbose=False, proxies=False, completely=False))
-        self.assertEquals(count, 1) # 2 events
</del><ins>+        calobj2 = yield self.calendarObjectUnderTest(name=&quot;attendee.ics&quot;, calendar_name=&quot;calendar2&quot;, home=self.uid2)
+        comp = yield calobj2.componentForUser()
+        self.assertTrue(&quot;STATUS:CANCELLED&quot; not in str(comp))
+        self.assertTrue(&quot;;UNTIL=&quot; not in str(comp))
+        yield self.commit()
</ins><span class="cx"> 
</span><del>-        # Now you still see it
-        txn = self._sqlCalendarStore.newTransaction()
-        home = (yield txn.calendarHomeWithUID(self.uid))
-        self.assertNotEquals(home, None)
</del><ins>+        txn = self.transactionUnderTest()
+        notBefore = (
+            datetime.datetime.utcnow() +
+            datetime.timedelta(seconds=3)
+        )
+        yield txn.enqueue(PrincipalPurgePollingWork, notBefore=notBefore)
+        yield self.commit()
+
+        while True:
+            txn = self.transactionUnderTest()
+            work1 = yield PrincipalPurgePollingWork.all(txn)
+            work2 = yield PrincipalPurgeCheckWork.all(txn)
+            work3 = yield PrincipalPurgeWork.all(txn)
+            work4 = yield PrincipalPurgeHomeWork.all(txn)
+
+            if len(work4) != 0:
+                home = yield txn.calendarHomeWithUID(self.uid)
+                self.assertTrue(home.purging())
+
+            yield self.commit()
+            #print len(work1), len(work2), len(work3), len(work4)
+            if len(work1) + len(work2) + len(work3) + len(work4) == 0:
+                break
+            d = Deferred()
+            reactor.callLater(1, lambda : d.callback(None))
+            yield d
+
+        # Now you don't
+        home = yield self.homeUnderTest(name=self.uid)
+        self.assertEquals(home, None)
</ins><span class="cx">         # Verify calendar1 was unshared to uid2
</span><del>-        home2 = (yield txn.calendarHomeWithUID(self.uid2))
</del><ins>+        home2 = yield self.homeUnderTest(name=self.uid2)
</ins><span class="cx">         self.assertEquals((yield home2.childWithName(self.sharedName)), None)
</span><del>-        yield txn.commit()
</del><span class="cx"> 
</span><del>-        count = yield PurgePrincipalService.purgeUIDs(self.storeUnderTest(), self.directory,
-            (self.uid,), verbose=False, proxies=False, completely=False)
-        self.assertEquals(count, 1)
-
-        # And you still do
-        txn = self._sqlCalendarStore.newTransaction()
-        home = (yield txn.calendarHomeWithUID(self.uid))
-        self.assertNotEquals(home, None)
-        yield txn.commit()
</del><ins>+        calobj2 = yield self.calendarObjectUnderTest(name=&quot;attendee.ics&quot;, calendar_name=&quot;calendar2&quot;, home=self.uid2)
+        comp = yield calobj2.componentForUser()
+        self.assertTrue(&quot;STATUS:CANCELLED&quot; in str(comp))
+        self.assertTrue(&quot;;UNTIL=&quot; not in str(comp))
+        yield self.commit()
</ins></span></pre></div>
<a id="CalendarServertrunkcalendarservertoolstesttest_purge_old_eventspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -21,12 +21,12 @@
</span><span class="cx"> import os
</span><span class="cx"> 
</span><span class="cx"> from calendarserver.tools.purge import (
</span><del>-    PurgeOldEventsService, PurgeAttachmentsService, PurgePrincipalService
</del><ins>+    PurgeOldEventsService, PurgeAttachmentsService, PurgePrincipalService, PrincipalPurgeHomeWork
</ins><span class="cx"> )
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><del>-from pycalendar.timezone import Timezone
</del><span class="cx"> from twext.enterprise.dal.syntax import Update, Delete
</span><del>-from twisted.internet.defer import inlineCallbacks, returnValue
</del><ins>+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
</ins><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.test.util import StoreTestCase
</span><span class="cx"> from twistedcaldav.vcard import Component as VCardComponent
</span><span class="lines">@@ -385,7 +385,8 @@
</span><span class="cx">                 &quot;oldmattachment1.ics&quot; : (MATTACHMENT_ICS % {&quot;year&quot;: now - 5, &quot;uid&quot;: &quot;1.1m&quot;}, metadata,),
</span><span class="cx">                 &quot;oldmattachment2.ics&quot; : (MATTACHMENT_ICS % {&quot;year&quot;: now - 5, &quot;uid&quot;: &quot;1.2m&quot;}, metadata,),
</span><span class="cx">                 &quot;currentmattachment3.ics&quot; : (MATTACHMENT_ICS % {&quot;year&quot;: now + 1, &quot;uid&quot;: &quot;1.3m&quot;}, metadata,),
</span><del>-            }
</del><ins>+            },
+            &quot;inbox&quot;: {},
</ins><span class="cx">         },
</span><span class="cx">         &quot;home2&quot; : {
</span><span class="cx">             &quot;calendar2&quot; : {
</span><span class="lines">@@ -402,16 +403,25 @@
</span><span class="cx">             },
</span><span class="cx">             &quot;calendar3&quot; : {
</span><span class="cx">                 &quot;repeating_awhile.ics&quot; : (REPEATING_AWHILE_ICS, metadata,),
</span><del>-            }
</del><ins>+            },
+            &quot;inbox&quot;: {},
</ins><span class="cx">         }
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx">     def configure(self):
</span><span class="cx">         super(PurgeOldEventsTests, self).configure()
</span><span class="cx"> 
</span><span class="cx">         # Turn off delayed indexing option so we can have some useful tests
</span><span class="cx">         self.patch(config, &quot;FreeBusyIndexDelayedExpand&quot;, False)
</span><span class="cx"> 
</span><ins>+        # Tweak queue timing to speed things up
+        self.patch(config.Scheduling.Options.WorkQueues, &quot;Enabled&quot;, False)
+        self.patch(config.AutomaticPurging, &quot;PollingIntervalSeconds&quot;, -1)
+        self.patch(config.AutomaticPurging, &quot;CheckStaggerSeconds&quot;, 1)
+        self.patch(config.AutomaticPurging, &quot;PurgeIntervalSeconds&quot;, 3)
+        self.patch(config.AutomaticPurging, &quot;HomePurgeDelaySeconds&quot;, 1)
+
</ins><span class="cx">         # self.patch(config.DirectoryService.params, &quot;xmlFile&quot;,
</span><span class="cx">         #     os.path.join(
</span><span class="cx">         #         os.path.dirname(__file__), &quot;purge&quot;, &quot;accounts.xml&quot;
</span><span class="lines">@@ -675,47 +685,21 @@
</span><span class="cx">         # Make the newly created objects available to the purgeUID transaction
</span><span class="cx">         (yield txn.commit())
</span><span class="cx"> 
</span><del>-        # Purge home1
-        total = yield PurgePrincipalService.purgeUIDs(self._sqlCalendarStore, self.directory,
-            (&quot;home1&quot;,), verbose=False, proxies=False,
-            when=DateTime(now, 4, 1, 12, 0, 0, 0, Timezone(utc=True)))
-
-        # 4 items deleted: 3 events and 1 vcard
-        self.assertEquals(total, 4)
-
-        txn = self._sqlCalendarStore.newTransaction()
-        # adressbook home is deleted since it's now empty
-        abHome = (yield txn.addressbookHomeWithUID(&quot;home1&quot;))
-        self.assertEquals(abHome, None)
-
-        calHome = (yield txn.calendarHomeWithUID(&quot;home1&quot;))
-        calColl = (yield calHome.calendarWithName(&quot;calendar1&quot;))
-        self.assertEquals(len((yield calColl.calendarObjects())), 5)
-
-
-    @inlineCallbacks
-    def test_purgeUIDCompletely(self):
-        txn = self._sqlCalendarStore.newTransaction()
-
-        # Create an addressbook and one CardDAV resource
-        abHome = (yield txn.addressbookHomeWithUID(&quot;home1&quot;, create=True))
-        abColl = (yield abHome.addressbookWithName(&quot;addressbook&quot;))
-        (yield abColl.createAddressBookObjectWithName(&quot;card1&quot;,
-            VCardComponent.fromString(VCARD_1)))
-        self.assertEquals(len((yield abColl.addressbookObjects())), 1)
-
-        # Verify there are 8 events in calendar1
-        calHome = (yield txn.calendarHomeWithUID(&quot;home1&quot;))
-        calColl = (yield calHome.calendarWithName(&quot;calendar1&quot;))
-        self.assertEquals(len((yield calColl.calendarObjects())), 8)
-
-        # Make the newly created objects available to the purgeUID transaction
-        (yield txn.commit())
-
</del><span class="cx">         # Purge home1 completely
</span><span class="cx">         total = yield PurgePrincipalService.purgeUIDs(self._sqlCalendarStore, self.directory,
</span><del>-            (&quot;home1&quot;,), verbose=False, proxies=False, completely=True)
</del><ins>+            (&quot;home1&quot;,), verbose=False, proxies=False)
</ins><span class="cx"> 
</span><ins>+        # Wait for queue to process
+        while(True):
+            txn = self.transactionUnderTest()
+            work = yield PrincipalPurgeHomeWork.all(txn)
+            yield self.commit()
+            if len(work) == 0:
+                break
+            d = Deferred()
+            reactor.callLater(1, lambda : d.callback(None))
+            yield d
+
</ins><span class="cx">         # 9 items deleted: 8 events and 1 vcard
</span><span class="cx">         self.assertEquals(total, 9)
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavdirectoryprincipalpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/directory/principal.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -54,7 +54,7 @@
</span><span class="cx"> )
</span><span class="cx"> from twistedcaldav.extensions import DirectoryElement
</span><span class="cx"> from twistedcaldav.resource import CalendarPrincipalCollectionResource, CalendarPrincipalResource
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
</del><ins>+from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr
</ins><span class="cx"> from txdav.who.delegates import RecordType as DelegateRecordType
</span><span class="cx"> from txdav.who.directory import CalendarDirectoryRecordMixin
</span><span class="cx"> from txdav.xml import element as davxml
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavicalpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/ical.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/ical.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/twistedcaldav/ical.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -45,9 +45,10 @@
</span><span class="cx"> from twistedcaldav.dateops import timeRangesOverlap, normalizeForIndex, differenceDateTime, \
</span><span class="cx">     normalizeForExpand
</span><span class="cx"> from twistedcaldav.instance import InstanceList, InvalidOverriddenInstanceError
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
</del><span class="cx"> from twistedcaldav.timezones import hasTZ, TimezoneException
</span><span class="cx"> 
</span><ins>+from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr
+
</ins><span class="cx"> from pycalendar.icalendar import definitions
</span><span class="cx"> from pycalendar.parameter import Parameter
</span><span class="cx"> from pycalendar.icalendar.calendar import Calendar
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavstdconfigpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/stdconfig.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -1029,6 +1029,14 @@
</span><span class="cx">         &quot;ReconciliationDelaySeconds&quot; : 5,
</span><span class="cx">     },
</span><span class="cx"> 
</span><ins>+    &quot;AutomaticPurging&quot;: {
+        &quot;Enabled&quot;: False,
+        &quot;PollingIntervalSeconds&quot;: 7 * 24 * 60 * 60,   # 7 days
+        &quot;CheckStaggerSeconds&quot;: 0, # No staggering
+        &quot;PurgeIntervalSeconds&quot;: 7 * 24 * 60 * 60,   # 7 days
+        &quot;HomePurgeDelaySeconds&quot;: 60,
+    },
+
</ins><span class="cx">     &quot;Manhole&quot;: {
</span><span class="cx">         &quot;Enabled&quot;: False,
</span><span class="cx">         &quot;StartingPortNumber&quot;: 5000,
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingaddressmappingpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/addressmapping.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/addressmapping.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/addressmapping.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -22,11 +22,10 @@
</span><span class="cx"> from twistedcaldav.memcacher import Memcacher
</span><span class="cx"> 
</span><span class="cx"> from txdav.caldav.datastore.scheduling.caldav.delivery import ScheduleViaCalDAV
</span><ins>+from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser, EmailCalendarUser, InvalidCalendarUser
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.delivery import DeliveryService
</span><span class="cx"> from txdav.caldav.datastore.scheduling.imip.delivery import ScheduleViaIMip
</span><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.delivery import ScheduleViaISchedule
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser, EmailCalendarUser, InvalidCalendarUser, \
-    calendarUserFromPrincipal
</del><span class="cx"> 
</span><span class="cx"> __all__ = [
</span><span class="cx">     &quot;ScheduleAddressMapper&quot;,
</span><span class="lines">@@ -51,12 +50,8 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def getCalendarUser(self, cuaddr, principal):
</del><ins>+    def getCalendarUser(self, cuaddr):
</ins><span class="cx"> 
</span><del>-        # If we have a principal always treat the user as local
-        if principal:
-            returnValue(calendarUserFromPrincipal(cuaddr, principal))
-
</del><span class="cx">         # Get the type
</span><span class="cx">         cuaddr_type = (yield self.getCalendarUserServiceType(cuaddr))
</span><span class="cx">         if cuaddr_type == DeliveryService.serviceType_caldav:
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingcaldavdeliverypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -99,7 +99,7 @@
</span><span class="cx"> 
</span><span class="cx">         organizerPrincipal = None
</span><span class="cx">         if type(self.scheduler.organizer) in (LocalCalendarUser, OtherServerCalendarUser,):
</span><del>-            organizerPrincipal = self.scheduler.organizer.principal.uid
</del><ins>+            organizerPrincipal = self.scheduler.organizer.record.uid
</ins><span class="cx"> 
</span><span class="cx">         for recipient in self.recipients:
</span><span class="cx"> 
</span><span class="lines">@@ -187,7 +187,7 @@
</span><span class="cx">     def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid, event_details):
</span><span class="cx"> 
</span><span class="cx">         # Extract the ATTENDEE property matching current recipient from the calendar data
</span><del>-        cuas = recipient.principal.calendarUserAddresses
</del><ins>+        cuas = recipient.record.calendarUserAddresses
</ins><span class="cx">         attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
</span><span class="cx"> 
</span><span class="cx">         remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
</span><span class="lines">@@ -250,7 +250,7 @@
</span><span class="cx">         # Check to see if the recipient is the same calendar user as the organizer.
</span><span class="cx">         # Needed for masked UID stuff.
</span><span class="cx">         if isinstance(self.scheduler.organizer, LocalCalendarUser):
</span><del>-            same_calendar_user = self.scheduler.organizer.principal.uid == recipient.principal.uid
</del><ins>+            same_calendar_user = self.scheduler.organizer.record.uid == recipient.record.uid
</ins><span class="cx">         else:
</span><span class="cx">             same_calendar_user = False
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingcaldavschedulerpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/scheduler.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/scheduler.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/scheduler.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -26,7 +26,8 @@
</span><span class="cx"> 
</span><span class="cx"> from txdav.caldav.datastore.scheduling import addressmapping
</span><span class="cx"> from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser, \
</span><del>-    InvalidCalendarUser, calendarUserFromPrincipal, RemoteCalendarUser
</del><ins>+    OtherServerCalendarUser, InvalidCalendarUser, \
+    calendarUserFromCalendarUserAddress
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.scheduler import Scheduler, ScheduleResponseQueue
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -92,8 +93,8 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Verify that Originator is a valid calendar user
</span><del>-        originatorPrincipal = yield self.txn.directoryService().recordWithCalendarUserAddress(self.originator)
-        if originatorPrincipal is None:
</del><ins>+        originatorAddress = yield calendarUserFromCalendarUserAddress(self.originator, self.txn)
+        if not originatorAddress.hosted():
</ins><span class="cx">             # Local requests MUST have a principal.
</span><span class="cx">             log.error(&quot;Could not find principal for originator: %s&quot; % (self.originator,))
</span><span class="cx">             raise HTTPError(self.errorResponse(
</span><span class="lines">@@ -102,7 +103,7 @@
</span><span class="cx">                 &quot;No principal for originator&quot;,
</span><span class="cx">             ))
</span><span class="cx">         else:
</span><del>-            if not (originatorPrincipal.calendarsEnabled() and originatorPrincipal.thisServer()):
</del><ins>+            if not originatorAddress.validOriginator() or isinstance(originatorAddress, OtherServerCalendarUser):
</ins><span class="cx">                 log.error(&quot;Originator not enabled or hosted on this server: %s&quot; % (self.originator,))
</span><span class="cx">                 raise HTTPError(self.errorResponse(
</span><span class="cx">                     responsecode.FORBIDDEN,
</span><span class="lines">@@ -110,7 +111,7 @@
</span><span class="cx">                     &quot;Originator cannot be scheduled&quot;,
</span><span class="cx">                 ))
</span><span class="cx"> 
</span><del>-            self.originator = LocalCalendarUser(self.originator, originatorPrincipal)
</del><ins>+            self.originator = originatorAddress
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -122,32 +123,32 @@
</span><span class="cx"> 
</span><span class="cx">         results = []
</span><span class="cx">         for recipient in self.recipients:
</span><del>-            # Get the principal resource for this recipient
-            principal = yield self.txn.directoryService().recordWithCalendarUserAddress(recipient)
</del><ins>+            # Get the calendar user object for this recipient
+            recipientAddress = yield calendarUserFromCalendarUserAddress(recipient, self.txn)
</ins><span class="cx"> 
</span><span class="cx">             # If no principal we may have a remote recipient but we should check whether
</span><span class="cx">             # the address is one that ought to be on our server and treat that as a missing
</span><span class="cx">             # user. Also if server-to-server is not enabled then remote addresses are not allowed.
</span><del>-            if principal is None:
-                address = (yield addressmapping.mapper.getCalendarUser(recipient, principal))
-                if isinstance(address, InvalidCalendarUser):
</del><ins>+            if not recipientAddress.hosted():
+                if isinstance(recipientAddress, InvalidCalendarUser):
</ins><span class="cx">                     log.error(&quot;Unknown calendar user address: %s&quot; % (recipient,))
</span><del>-                results.append(address)
</del><ins>+                results.append(recipientAddress)
</ins><span class="cx">             else:
</span><del>-                # Map recipient to their inbox
</del><ins>+                # Map recipient to their inbox and cache on calendar user object
</ins><span class="cx">                 inbox = None
</span><del>-                if principal.calendarsEnabled():
-                    if principal.thisServer():
-                        recipient_home = yield self.txn.calendarHomeWithUID(principal.uid, create=True)
</del><ins>+                if recipientAddress.validRecipient():
+                    if isinstance(recipientAddress, LocalCalendarUser):
+                        recipient_home = yield self.txn.calendarHomeWithUID(recipientAddress.record.uid, create=True)
</ins><span class="cx">                         if recipient_home:
</span><span class="cx">                             inbox = (yield recipient_home.calendarWithName(&quot;inbox&quot;))
</span><span class="cx">                     else:
</span><span class="cx">                         inbox = &quot;dummy&quot;
</span><ins>+                    recipientAddress.inbox = inbox
</ins><span class="cx"> 
</span><span class="cx">                 if inbox:
</span><del>-                    results.append(calendarUserFromPrincipal(recipient, principal, inbox))
</del><ins>+                    results.append(recipientAddress)
</ins><span class="cx">                 else:
</span><del>-                    log.error(&quot;Recipient not enabled for calendaring: %s&quot; % (principal,))
</del><ins>+                    log.error(&quot;No scheduling for calendar user: %s&quot; % (recipient,))
</ins><span class="cx">                     results.append(InvalidCalendarUser(recipient))
</span><span class="cx"> 
</span><span class="cx">         self.recipients = results
</span><span class="lines">@@ -162,14 +163,14 @@
</span><span class="cx">         # Verify that the ORGANIZER's cu address maps to a valid user
</span><span class="cx">         organizer = self.calendar.getOrganizer()
</span><span class="cx">         if organizer:
</span><del>-            organizerPrincipal = yield self.txn.directoryService().recordWithCalendarUserAddress(organizer)
-            if organizerPrincipal:
-                if organizerPrincipal.calendarsEnabled():
</del><ins>+            organizerAddress = yield calendarUserFromCalendarUserAddress(organizer, self.txn)
+            if organizerAddress.hosted():
+                if organizerAddress.record.calendarsEnabled():
</ins><span class="cx"> 
</span><span class="cx">                     # Only do this check for a freebusy request. A check for an invite needs
</span><span class="cx">                     # to be handled later when we know whether a new invite is being added
</span><span class="cx">                     # (which we reject) vs an update to an existing one (which we allow).
</span><del>-                    if self.checkForFreeBusy() and not organizerPrincipal.enabledAsOrganizer():
</del><ins>+                    if self.checkForFreeBusy() and not organizerAddress.record.enabledAsOrganizer():
</ins><span class="cx">                         log.error(&quot;ORGANIZER not allowed to be an Organizer: %s&quot; % (self.calendar,))
</span><span class="cx">                         raise HTTPError(self.errorResponse(
</span><span class="cx">                             responsecode.FORBIDDEN,
</span><span class="lines">@@ -177,9 +178,9 @@
</span><span class="cx">                             &quot;Organizer cannot schedule&quot;,
</span><span class="cx">                         ))
</span><span class="cx"> 
</span><del>-                    self.organizer = LocalCalendarUser(organizer, organizerPrincipal)
</del><ins>+                    self.organizer = organizerAddress
</ins><span class="cx">                 else:
</span><del>-                    log.error(&quot;No outbox for ORGANIZER in calendar data: %s&quot; % (self.calendar,))
</del><ins>+                    log.error(&quot;No scheduling for ORGANIZER: %s&quot; % (organizer,))
</ins><span class="cx">                     raise HTTPError(self.errorResponse(
</span><span class="cx">                         responsecode.FORBIDDEN,
</span><span class="cx">                         self.errorElements[&quot;organizer-denied&quot;],
</span><span class="lines">@@ -195,7 +196,7 @@
</span><span class="cx">                         &quot;No principal for organizer&quot;,
</span><span class="cx">                     ))
</span><span class="cx">                 else:
</span><del>-                    self.organizer = RemoteCalendarUser(organizer)
</del><ins>+                    self.organizer = organizerAddress
</ins><span class="cx">         else:
</span><span class="cx">             log.error(&quot;ORGANIZER missing in calendar data: %s&quot; % (self.calendar,))
</span><span class="cx">             raise HTTPError(self.errorResponse(
</span><span class="lines">@@ -216,8 +217,8 @@
</span><span class="cx">                 &quot;Organizer is not local to server&quot;,
</span><span class="cx">             ))
</span><span class="cx"> 
</span><del>-        # Make sure that the ORGANIZER's Outbox is the request URI
-        if self.doingPOST is not None and self.organizer.principal.uid != self.originator_uid:
</del><ins>+        # Make sure that the ORGANIZER's is the request URI owner
+        if self.doingPOST is not None and self.organizer.record.uid != self.originator_uid:
</ins><span class="cx">             log.error(&quot;Wrong outbox for ORGANIZER in calendar data: %s&quot; % (self.calendar,))
</span><span class="cx">             raise HTTPError(self.errorResponse(
</span><span class="cx">                 responsecode.FORBIDDEN,
</span><span class="lines">@@ -233,18 +234,18 @@
</span><span class="cx">         Only local attendees are allowed for message originating from this server.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        # Attendee's Outbox MUST be the request URI
-        attendeePrincipal = yield self.txn.directoryService().recordWithCalendarUserAddress(self.attendee)
-        if attendeePrincipal:
-            if self.doingPOST is not None and attendeePrincipal.uid != self.originator_uid:
-                log.error(&quot;ATTENDEE in calendar data does not match owner of Outbox: %s&quot; % (self.calendar,))
</del><ins>+        # Attendee's MUST be the request URI owner
+        attendeeAddress = yield calendarUserFromCalendarUserAddress(self.attendee, self.txn)
+        if attendeeAddress.hosted():
+            if self.doingPOST is not None and attendeeAddress.record.uid != self.originator_uid:
+                log.error(&quot;ATTENDEE in calendar data does not match owner of Outbox: %s&quot; % (self.attendee,))
</ins><span class="cx">                 raise HTTPError(self.errorResponse(
</span><span class="cx">                     responsecode.FORBIDDEN,
</span><span class="cx">                     self.errorElements[&quot;attendee-denied&quot;],
</span><span class="cx">                     &quot;Outbox does not belong to attendee&quot;,
</span><span class="cx">                 ))
</span><span class="cx">         else:
</span><del>-            log.error(&quot;Unknown ATTENDEE in calendar data: %s&quot; % (self.calendar,))
</del><ins>+            log.error(&quot;Unknown ATTENDEE in calendar data: %s&quot; % (self.attendee,))
</ins><span class="cx">             raise HTTPError(self.errorResponse(
</span><span class="cx">                 responsecode.FORBIDDEN,
</span><span class="cx">                 self.errorElements[&quot;attendee-denied&quot;],
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingcuaddresspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/cuaddress.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/cuaddress.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/cuaddress.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -15,17 +15,27 @@
</span><span class="cx"> ##
</span><span class="cx"> 
</span><span class="cx"> from twext.python.log import Logger
</span><ins>+from twext.who.directory import DirectoryRecord as BaseDirectoryRecord
+from twext.who.idirectory import FieldName as BaseFieldName
+from twext.who.idirectory import RecordType as BaseRecordType
</ins><span class="cx"> 
</span><del>-from txdav.caldav.datastore.scheduling.delivery import DeliveryService
-from txdav.caldav.datastore.scheduling.utils import extractEmailDomain
</del><ins>+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.constants import Names, NamedConstant
</ins><span class="cx"> 
</span><ins>+from txdav.caldav.datastore.scheduling.utils import extractEmailDomain, \
+    uidFromCalendarUserAddress
+from txdav.caldav.icalendardirectoryservice import ICalendarStoreDirectoryRecord
+from txdav.who.directory import CalendarDirectoryRecordMixin
+from txdav.who.idirectory import FieldName
+
+from zope.interface.declarations import implementer
+
</ins><span class="cx"> __all__ = [
</span><span class="cx">     &quot;LocalCalendarUser&quot;,
</span><span class="cx">     &quot;OtherServerCalendarUser&quot;,
</span><span class="cx">     &quot;RemoteCalendarUser&quot;,
</span><span class="cx">     &quot;EmailCalendarUser&quot;,
</span><span class="cx">     &quot;InvalidCalendarUser&quot;,
</span><del>-    &quot;normalizeCUAddr&quot;,
</del><span class="cx"> ]
</span><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="lines">@@ -34,30 +44,91 @@
</span><span class="cx"> 
</span><span class="cx">     def __init__(self, cuaddr):
</span><span class="cx">         self.cuaddr = cuaddr
</span><del>-        self.serviceType = None
</del><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def hosted(self):
+        &quot;&quot;&quot;
+        Is this user hosted on this service (this pod or any other)
+        &quot;&quot;&quot;
+        return False
</ins><span class="cx"> 
</span><del>-class LocalCalendarUser(CalendarUser):
</del><span class="cx"> 
</span><del>-    def __init__(self, cuaddr, principal, inbox=None):
</del><ins>+    def validOriginator(self):
+        &quot;&quot;&quot;
+        Is this user able to originate scheduling messages.
+        &quot;&quot;&quot;
+        return True
+
+
+    def validRecipient(self):
+        &quot;&quot;&quot;
+        Is this user able to receive scheduling messages.
+        &quot;&quot;&quot;
+        return True
+
+
+
+class HostedCalendarUser(CalendarUser):
+    &quot;&quot;&quot;
+    User hosted on any pod of this service. This is derived from an L{DirectoryRecord}
+    in most cases. However, we need to cope with the situation where a user has been
+    removed from the directory but still has calendar data that needs to be managed
+    (typically purged). In that case we there is no directory record, but we can confirm
+    from the cu-address that corresponding data for their UID exists, and thus can
+    determine the valid UID to use.
+    &quot;&quot;&quot;
+
+    def __init__(self, cuaddr, record):
</ins><span class="cx">         self.cuaddr = cuaddr
</span><del>-        self.principal = principal
-        self.inbox = inbox
-        self.serviceType = DeliveryService.serviceType_caldav
</del><ins>+        self.record = record
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def hosted(self):
+        &quot;&quot;&quot;
+        Is this user hosted on this service (this pod or any other)
+        &quot;&quot;&quot;
+        return True
+
+
+    def validOriginator(self):
+        &quot;&quot;&quot;
+        Is this user able to originate scheduling messages.
+        A user with a temporary directory record can be schedule, but that will
+        only be for purposes of automatic purge.
+        &quot;&quot;&quot;
+        return self.record.calendarsEnabled()
+
+
+    def validRecipient(self):
+        &quot;&quot;&quot;
+        Is this user able to receive scheduling messages.
+        A user with a temporary directory record cannot be scheduled with.
+        &quot;&quot;&quot;
+        return self.record.calendarsEnabled() and not isinstance(self.record, TemporaryDirectoryRecord)
+
+
+
+class LocalCalendarUser(HostedCalendarUser):
+    &quot;&quot;&quot;
+    User hosted on the current pod.
+    &quot;&quot;&quot;
+
+    def __init__(self, cuaddr, record):
+        super(LocalCalendarUser, self).__init__(cuaddr, record)
+
+
</ins><span class="cx">     def __str__(self):
</span><span class="cx">         return &quot;Local calendar user: %s&quot; % (self.cuaddr,)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-class OtherServerCalendarUser(CalendarUser):
</del><ins>+class OtherServerCalendarUser(HostedCalendarUser):
+    &quot;&quot;&quot;
+    User hosted on another pod.
+    &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-    def __init__(self, cuaddr, principal):
-        self.cuaddr = cuaddr
-        self.principal = principal
-        self.serviceType = DeliveryService.serviceType_ischedule
</del><ins>+    def __init__(self, cuaddr, record):
+        super(OtherServerCalendarUser, self).__init__(cuaddr, record)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def __str__(self):
</span><span class="lines">@@ -66,11 +137,13 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> class RemoteCalendarUser(CalendarUser):
</span><ins>+    &quot;&quot;&quot;
+    User external to the entire system (set of pods). Used for iSchedule.
+    &quot;&quot;&quot;
</ins><span class="cx"> 
</span><span class="cx">     def __init__(self, cuaddr):
</span><del>-        self.cuaddr = cuaddr
</del><ins>+        super(RemoteCalendarUser, self).__init__(cuaddr)
</ins><span class="cx">         self.extractDomain()
</span><del>-        self.serviceType = DeliveryService.serviceType_ischedule
</del><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def __str__(self):
</span><span class="lines">@@ -89,10 +162,12 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> class EmailCalendarUser(CalendarUser):
</span><ins>+    &quot;&quot;&quot;
+    User external to the entire system (set of pods). Used for iMIP.
+    &quot;&quot;&quot;
</ins><span class="cx"> 
</span><span class="cx">     def __init__(self, cuaddr):
</span><del>-        self.cuaddr = cuaddr
-        self.serviceType = DeliveryService.serviceType_imip
</del><ins>+        super(EmailCalendarUser, self).__init__(cuaddr)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def __str__(self):
</span><span class="lines">@@ -101,37 +176,127 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> class InvalidCalendarUser(CalendarUser):
</span><ins>+    &quot;&quot;&quot;
+    A calendar user that ought to be hosted on the system, but does not have a valid
+    directory entry.
+    &quot;&quot;&quot;
</ins><span class="cx"> 
</span><span class="cx">     def __str__(self):
</span><span class="cx">         return &quot;Invalid calendar user: %s&quot; % (self.cuaddr,)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def validOriginator(self):
+        &quot;&quot;&quot;
+        Is this user able to originate scheduling messages.
+        &quot;&quot;&quot;
+        return False
</ins><span class="cx"> 
</span><del>-def normalizeCUAddr(addr):
</del><ins>+
+    def validRecipient(self):
+        &quot;&quot;&quot;
+        Is this user able to receive scheduling messages.
+        &quot;&quot;&quot;
+        return False
+
+
+
+@inlineCallbacks
+def calendarUserFromCalendarUserAddress(cuaddr, txn):
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    Normalize a cuaddr string by lower()ing it if it's a mailto:, or
-    removing trailing slash if it's a URL.
-    @param addr: a cuaddr string to normalize
-    @return: normalized string
</del><ins>+    Map a calendar user address into an L{CalendarUser} taking into account whether
+    they are hosted in the directory or known to be locally hosted - or match
+    address patterns for other services.
+
+    @param cuaddr: the calendar user address to map
+    @type cuaddr: L{str}
+    @param txn: a transaction to use for store operations
+    @type txn: L{ICommonStoreTransaction}
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    lower = addr.lower()
-    if lower.startswith(&quot;mailto:&quot;):
-        addr = lower
-    if (addr.startswith(&quot;/&quot;) or
-        addr.startswith(&quot;http:&quot;) or
-        addr.startswith(&quot;https:&quot;)):
-        return addr.rstrip(&quot;/&quot;)
-    else:
-        return addr
</del><span class="cx"> 
</span><ins>+    record = yield txn.directoryService().recordWithCalendarUserAddress(cuaddr)
+    returnValue((yield _fromRecord(cuaddr, record, txn)))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-def calendarUserFromPrincipal(recipient, principal, inbox=None):
</del><ins>+
+@inlineCallbacks
+def calendarUserFromCalendarUserUID(uid, txn):
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    Get the appropriate calendar user address class for the provided principal.
</del><ins>+    Map a calendar user address into an L{CalendarUser} taking into account whether
+    they are hosted in the directory or known to be locally hosted - or match
+    address patterns for other services.
+
+    @param uid: the calendar user UID to map
+    @type uid: L{str}
+    @param txn: a transaction to use for store operations
+    @type txn: L{ICommonStoreTransaction}
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-    if principal.thisServer():
-        return LocalCalendarUser(recipient, principal, inbox)
</del><ins>+    record = yield txn.directoryService().recordWithUID(uid)
+    cua = record.canonicalCalendarUserAddress() if record is not None else &quot;urn:x-uid:{}&quot;.format(uid)
+    returnValue((yield _fromRecord(cua, record, txn)))
+
+
+
+class RecordType(Names):
+    &quot;&quot;&quot;
+    Constants for temporary directory record type.
+
+    @cvar unknown: Location record.
+        Represents a calendar user of unknown type.
+    &quot;&quot;&quot;
+
+    unknown = NamedConstant()
+    unknown.description = u&quot;unknown&quot;
+
+
+
+@implementer(ICalendarStoreDirectoryRecord)
+class TemporaryDirectoryRecord(BaseDirectoryRecord, CalendarDirectoryRecordMixin):
+
+    def __init__(self, service, uid, nodeUID):
+
+        fields = {
+            BaseFieldName.uid: uid.decode(&quot;utf-8&quot;),
+            BaseFieldName.recordType: BaseRecordType.user,
+            FieldName.hasCalendars: True,
+            FieldName.serviceNodeUID: nodeUID,
+        }
+
+        super(TemporaryDirectoryRecord, self).__init__(service, fields)
+        self.fields[BaseFieldName.recordType] = RecordType.unknown
+        self.fields[BaseFieldName.guid] = uid.decode(&quot;utf-8&quot;)
+
+
+
+@inlineCallbacks
+def _fromRecord(cuaddr, record, txn):
+    &quot;&quot;&quot;
+    Map a calendar user record into an L{CalendarUser} taking into account whether
+    they are hosted in the directory or known to be locally hosted - or match
+    address patterns for other services.
+
+    @param cuaddr: the calendar user address to map
+    @type cuaddr: L{str}
+    @param record: the calendar user record to map or L{None}
+    @type record: L{IDirectoryRecord}
+    @param txn: a transaction to use for store operations
+    @type txn: L{ICommonStoreTransaction}
+    &quot;&quot;&quot;
+    if record is not None:
+        if not record.calendarsEnabled():
+            returnValue(InvalidCalendarUser(cuaddr))
+        elif record.thisServer():
+            returnValue(LocalCalendarUser(cuaddr, record))
+        else:
+            returnValue(OtherServerCalendarUser(cuaddr, record))
</ins><span class="cx">     else:
</span><del>-        return OtherServerCalendarUser(recipient, principal)
</del><ins>+        uid = uidFromCalendarUserAddress(cuaddr)
+        if uid is not None:
+            hosted, serviceNodeUID = yield txn.store().uidInStore(txn, uid)
+            if hosted:
+                record = TemporaryDirectoryRecord(txn.directoryService(), uid, serviceNodeUID)
+                returnValue(LocalCalendarUser(cuaddr, record))
+
+    from txdav.caldav.datastore.scheduling import addressmapping
+    result = (yield addressmapping.mapper.getCalendarUser(cuaddr))
+    returnValue(result)
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingicaldiffpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -24,7 +24,7 @@
</span><span class="cx"> from twistedcaldav import accounting
</span><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.ical import Component, Property
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
</del><ins>+from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTipGenerator
</span><span class="cx"> 
</span><span class="cx"> &quot;&quot;&quot;
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingimipoutboundpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/outbound.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/outbound.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/outbound.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -39,7 +39,7 @@
</span><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.ical import Component
</span><span class="cx"> from twistedcaldav.localization import translationTo, _, getLanguage
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
</del><ins>+from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.imip.smtpsender import SMTPSender
</span><span class="cx"> from txdav.common.datastore.sql_tables import schema
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingimplicitpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -25,11 +25,12 @@
</span><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.ical import Property
</span><span class="cx"> 
</span><del>-from txdav.caldav.datastore.scheduling import addressmapping
</del><span class="cx"> from txdav.caldav.datastore.scheduling.caldav.scheduler import CalDAVScheduler
</span><span class="cx"> from txdav.caldav.datastore.scheduling.cuaddress import InvalidCalendarUser, \
</span><span class="cx">     LocalCalendarUser, OtherServerCalendarUser, \
</span><del>-    normalizeCUAddr
</del><ins>+    calendarUserFromCalendarUserAddress, \
+    calendarUserFromCalendarUserUID
+from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.icaldiff import iCalDiff
</span><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTipGenerator, iTIPRequestStatus
</span><span class="cx"> from txdav.caldav.datastore.scheduling.utils import getCalendarObjectForRecord
</span><span class="lines">@@ -232,6 +233,7 @@
</span><span class="cx">         @type calendar: L{twistedcaldav.ical.Component}
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><ins>+        self.txn = parent._txn
</ins><span class="cx">         self.parent = parent
</span><span class="cx">         self.resource = resource
</span><span class="cx">         self.calendar = calendar
</span><span class="lines">@@ -257,7 +259,7 @@
</span><span class="cx">         # Check to see whether the organizer principal is enabled for scheduling. If not, do not allow them
</span><span class="cx">         # to create new scheduling resources.
</span><span class="cx">         if self.action == &quot;create&quot;:
</span><del>-            if self.organizerPrincipal and not self.organizerPrincipal.enabledAsOrganizer():
</del><ins>+            if self.organizerAddress.hosted() and not self.organizerAddress.record.enabledAsOrganizer():
</ins><span class="cx">                 log.error(&quot;ORGANIZER not allowed to be an Organizer: {organizer}&quot;, organizer=self.organizer)
</span><span class="cx">                 raise HTTPError(ErrorResponse(
</span><span class="cx">                     responsecode.FORBIDDEN,
</span><span class="lines">@@ -365,11 +367,9 @@
</span><span class="cx"> 
</span><span class="cx">         # Get some useful information from the calendar
</span><span class="cx">         yield self.extractCalendarData()
</span><del>-        self.organizerPrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(self.organizer)
-        self.organizerAddress = (yield addressmapping.mapper.getCalendarUser(self.organizer, self.organizerPrincipal))
</del><ins>+        self.organizerAddress = (yield calendarUserFromCalendarUserAddress(self.organizer, self.txn))
</ins><span class="cx"> 
</span><span class="cx">         # Originator is the organizer in this case
</span><del>-        self.originatorPrincipal = self.organizerPrincipal
</del><span class="cx">         self.originator = self.organizer
</span><span class="cx"> 
</span><span class="cx">         # We want to suppress chatty iMIP messages when other attendees reply
</span><span class="lines">@@ -447,11 +447,9 @@
</span><span class="cx">             self.calendar = calendar_old
</span><span class="cx"> 
</span><span class="cx">         yield self.extractCalendarData()
</span><del>-        self.organizerPrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(self.organizer)
-        self.organizerAddress = (yield addressmapping.mapper.getCalendarUser(self.organizer, self.organizerPrincipal))
</del><ins>+        self.organizerAddress = (yield calendarUserFromCalendarUserAddress(self.organizer, self.txn))
</ins><span class="cx"> 
</span><span class="cx">         # Originator is the organizer in this case
</span><del>-        self.originatorPrincipal = self.organizerPrincipal
</del><span class="cx">         self.originator = self.organizer
</span><span class="cx"> 
</span><span class="cx">         self.except_attendees = ()
</span><span class="lines">@@ -479,8 +477,8 @@
</span><span class="cx">         # Get some useful information from the calendar
</span><span class="cx">         yield self.extractCalendarData()
</span><span class="cx"> 
</span><del>-        self.attendeePrincipal = yield self.calendar_home.directoryService().recordWithUID(self.calendar_home.uid().decode(&quot;utf-8&quot;))
-        self.originator = self.attendee = self.attendeePrincipal.canonicalCalendarUserAddress()
</del><ins>+        self.attendeeAddress = (yield calendarUserFromCalendarUserUID(self.calendar_home.uid(), self.txn))
+        self.originator = self.attendee = self.attendeeAddress.record.canonicalCalendarUserAddress()
</ins><span class="cx"> 
</span><span class="cx">         result = (yield self.scheduleWithOrganizer())
</span><span class="cx"> 
</span><span class="lines">@@ -491,10 +489,10 @@
</span><span class="cx">     def extractCalendarData(self):
</span><span class="cx"> 
</span><span class="cx">         # Get the originator who is the owner of the calendar resource being modified
</span><del>-        self.originatorPrincipal = yield self.calendar_home.directoryService().recordWithUID(self.calendar_home.uid().decode(&quot;utf-8&quot;))
</del><ins>+        originatorAddress = yield calendarUserFromCalendarUserUID(self.calendar_home.uid(), self.txn)
</ins><span class="cx"> 
</span><span class="cx">         # Pick the canonical CUA:
</span><del>-        self.originator = self.originatorPrincipal.canonicalCalendarUserAddress()
</del><ins>+        self.originator = originatorAddress.record.canonicalCalendarUserAddress()
</ins><span class="cx"> 
</span><span class="cx">         # Get the ORGANIZER and verify it is the same for all components
</span><span class="cx">         try:
</span><span class="lines">@@ -567,13 +565,12 @@
</span><span class="cx">             returnValue(False)
</span><span class="cx"> 
</span><span class="cx">         # Organizer must map to a valid principal
</span><del>-        self.organizerPrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(self.organizer)
-        self.organizerAddress = (yield addressmapping.mapper.getCalendarUser(self.organizer, self.organizerPrincipal))
-        if not self.organizerPrincipal:
</del><ins>+        self.organizerAddress = (yield calendarUserFromCalendarUserAddress(self.organizer, self.txn))
+        if not self.organizerAddress.hosted():
</ins><span class="cx">             returnValue(False)
</span><span class="cx"> 
</span><span class="cx">         # Organizer must be the owner of the calendar resource
</span><del>-        if self.calendar_home.uid() != self.organizerPrincipal.uid:
</del><ins>+        if self.calendar_home.uid() != self.organizerAddress.record.uid:
</ins><span class="cx">             returnValue(False)
</span><span class="cx"> 
</span><span class="cx">         returnValue(True)
</span><span class="lines">@@ -588,10 +585,10 @@
</span><span class="cx"> 
</span><span class="cx">         # Check to see whether any attendee is the owner
</span><span class="cx">         for attendee in self.attendees:
</span><del>-            attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
-            if attendeePrincipal and attendeePrincipal.uid == self.calendar_home.uid():
</del><ins>+            attendeeAddress = yield calendarUserFromCalendarUserAddress(attendee, self.txn)
+            if attendeeAddress.hosted() and attendeeAddress.record.uid == self.calendar_home.uid():
</ins><span class="cx">                 self.attendee = attendee
</span><del>-                self.attendeePrincipal = attendeePrincipal
</del><ins>+                self.attendeeAddress = attendeeAddress
</ins><span class="cx">                 returnValue(True)
</span><span class="cx"> 
</span><span class="cx">         returnValue(False)
</span><span class="lines">@@ -670,7 +667,7 @@
</span><span class="cx">                             if attendee.hasParameter(&quot;PARTSTAT&quot;):
</span><span class="cx">                                 cuaddr = attendee.value()
</span><span class="cx"> 
</span><del>-                                if cuaddr in self.organizerPrincipal.calendarUserAddresses:
</del><ins>+                                if cuaddr in self.organizerAddress.record.calendarUserAddresses:
</ins><span class="cx">                                     # If the attendee is the organizer then do not update
</span><span class="cx">                                     # the PARTSTAT to NEEDS-ACTION.
</span><span class="cx">                                     # The organizer is automatically ACCEPTED to the event.
</span><span class="lines">@@ -971,7 +968,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         for attendee in self.calendar.getAllAttendeeProperties():
</span><span class="cx">             # Don't adjust ORGANIZER's ATTENDEE
</span><del>-            if attendee.value() in self.organizerPrincipal.calendarUserAddresses:
</del><ins>+            if attendee.value() in self.organizerAddress.record.calendarUserAddresses:
</ins><span class="cx">                 continue
</span><span class="cx">             if attendee.parameterValue(&quot;SCHEDULE-AGENT&quot;, &quot;SERVER&quot;).upper() == &quot;SERVER&quot; and attendee.hasParameter(&quot;PARTSTAT&quot;):
</span><span class="cx">                 attendee.setParameter(&quot;PARTSTAT&quot;, &quot;NEEDS-ACTION&quot;)
</span><span class="lines">@@ -1026,7 +1023,7 @@
</span><span class="cx">         changed = False
</span><span class="cx">         for cuaddr, newattendee in new_attendees.items():
</span><span class="cx">             # Don't adjust ORGANIZER's ATTENDEE
</span><del>-            if newattendee.value() in self.organizerPrincipal.calendarUserAddresses:
</del><ins>+            if newattendee.value() in self.organizerAddress.record.calendarUserAddresses:
</ins><span class="cx">                 continue
</span><span class="cx">             new_partstat = newattendee.parameterValue(&quot;PARTSTAT&quot;, &quot;NEEDS-ACTION&quot;).upper()
</span><span class="cx">             if newattendee.parameterValue(&quot;SCHEDULE-AGENT&quot;, &quot;SERVER&quot;).upper() == &quot;SERVER&quot; and new_partstat != &quot;NEEDS-ACTION&quot;:
</span><span class="lines">@@ -1060,8 +1057,7 @@
</span><span class="cx">             if attendee.parameterValue(&quot;SCHEDULE-AGENT&quot;, &quot;SERVER&quot;).upper() == &quot;CLIENT&quot;:
</span><span class="cx">                 cuaddr = attendee.value()
</span><span class="cx">                 if cuaddr not in coerced:
</span><del>-                    attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(cuaddr)
-                    attendeeAddress = (yield addressmapping.mapper.getCalendarUser(cuaddr, attendeePrincipal))
</del><ins>+                    attendeeAddress = (yield calendarUserFromCalendarUserAddress(cuaddr, self.txn))
</ins><span class="cx">                     local_attendee = type(attendeeAddress) in (LocalCalendarUser, OtherServerCalendarUser,)
</span><span class="cx">                     coerced[cuaddr] = local_attendee
</span><span class="cx">                 if coerced[cuaddr]:
</span><span class="lines">@@ -1082,7 +1078,7 @@
</span><span class="cx">             self.resource,
</span><span class="cx">             self.oldcalendar,
</span><span class="cx">             self.calendar,
</span><del>-            self.organizerPrincipal.canonicalCalendarUserAddress(),
</del><ins>+            self.organizerAddress.record.canonicalCalendarUserAddress(),
</ins><span class="cx">             len(self.calendar.getAllUniqueAttendees()) - 1,
</span><span class="cx">             self.do_smart_merge,
</span><span class="cx">         )
</span><span class="lines">@@ -1128,13 +1124,12 @@
</span><span class="cx">         for attendee, rids in aggregated.iteritems():
</span><span class="cx"> 
</span><span class="cx">             # Don't send message back to the ORGANIZER
</span><del>-            if attendee in self.organizerPrincipal.calendarUserAddresses:
</del><ins>+            if attendee in self.organizerAddress.record.calendarUserAddresses:
</ins><span class="cx">                 continue
</span><span class="cx"> 
</span><span class="cx">             # Handle split by not scheduling local attendees
</span><span class="cx">             if self.split_details is not None:
</span><del>-                attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
-                attendeeAddress = (yield addressmapping.mapper.getCalendarUser(attendee, attendeePrincipal))
</del><ins>+                attendeeAddress = (yield calendarUserFromCalendarUserAddress(attendee, self.txn))
</ins><span class="cx">                 if type(attendeeAddress) is LocalCalendarUser:
</span><span class="cx">                     continue
</span><span class="cx"> 
</span><span class="lines">@@ -1173,7 +1168,7 @@
</span><span class="cx">         for attendee in self.attendees:
</span><span class="cx"> 
</span><span class="cx">             # Don't send message back to the ORGANIZER
</span><del>-            if attendee in self.organizerPrincipal.calendarUserAddresses:
</del><ins>+            if attendee in self.organizerAddress.record.calendarUserAddresses:
</ins><span class="cx">                 continue
</span><span class="cx"> 
</span><span class="cx">             # Don't send message to specified attendees
</span><span class="lines">@@ -1190,8 +1185,7 @@
</span><span class="cx"> 
</span><span class="cx">             # Handle split by not scheduling local attendees
</span><span class="cx">             if self.split_details is not None:
</span><del>-                attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
-                attendeeAddress = (yield addressmapping.mapper.getCalendarUser(attendee, attendeePrincipal))
</del><ins>+                attendeeAddress = (yield calendarUserFromCalendarUserAddress(attendee, self.txn))
</ins><span class="cx">                 if type(attendeeAddress) is LocalCalendarUser:
</span><span class="cx">                     continue
</span><span class="cx"> 
</span><span class="lines">@@ -1245,13 +1239,12 @@
</span><span class="cx">         for attendee, rids in aggregated.iteritems():
</span><span class="cx"> 
</span><span class="cx">             # Don't send message back to the ORGANIZER
</span><del>-            if attendee in self.organizerPrincipal.calendarUserAddresses:
</del><ins>+            if attendee in self.organizerAddress.record.calendarUserAddresses:
</ins><span class="cx">                 continue
</span><span class="cx"> 
</span><span class="cx">             # Handle split by not scheduling local attendees
</span><span class="cx">             if self.split_details is not None:
</span><del>-                attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
-                attendeeAddress = (yield addressmapping.mapper.getCalendarUser(attendee, attendeePrincipal))
</del><ins>+                attendeeAddress = (yield calendarUserFromCalendarUserAddress(attendee, self.txn))
</ins><span class="cx">                 if type(attendeeAddress) is LocalCalendarUser:
</span><span class="cx">                     continue
</span><span class="cx"> 
</span><span class="lines">@@ -1298,7 +1291,7 @@
</span><span class="cx">         for attendee in self.attendees:
</span><span class="cx"> 
</span><span class="cx">             # Don't send message back to the ORGANIZER
</span><del>-            if attendee in self.organizerPrincipal.calendarUserAddresses:
</del><ins>+            if attendee in self.organizerAddress.record.calendarUserAddresses:
</ins><span class="cx">                 continue
</span><span class="cx"> 
</span><span class="cx">             # Don't send message to specified attendees
</span><span class="lines">@@ -1315,8 +1308,7 @@
</span><span class="cx"> 
</span><span class="cx">             # Handle split by not scheduling local attendees
</span><span class="cx">             if self.split_details is not None:
</span><del>-                attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
-                attendeeAddress = (yield addressmapping.mapper.getCalendarUser(attendee, attendeePrincipal))
</del><ins>+                attendeeAddress = (yield calendarUserFromCalendarUserAddress(attendee, self.txn))
</ins><span class="cx">                 if type(attendeeAddress) is LocalCalendarUser:
</span><span class="cx">                     continue
</span><span class="cx"> 
</span><span class="lines">@@ -1412,7 +1404,7 @@
</span><span class="cx"> 
</span><span class="cx">                 # If Organizer copy exists we cannot allow SCHEDULE-AGENT=CLIENT or NONE
</span><span class="cx">                 if not doScheduling:
</span><del>-                    log.error(&quot;Attendee '{attendee}' is not allowed to change SCHEDULE-AGENT on organizer: UID:{uid}&quot;, attendee=self.attendeePrincipal, uid=self.uid)
</del><ins>+                    log.error(&quot;Attendee '{attendee}' is not allowed to change SCHEDULE-AGENT on organizer: UID:{uid}&quot;, attendee=self.attendeeAddress.record, uid=self.uid)
</ins><span class="cx">                     raise HTTPError(ErrorResponse(
</span><span class="cx">                         responsecode.FORBIDDEN,
</span><span class="cx">                         (caldav_namespace, &quot;valid-attendee-change&quot;),
</span><span class="lines">@@ -1430,7 +1422,7 @@
</span><span class="cx">                         self.return_status = ImplicitScheduler.STATUS_ORPHANED_EVENT
</span><span class="cx">                         returnValue(None)
</span><span class="cx">                     else:
</span><del>-                        log.error(&quot;Attendee '{attendee}' is not allowed to make an unauthorized change to an organized event: UID:{uid}&quot;, attendee=self.attendeePrincipal, uid=self.uid)
</del><ins>+                        log.error(&quot;Attendee '{attendee}' is not allowed to make an unauthorized change to an organized event: UID:{uid}&quot;, attendee=self.attendeeAddress.record, uid=self.uid)
</ins><span class="cx">                         raise HTTPError(ErrorResponse(
</span><span class="cx">                             responsecode.FORBIDDEN,
</span><span class="cx">                             (caldav_namespace, &quot;valid-attendee-change&quot;),
</span><span class="lines">@@ -1462,7 +1454,7 @@
</span><span class="cx">                         if self.oldcalendar:
</span><span class="cx">                             oldScheduling = self.oldcalendar.getOrganizerScheduleAgent()
</span><span class="cx">                             if not oldScheduling:
</span><del>-                                log.error(&quot;Attendee '{attendee}' is not allowed to set SCHEDULE-AGENT=SERVER on organizer: UID:{uid}&quot;, attendee=self.attendeePrincipal, uid=self.uid)
</del><ins>+                                log.error(&quot;Attendee '{attendee}' is not allowed to set SCHEDULE-AGENT=SERVER on organizer: UID:{uid}&quot;, attendee=self.attendeeAddress.record, uid=self.uid)
</ins><span class="cx">                                 raise HTTPError(ErrorResponse(
</span><span class="cx">                                     responsecode.FORBIDDEN,
</span><span class="cx">                                     (caldav_namespace, &quot;valid-attendee-change&quot;),
</span><span class="lines">@@ -1533,8 +1525,8 @@
</span><span class="cx">                     oldattendess = self.oldcalendar.getAllUniqueAttendees()
</span><span class="cx">                     found_old = False
</span><span class="cx">                     for attendee in oldattendess:
</span><del>-                        attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
-                        if attendeePrincipal and attendeePrincipal.uid == self.calendar_home.uid():
</del><ins>+                        attendeeAddress = (yield calendarUserFromCalendarUserAddress(attendee, self.txn))
+                        if attendeeAddress and attendeeAddress.record.uid == self.calendar_home.uid():
</ins><span class="cx">                             found_old = True
</span><span class="cx">                             break
</span><span class="cx"> 
</span><span class="lines">@@ -1566,7 +1558,7 @@
</span><span class="cx"> 
</span><span class="cx">         if not config.Scheduling.iSchedule.Enabled and not local_organizer and is_server:
</span><span class="cx">             # Coerce ORGANIZER to SCHEDULE-AGENT=NONE
</span><del>-            log.debug(&quot;Attendee '{attendee}' is not allowed to use SCHEDULE-AGENT=SERVER on organizer: UID:{uid}&quot;, attendee=self.attendeePrincipal, uid=self.uid)
</del><ins>+            log.debug(&quot;Attendee '{attendee}' is not allowed to use SCHEDULE-AGENT=SERVER on organizer: UID:{uid}&quot;, attendee=self.attendeeAddress.record, uid=self.uid)
</ins><span class="cx">             self.calendar.setParameterToValueForPropertyWithValue(&quot;SCHEDULE-AGENT&quot;, &quot;NONE&quot;, &quot;ORGANIZER&quot;, None)
</span><span class="cx">             self.calendar.setParameterToValueForPropertyWithValue(&quot;SCHEDULE-STATUS&quot;, iTIPRequestStatus.NO_USER_SUPPORT_CODE, &quot;ORGANIZER&quot;, None)
</span><span class="cx">             is_server = False
</span><span class="lines">@@ -1585,7 +1577,10 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         self.organizer_calendar = None
</span><del>-        calendar_resource = (yield getCalendarObjectForRecord(self.calendar_home.transaction(), self.organizerPrincipal, self.uid))
</del><ins>+        if self.organizerAddress.hosted():
+            calendar_resource = (yield getCalendarObjectForRecord(self.calendar_home.transaction(), self.organizerAddress.record, self.uid))
+        else:
+            calendar_resource = None
</ins><span class="cx">         if calendar_resource is not None:
</span><span class="cx">             self.organizer_calendar = (yield calendar_resource.componentForUser())
</span><span class="cx">         elif type(self.organizerAddress) in (OtherServerCalendarUser,):
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingischeduledeliverypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/delivery.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/delivery.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/delivery.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -188,12 +188,12 @@
</span><span class="cx">         if not hasattr(self, &quot;otherServers&quot;):
</span><span class="cx">             self.otherServers = {}
</span><span class="cx"> 
</span><del>-        serverURI = recipient.principal.serverURI()
</del><ins>+        serverURI = recipient.record.serverURI()
</ins><span class="cx">         if serverURI not in self.otherServers:
</span><span class="cx">             self.otherServers[serverURI] = IScheduleServerRecord(
</span><span class="cx">                 uri=joinURL(serverURI, config.Servers.InboxName),
</span><del>-                unNormalizeAddresses=not recipient.principal.server().isImplicit,
-                moreHeaders=[recipient.principal.server().secretHeader(), ],
</del><ins>+                unNormalizeAddresses=not recipient.record.server().isImplicit,
+                moreHeaders=[recipient.record.server().secretHeader(), ],
</ins><span class="cx">                 podding=True,
</span><span class="cx">             )
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingischeduleschedulerpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/scheduler.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/scheduler.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/scheduler.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -26,8 +26,8 @@
</span><span class="cx"> from twistedcaldav.ical import normalizeCUAddress
</span><span class="cx"> 
</span><span class="cx"> from txdav.caldav.datastore.scheduling import addressmapping
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser
-from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromPrincipal
</del><ins>+from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser, \
+    calendarUserFromCalendarUserAddress, RemoteCalendarUser
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule import xml
</span><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.dkim import DKIMVerifier, \
</span><span class="cx">     DKIMVerificationError, DKIMMissingError
</span><span class="lines">@@ -226,19 +226,11 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # For remote requests we do not allow the originator to be a local user or one within our domain.
</span><del>-        originatorPrincipal = (yield self.txn.directoryService().recordWithCalendarUserAddress(self.originator))
</del><ins>+        originatorAddress = yield calendarUserFromCalendarUserAddress(self.originator, self.txn)
</ins><span class="cx">         localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
</span><span class="cx"> 
</span><del>-        if (originatorPrincipal or localUser) and not self._podding:
-            log.error(&quot;Cannot use originator that is external to this server: %s&quot; % (self.originator,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements[&quot;originator-denied&quot;],
-                &quot;Originator cannot be external to server&quot;,
-            ))
</del><ins>+        if originatorAddress.hosted() or localUser:
</ins><span class="cx"> 
</span><del>-        if originatorPrincipal or localUser:
-
</del><span class="cx">             # iSchedule must never deliver for users hosted on the server or any pod
</span><span class="cx">             if not self._podding:
</span><span class="cx">                 log.error(&quot;Cannot use originator that is local to this server: %s&quot; % (self.originator,))
</span><span class="lines">@@ -249,7 +241,7 @@
</span><span class="cx">                 ))
</span><span class="cx"> 
</span><span class="cx">             # Cannot deliver message for someone hosted on the same pod
</span><del>-            elif originatorPrincipal.thisServer():
</del><ins>+            elif isinstance(originatorAddress, LocalCalendarUser):
</ins><span class="cx">                 log.error(&quot;Cannot use originator that is on this server: %s&quot; % (self.originator,))
</span><span class="cx">                 raise HTTPError(self.errorResponse(
</span><span class="cx">                     responsecode.FORBIDDEN,
</span><span class="lines">@@ -257,8 +249,8 @@
</span><span class="cx">                     &quot;Originator cannot be local to server&quot;,
</span><span class="cx">                 ))
</span><span class="cx">             else:
</span><del>-                self.originator = calendarUserFromPrincipal(self.originator, originatorPrincipal)
-                self._validAlternateServer(originatorPrincipal)
</del><ins>+                self.originator = originatorAddress
+                self._validAlternateServer(originatorAddress)
</ins><span class="cx">         else:
</span><span class="cx">             if self._podding:
</span><span class="cx">                 log.error(&quot;Cannot use originator that is external to this server: %s&quot; % (self.originator,))
</span><span class="lines">@@ -333,13 +325,13 @@
</span><span class="cx">                 ))
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def _validAlternateServer(self, principal):
</del><ins>+    def _validAlternateServer(self, cuuser):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Check the validity of the podded host.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Extract expected host/port. This will be the serverURI.
</span><del>-        expected_uri = principal.serverURI()
</del><ins>+        expected_uri = cuuser.record.serverURI()
</ins><span class="cx">         expected_uri = urlparse.urlparse(expected_uri)
</span><span class="cx"> 
</span><span class="cx">         # Get the request IP and map to hostname.
</span><span class="lines">@@ -395,9 +387,9 @@
</span><span class="cx">         # Verify that the ORGANIZER's cu address does not map to a valid user
</span><span class="cx">         organizer = self.calendar.getOrganizer()
</span><span class="cx">         if organizer:
</span><del>-            organizerPrincipal = yield self.txn.directoryService().recordWithCalendarUserAddress(organizer)
-            if organizerPrincipal:
-                if organizerPrincipal.thisServer():
</del><ins>+            organizerAddress = yield calendarUserFromCalendarUserAddress(organizer, self.txn)
+            if organizerAddress.hosted():
+                if isinstance(organizerAddress, LocalCalendarUser):
</ins><span class="cx">                     log.error(&quot;Invalid ORGANIZER in calendar data: %s&quot; % (self.calendar,))
</span><span class="cx">                     raise HTTPError(self.errorResponse(
</span><span class="cx">                         responsecode.FORBIDDEN,
</span><span class="lines">@@ -406,8 +398,8 @@
</span><span class="cx">                     ))
</span><span class="cx">                 else:
</span><span class="cx">                     # Check that the origin server is the correct pod
</span><del>-                    self.organizer = calendarUserFromPrincipal(organizer, organizerPrincipal)
-                    self._validAlternateServer(self.organizer.principal)
</del><ins>+                    self.organizer = organizerAddress
+                    self._validAlternateServer(self.organizer)
</ins><span class="cx">             else:
</span><span class="cx">                 localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
</span><span class="cx">                 if localUser:
</span><span class="lines">@@ -436,9 +428,9 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Attendee cannot be local.
</span><del>-        attendeePrincipal = yield self.txn.directoryService().recordWithCalendarUserAddress(self.attendee)
-        if attendeePrincipal:
-            if attendeePrincipal.thisServer():
</del><ins>+        attendeeAddress = yield calendarUserFromCalendarUserAddress(self.attendee, self.txn)
+        if attendeeAddress.hosted():
+            if isinstance(attendeeAddress, LocalCalendarUser):
</ins><span class="cx">                 log.error(&quot;Invalid ATTENDEE in calendar data: %s&quot; % (self.calendar,))
</span><span class="cx">                 raise HTTPError(self.errorResponse(
</span><span class="cx">                     responsecode.FORBIDDEN,
</span><span class="lines">@@ -446,7 +438,7 @@
</span><span class="cx">                     &quot;Local attendee cannot send to this server&quot;,
</span><span class="cx">                 ))
</span><span class="cx">             else:
</span><del>-                self._validAlternateServer(attendeePrincipal)
</del><ins>+                self._validAlternateServer(attendeeAddress)
</ins><span class="cx">         else:
</span><span class="cx">             localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
</span><span class="cx">             if localUser:
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingprocessingpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -29,10 +29,10 @@
</span><span class="cx"> from twistedcaldav.ical import Property
</span><span class="cx"> from twistedcaldav.instance import InvalidOverriddenInstanceError
</span><span class="cx"> 
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
</del><span class="cx"> from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
</span><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTipProcessing, iTIPRequestStatus
</span><span class="cx"> from txdav.caldav.datastore.scheduling.utils import getCalendarObjectForRecord
</span><ins>+from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.work import ScheduleRefreshWork, \
</span><span class="cx">     ScheduleAutoReplyWork
</span><span class="cx"> from txdav.caldav.icalendarstore import ComponentUpdateState, \
</span><span class="lines">@@ -161,9 +161,9 @@
</span><span class="cx"> 
</span><span class="cx">         self.recipient_calendar = None
</span><span class="cx">         self.recipient_calendar_resource = None
</span><del>-        calendar_resource = (yield getCalendarObjectForRecord(self.txn, self.recipient.principal, self.uid))
</del><ins>+        calendar_resource = (yield getCalendarObjectForRecord(self.txn, self.recipient.record, self.uid))
</ins><span class="cx">         if calendar_resource:
</span><del>-            self.recipient_calendar = (yield calendar_resource.componentForUser(self.recipient.principal.uid))
</del><ins>+            self.recipient_calendar = (yield calendar_resource.componentForUser(self.recipient.record.uid))
</ins><span class="cx">             self.recipient_calendar_resource = calendar_resource
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -436,21 +436,21 @@
</span><span class="cx"> 
</span><span class="cx">             # Handle auto-reply behavior
</span><span class="cx">             organizer = normalizeCUAddr(self.message.getOrganizer())
</span><del>-            if (yield self.recipient.principal.canAutoSchedule(organizer=organizer)):
</del><ins>+            if (yield self.recipient.record.canAutoSchedule(organizer=organizer)):
</ins><span class="cx">                 # auto schedule mode can depend on who the organizer is
</span><del>-                mode = yield self.recipient.principal.getAutoScheduleMode(organizer=organizer)
</del><ins>+                mode = yield self.recipient.record.getAutoScheduleMode(organizer=organizer)
</ins><span class="cx">                 send_reply, store_inbox, partstat, accounting = (yield self.checkAttendeeAutoReply(new_calendar, mode))
</span><span class="cx">                 if accounting is not None:
</span><span class="cx">                     accounting[&quot;action&quot;] = &quot;create&quot;
</span><span class="cx">                     emitAccounting(
</span><span class="cx">                         &quot;AutoScheduling&quot;,
</span><del>-                        self.recipient.principal,
</del><ins>+                        self.recipient.record,
</ins><span class="cx">                         json.dumps(accounting) + &quot;\r\n&quot;,
</span><span class="cx">                         filename=self.uid.encode(&quot;base64&quot;)[:-1] + &quot;.txt&quot;
</span><span class="cx">                     )
</span><span class="cx"> 
</span><span class="cx">                 # Only store inbox item when reply is not sent or always for users
</span><del>-                store_inbox = store_inbox or self.recipient.principal.getCUType() == &quot;INDIVIDUAL&quot;
</del><ins>+                store_inbox = store_inbox or self.recipient.record.getCUType() == &quot;INDIVIDUAL&quot;
</ins><span class="cx">             else:
</span><span class="cx">                 send_reply = False
</span><span class="cx">                 store_inbox = True
</span><span class="lines">@@ -477,21 +477,21 @@
</span><span class="cx"> 
</span><span class="cx">                 # Handle auto-reply behavior
</span><span class="cx">                 organizer = normalizeCUAddr(self.message.getOrganizer())
</span><del>-                if (yield self.recipient.principal.canAutoSchedule(organizer=organizer)) and not hasattr(self.txn, &quot;doing_attendee_refresh&quot;):
</del><ins>+                if (yield self.recipient.record.canAutoSchedule(organizer=organizer)) and not hasattr(self.txn, &quot;doing_attendee_refresh&quot;):
</ins><span class="cx">                     # auto schedule mode can depend on who the organizer is
</span><del>-                    mode = yield self.recipient.principal.getAutoScheduleMode(organizer=organizer)
</del><ins>+                    mode = yield self.recipient.record.getAutoScheduleMode(organizer=organizer)
</ins><span class="cx">                     send_reply, store_inbox, partstat, accounting = (yield self.checkAttendeeAutoReply(new_calendar, mode))
</span><span class="cx">                     if accounting is not None:
</span><span class="cx">                         accounting[&quot;action&quot;] = &quot;modify&quot;
</span><span class="cx">                         emitAccounting(
</span><span class="cx">                             &quot;AutoScheduling&quot;,
</span><del>-                            self.recipient.principal,
</del><ins>+                            self.recipient.record,
</ins><span class="cx">                             json.dumps(accounting) + &quot;\r\n&quot;,
</span><span class="cx">                             filename=self.uid.encode(&quot;base64&quot;)[:-1] + &quot;.txt&quot;
</span><span class="cx">                         )
</span><span class="cx"> 
</span><span class="cx">                     # Only store inbox item when reply is not sent or always for users
</span><del>-                    store_inbox = store_inbox or self.recipient.principal.getCUType() == &quot;INDIVIDUAL&quot;
</del><ins>+                    store_inbox = store_inbox or self.recipient.record.getCUType() == &quot;INDIVIDUAL&quot;
</ins><span class="cx">                 else:
</span><span class="cx">                     send_reply = False
</span><span class="cx">                     store_inbox = True
</span><span class="lines">@@ -566,13 +566,13 @@
</span><span class="cx">             # inbox item on them even if auto-schedule is true so that they get a notification
</span><span class="cx">             # of the cancel.
</span><span class="cx">             organizer = normalizeCUAddr(self.message.getOrganizer())
</span><del>-            autoprocessed = yield self.recipient.principal.canAutoSchedule(organizer=organizer)
-            store_inbox = not autoprocessed or self.recipient.principal.getCUType() == &quot;INDIVIDUAL&quot;
</del><ins>+            autoprocessed = yield self.recipient.record.canAutoSchedule(organizer=organizer)
+            store_inbox = not autoprocessed or self.recipient.record.getCUType() == &quot;INDIVIDUAL&quot;
</ins><span class="cx"> 
</span><span class="cx">             # Check to see if this is a cancel of the entire event
</span><span class="cx">             processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar, autoprocessing=autoprocessed)
</span><span class="cx">             if processed_message:
</span><del>-                if autoprocessed and accountingEnabled(&quot;AutoScheduling&quot;, self.recipient.principal):
</del><ins>+                if autoprocessed and accountingEnabled(&quot;AutoScheduling&quot;, self.recipient.record):
</ins><span class="cx">                     accounting = {
</span><span class="cx">                         &quot;action&quot;: &quot;cancel&quot;,
</span><span class="cx">                         &quot;when&quot;: DateTime.getNowUTC().getText(),
</span><span class="lines">@@ -580,7 +580,7 @@
</span><span class="cx">                     }
</span><span class="cx">                     emitAccounting(
</span><span class="cx">                         &quot;AutoScheduling&quot;,
</span><del>-                        self.recipient.principal,
</del><ins>+                        self.recipient.record,
</ins><span class="cx">                         json.dumps(accounting) + &quot;\r\n&quot;,
</span><span class="cx">                         filename=self.uid.encode(&quot;base64&quot;)[:-1] + &quot;.txt&quot;
</span><span class="cx">                     )
</span><span class="lines">@@ -643,7 +643,7 @@
</span><span class="cx">         @return: C{tuple} of C{bool}, C{bool}, C{str} indicating whether changes were made, whether the inbox item
</span><span class="cx">             should be added, and the new PARTSTAT.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        if accountingEnabled(&quot;AutoScheduling&quot;, self.recipient.principal):
</del><ins>+        if accountingEnabled(&quot;AutoScheduling&quot;, self.recipient.record):
</ins><span class="cx">             accounting = {
</span><span class="cx">                 &quot;when&quot;: DateTime.getNowUTC().getText(),
</span><span class="cx">                 &quot;automode&quot;: automode,
</span><span class="lines">@@ -670,7 +670,7 @@
</span><span class="cx"> 
</span><span class="cx">         log.debug(&quot;ImplicitProcessing - recipient '%s' processing UID: '%s' - checking for auto-reply with mode: %s&quot; % (self.recipient.cuaddr, self.uid, automode.name,))
</span><span class="cx"> 
</span><del>-        cuas = self.recipient.principal.calendarUserAddresses
</del><ins>+        cuas = self.recipient.record.calendarUserAddresses
</ins><span class="cx"> 
</span><span class="cx">         # First expand current one to get instances (only go 1 year into the future)
</span><span class="cx">         default_future_expansion_duration = Duration(days=config.Scheduling.Options.AutoSchedule.FutureFreeBusyDays)
</span><span class="lines">@@ -979,20 +979,20 @@
</span><span class="cx">             raise ImplicitProcessorException(&quot;5.1;Service unavailable&quot;)
</span><span class="cx"> 
</span><span class="cx">         # Check to see whether the originator is hosted on this server
</span><del>-        if not self.originator.principal:
</del><ins>+        if not self.originator.record:
</ins><span class="cx">             raise ImplicitProcessorException(&quot;5.1;Service unavailable&quot;)
</span><span class="cx"> 
</span><span class="cx">         # Locate the originator's copy of the event
</span><del>-        calendar_resource = (yield getCalendarObjectForRecord(self.txn, self.originator.principal, self.uid))
</del><ins>+        calendar_resource = (yield getCalendarObjectForRecord(self.txn, self.originator.record, self.uid))
</ins><span class="cx">         if calendar_resource is None:
</span><span class="cx">             raise ImplicitProcessorException(&quot;5.1;Service unavailable&quot;)
</span><del>-        originator_calendar = (yield calendar_resource.componentForUser(self.originator.principal.uid))
</del><ins>+        originator_calendar = (yield calendar_resource.componentForUser(self.originator.record.uid))
</ins><span class="cx"> 
</span><span class="cx">         # Get attendee's view of that
</span><span class="cx">         originator_calendar.attendeesView((self.recipient.cuaddr,))
</span><span class="cx"> 
</span><span class="cx">         # Locate the attendee's copy of the event if it exists.
</span><del>-        recipient_resource = (yield getCalendarObjectForRecord(self.txn, self.recipient.principal, self.uid))
</del><ins>+        recipient_resource = (yield getCalendarObjectForRecord(self.txn, self.recipient.record, self.uid))
</ins><span class="cx"> 
</span><span class="cx">         # We only need to fix data that already exists
</span><span class="cx">         if recipient_resource is not None:
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingschedulerpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -33,11 +33,11 @@
</span><span class="cx"> from twistedcaldav.ical import Component
</span><span class="cx"> from txdav.caldav.datastore.scheduling import addressmapping
</span><span class="cx"> from txdav.caldav.datastore.scheduling.caldav.delivery import ScheduleViaCalDAV
</span><ins>+from txdav.caldav.datastore.scheduling.cuaddress import EmailCalendarUser
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.cuaddress import InvalidCalendarUser, \
</span><del>-    calendarUserFromPrincipal, OtherServerCalendarUser
</del><ins>+    OtherServerCalendarUser, calendarUserFromCalendarUserAddress
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
</span><span class="cx"> from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import EmailCalendarUser
</del><span class="cx"> from txdav.caldav.datastore.scheduling.imip.delivery import ScheduleViaIMip
</span><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.delivery import ScheduleViaISchedule
</span><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
</span><span class="lines">@@ -402,10 +402,10 @@
</span><span class="cx">         #
</span><span class="cx">         if isinstance(self.organizer, LocalCalendarUser):
</span><span class="cx">             accountingType = &quot;iTIP-VFREEBUSY&quot; if self.calendar.mainType() == &quot;VFREEBUSY&quot; else &quot;iTIP&quot;
</span><del>-            if accountingEnabled(accountingType, self.organizer.principal):
</del><ins>+            if accountingEnabled(accountingType, self.organizer.record):
</ins><span class="cx">                 emitAccounting(
</span><span class="cx">                     accountingType,
</span><del>-                    self.organizer.principal,
</del><ins>+                    self.organizer.record,
</ins><span class="cx">                     &quot;Originator: %s\nRecipients:\n%sMethod:%s\n\n%s&quot;
</span><span class="cx">                     % (
</span><span class="cx">                         str(self.originator),
</span><span class="lines">@@ -550,34 +550,35 @@
</span><span class="cx"> 
</span><span class="cx">         results = []
</span><span class="cx">         for recipient in self.recipients:
</span><del>-            # Get the principal resource for this recipient
-            principal = yield self.txn.directoryService().recordWithCalendarUserAddress(recipient)
</del><ins>+            # Get the calendar user object for this recipient
+            recipientAddress = yield calendarUserFromCalendarUserAddress(recipient, self.txn)
</ins><span class="cx"> 
</span><del>-            # If no principal we may have a remote recipient but we should check whether
</del><ins>+            # If no calendar user we may have a remote recipient but we should check whether
</ins><span class="cx">             # the address is one that ought to be on our server and treat that as a missing
</span><span class="cx">             # user. Also if server-to-server is not enabled then remote addresses are not allowed.
</span><del>-            if principal is None:
</del><ins>+            if not recipientAddress.hosted():
</ins><span class="cx">                 localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(recipient))
</span><span class="cx">                 if localUser:
</span><del>-                    log.error(&quot;No principal for calendar user address: %s&quot; % (recipient,))
</del><ins>+                    log.error(&quot;No record for calendar user address: %s&quot; % (recipient,))
</ins><span class="cx">                 else:
</span><span class="cx">                     log.error(&quot;Unknown calendar user address: %s&quot; % (recipient,))
</span><span class="cx">                 results.append(InvalidCalendarUser(recipient))
</span><span class="cx">             else:
</span><del>-                # Map recipient to their inbox
</del><ins>+                # Map recipient to their inbox and cache on calendar user object
</ins><span class="cx">                 inbox = None
</span><del>-                if principal.calendarsEnabled():
-                    if principal.thisServer():
-                        recipient_home = yield self.txn.calendarHomeWithUID(principal.uid, create=True)
</del><ins>+                if recipientAddress.validRecipient():
+                    if isinstance(recipientAddress, LocalCalendarUser):
+                        recipient_home = yield self.txn.calendarHomeWithUID(recipientAddress.record.uid, create=True)
</ins><span class="cx">                         if recipient_home:
</span><span class="cx">                             inbox = (yield recipient_home.calendarWithName(&quot;inbox&quot;))
</span><span class="cx">                     else:
</span><span class="cx">                         inbox = &quot;dummy&quot;
</span><ins>+                    recipientAddress.inbox = inbox
</ins><span class="cx"> 
</span><span class="cx">                 if inbox:
</span><del>-                    results.append(calendarUserFromPrincipal(recipient, principal, inbox))
</del><ins>+                    results.append(recipientAddress)
</ins><span class="cx">                 else:
</span><del>-                    log.error(&quot;No schedule inbox for principal: %s&quot; % (principal,))
</del><ins>+                    log.error(&quot;No scheduling for calendar user: %s&quot; % (recipient,))
</ins><span class="cx">                     results.append(InvalidCalendarUser(recipient))
</span><span class="cx"> 
</span><span class="cx">         self.recipients = results
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_cuaddresspy"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_cuaddress.py (0 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_cuaddress.py                                (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_cuaddress.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -0,0 +1,103 @@
</span><ins>+##
+# Copyright (c) 2013-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+&quot;&quot;&quot;
+Tests for txdav.caldav.datastore.cuaddress
+&quot;&quot;&quot;
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.trial import unittest
+
+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress, \
+    LocalCalendarUser, InvalidCalendarUser
+from txdav.caldav.datastore.test.util import buildCalendarStore
+from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
+
+
+class CalendarUser(CommonCommonTests, unittest.TestCase):
+    &quot;&quot;&quot;
+    Tests for deleting events older than a given date
+    &quot;&quot;&quot;
+
+    requirements = {
+        &quot;user01&quot; : {
+            &quot;calendar1&quot; : {},
+            &quot;inbox&quot; : {},
+        },
+        &quot;user02&quot; : {
+            &quot;calendar2&quot; : {},
+            &quot;inbox&quot; : {},
+        },
+        &quot;user03&quot; : {
+            &quot;calendar3&quot; : {},
+            &quot;inbox&quot; : {},
+        }
+    }
+
+    @inlineCallbacks
+    def setUp(self):
+
+        yield super(CalendarUser, self).setUp()
+        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+        self.directory = self._sqlCalendarStore.directoryService()
+        yield self.populate()
+        self.directory.removeRecord(&quot;user03&quot;)
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    def storeUnderTest(self):
+        &quot;&quot;&quot;
+        Create and return a L{CalendarStore} for testing.
+        &quot;&quot;&quot;
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def test_lookup(self):
+        &quot;&quot;&quot;
+        Test that L{CalendarUser.hosted} returns the expected results.
+        &quot;&quot;&quot;
+
+        txn = self.transactionUnderTest()
+        cu = yield calendarUserFromCalendarUserAddress(&quot;urn:x-uid:user01&quot;, txn)
+        yield self.commit()
+
+        self.assertTrue(isinstance(cu, LocalCalendarUser))
+        self.assertTrue(cu.hosted())
+        self.assertTrue(cu.validOriginator())
+        self.assertTrue(cu.validRecipient())
+
+        txn = self.transactionUnderTest()
+        cu = yield calendarUserFromCalendarUserAddress(&quot;mailto:foobar@example.org&quot;, txn)
+        yield self.commit()
+
+        self.assertTrue(isinstance(cu, InvalidCalendarUser))
+        self.assertFalse(cu.hosted())
+        self.assertFalse(cu.validOriginator())
+        self.assertFalse(cu.validRecipient())
+
+        txn = self.transactionUnderTest()
+        cu = yield calendarUserFromCalendarUserAddress(&quot;urn:x-uid:user03&quot;, txn)
+        yield self.commit()
+
+        self.assertTrue(isinstance(cu, LocalCalendarUser))
+        self.assertTrue(cu.hosted())
+        self.assertTrue(cu.validOriginator())
+        self.assertFalse(cu.validRecipient())
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_implicitpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_implicit.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_implicit.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_implicit.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -30,6 +30,7 @@
</span><span class="cx"> from twistedcaldav.ical import Component
</span><span class="cx"> from twistedcaldav.timezones import TimezoneCache
</span><span class="cx"> 
</span><ins>+from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler, \
</span><span class="cx">     ScheduleReplyWork
</span><span class="cx"> from txdav.caldav.datastore.scheduling.scheduler import ScheduleResponseQueue
</span><span class="lines">@@ -81,6 +82,15 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+class FakeTxn(object):
+
+    def directoryService(self):
+        if not hasattr(self, &quot;_directoryService&quot;):
+            self._directoryService = FakeDirectoryService()
+        return self._directoryService
+
+
+
</ins><span class="cx"> class Implicit(TestCase):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     iCalendar support tests
</span><span class="lines">@@ -817,6 +827,7 @@
</span><span class="cx">             scheduler.calendar = Component.fromString(calendar2)
</span><span class="cx"> 
</span><span class="cx">             scheduler.calendar_home = FakeCalendarHome(&quot;user01&quot;)
</span><ins>+            scheduler.txn = FakeTxn()
</ins><span class="cx"> 
</span><span class="cx">             yield scheduler.extractCalendarData()
</span><span class="cx">             scheduler.findRemovedAttendees()
</span><span class="lines">@@ -864,10 +875,14 @@
</span><span class="cx">             scheduler.reinvites = None
</span><span class="cx"> 
</span><span class="cx">             scheduler.calendar_home = FakeCalendarHome(&quot;user1&quot;)
</span><ins>+            scheduler.txn = FakeTxn()
</ins><span class="cx"> 
</span><span class="cx">             # Get some useful information from the calendar
</span><span class="cx">             yield scheduler.extractCalendarData()
</span><del>-            scheduler.organizerPrincipal = buildDirectoryRecord(scheduler.calendar_home.uid())
</del><ins>+            scheduler.organizerAddress = LocalCalendarUser(
+                &quot;mailto:user1@example.com&quot;,
+                buildDirectoryRecord(scheduler.calendar_home.uid()),
+            )
</ins><span class="cx"> 
</span><span class="cx">             recipients = []
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_utilspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_utils.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_utils.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_utils.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -15,7 +15,7 @@
</span><span class="cx"> ##
</span><span class="cx"> 
</span><span class="cx"> &quot;&quot;&quot;
</span><del>-Tests for calendarserver.tools.purge
</del><ins>+Tests for txdav.caldav.datastore.utils
</ins><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><span class="lines">@@ -24,7 +24,7 @@
</span><span class="cx"> from twisted.trial import unittest
</span><span class="cx"> 
</span><span class="cx"> from txdav.caldav.datastore.scheduling.utils import getCalendarObjectForRecord, \
</span><del>-    extractEmailDomain
</del><ins>+    extractEmailDomain, uidFromCalendarUserAddress
</ins><span class="cx"> from txdav.caldav.datastore.test.util import buildCalendarStore, \
</span><span class="cx">     buildDirectoryRecord
</span><span class="cx"> from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
</span><span class="lines">@@ -190,6 +190,25 @@
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def test_uidFromCalendarUserAddress(self):
+        &quot;&quot;&quot;
+        Test that L{uidFromCalendarUserAddress} returns the expected results.
+        &quot;&quot;&quot;
+
+        data = (
+            (&quot;urn:x-uid:foobar&quot;, &quot;foobar&quot;),
+            (&quot;urn:uuid:foobar&quot;, &quot;foobar&quot;),
+            (&quot;urn:uuid:49DE7436-F01C-4AD8-B685-A94303F40301&quot;, &quot;49DE7436-F01C-4AD8-B685-A94303F40301&quot;),
+            (&quot;/principals/__uids__/foobar&quot;, &quot;foobar&quot;),
+            (&quot;/principals/users/foobar&quot;, None),
+            (&quot;/principals/groups/foobar&quot;, None),
+            (&quot;mailto:foo@example.com&quot;, None),
+        )
+
+        for cuaddr, uid in data:
+            self.assertEqual(uidFromCalendarUserAddress(cuaddr), uid)
+
+
</ins><span class="cx">     def test_extractEmailDomain(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Test that L{extractEmailDomain} returns the expected results.
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingutilspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/utils.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/utils.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/utils.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -17,6 +17,7 @@
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from txdav.caldav.icalendarstore import ComponentRemoveState
</span><ins>+from uuid import UUID
</ins><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="lines">@@ -51,6 +52,60 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+def normalizeCUAddr(addr):
+    &quot;&quot;&quot;
+    Normalize a cuaddr string by lower()ing it if it's a mailto:, or
+    removing trailing slash if it's a URL.
+    @param addr: a cuaddr string to normalize
+    @return: normalized string
+    &quot;&quot;&quot;
+    lower = addr.lower()
+    if lower.startswith(&quot;mailto:&quot;):
+        addr = lower
+    if (addr.startswith(&quot;/&quot;) or
+        addr.startswith(&quot;http:&quot;) or
+        addr.startswith(&quot;https:&quot;)):
+        return addr.rstrip(&quot;/&quot;)
+    else:
+        return addr
+
+
+
+def uidFromCalendarUserAddress(address):
+    &quot;&quot;&quot;
+    Try to extract a record UID from a calendar user address of the appropriate format.
+    Allowed formats are urn:x-uid, urn:uuid, or /principals/(__uids__).
+
+    @param address: calendar user address to operate on
+    @type address: L{str}
+
+    @return: the extracted uid or L{None}
+    @rtype: L{str} or L{None}
+    &quot;&quot;&quot;
+
+    address = normalizeCUAddr(address)
+
+    if address.startswith(&quot;urn:x-uid:&quot;):
+        return address[10:]
+
+    elif address.startswith(&quot;urn:uuid:&quot;):
+        try:
+            UUID(address[9:])
+        except ValueError:
+            log.info(&quot;Invalid GUID: {guid}&quot;, guid=address[9:])
+            return address[9:]
+        else:
+            return address[9:]
+
+    elif address.startswith(&quot;/principals/__uids__&quot;):
+        parts = address.split(&quot;/&quot;)
+        if len(parts) == 4:
+            return parts[3]
+
+    return None
+
+
+
</ins><span class="cx"> def extractEmailDomain(mailtoURI):
</span><span class="cx">     try:
</span><span class="cx">         addr = mailtoURI[7:].split(&quot;?&quot;)[0]
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingworkpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -25,14 +25,16 @@
</span><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.ical import Component
</span><span class="cx"> 
</span><ins>+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserUID
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTipGenerator, iTIPRequestStatus
</span><span class="cx"> from txdav.caldav.icalendarstore import ComponentUpdateState
</span><span class="cx"> from txdav.common.datastore.sql_tables import schema, \
</span><span class="cx">     scheduleActionToSQL, scheduleActionFromSQL
</span><span class="cx"> 
</span><ins>+from pycalendar.datetime import DateTime
+
</ins><span class="cx"> import datetime
</span><span class="cx"> import hashlib
</span><del>-from pycalendar.datetime import DateTime
</del><span class="cx"> import traceback
</span><span class="cx"> 
</span><span class="cx"> __all__ = [
</span><span class="lines">@@ -191,8 +193,8 @@
</span><span class="cx">         try:
</span><span class="cx">             home = (yield self.transaction.calendarHomeWithResourceID(self.homeResourceID))
</span><span class="cx">             resource = (yield home.objectResourceWithID(self.resourceID))
</span><del>-            organizerPrincipal = yield home.directoryService().recordWithUID(home.uid().decode(&quot;utf-8&quot;))
-            organizer = organizerPrincipal.canonicalCalendarUserAddress()
</del><ins>+            organizerAddress = yield calendarUserFromCalendarUserUID(home.uid(), self.transaction)
+            organizer = organizerAddress.record.canonicalCalendarUserAddress()
</ins><span class="cx">             calendar_old = Component.fromString(self.icalendarTextOld) if self.icalendarTextOld else None
</span><span class="cx">             calendar_new = Component.fromString(self.icalendarTextNew) if self.icalendarTextNew else None
</span><span class="cx"> 
</span><span class="lines">@@ -313,8 +315,8 @@
</span><span class="cx">         try:
</span><span class="cx">             home = (yield self.transaction.calendarHomeWithResourceID(self.homeResourceID))
</span><span class="cx">             resource = (yield home.objectResourceWithID(self.resourceID))
</span><del>-            attendeePrincipal = yield home.directoryService().recordWithUID(home.uid().decode(&quot;utf-8&quot;))
-            attendee = attendeePrincipal.canonicalCalendarUserAddress()
</del><ins>+            attendeeAddress = yield calendarUserFromCalendarUserUID(home.uid(), self.transaction)
+            attendee = attendeeAddress.record.canonicalCalendarUserAddress()
</ins><span class="cx">             calendar = (yield resource.componentForUser())
</span><span class="cx">             organizer = calendar.validOrganizerForScheduling()
</span><span class="cx"> 
</span><span class="lines">@@ -384,8 +386,8 @@
</span><span class="cx"> 
</span><span class="cx">         try:
</span><span class="cx">             home = (yield self.transaction.calendarHomeWithResourceID(self.homeResourceID))
</span><del>-            attendeePrincipal = yield home.directoryService().recordWithUID(home.uid().decode(&quot;utf-8&quot;))
-            attendee = attendeePrincipal.canonicalCalendarUserAddress()
</del><ins>+            attendeeAddress = yield calendarUserFromCalendarUserUID(home.uid(), self.transaction)
+            attendee = attendeeAddress.record.canonicalCalendarUserAddress()
</ins><span class="cx">             calendar = Component.fromString(self.icalendarText)
</span><span class="cx">             organizer = calendar.validOrganizerForScheduling()
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/sql.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -61,8 +61,10 @@
</span><span class="cx"> from txdav.caldav.datastore.query.builder import buildExpression
</span><span class="cx"> from txdav.caldav.datastore.query.filter import Filter
</span><span class="cx"> from txdav.caldav.datastore.query.generator import CalDAVSQLQueryGenerator
</span><ins>+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.icalsplitter import iCalSplitter
</span><span class="cx"> from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
</span><ins>+from txdav.caldav.datastore.scheduling.utils import uidFromCalendarUserAddress
</ins><span class="cx"> from txdav.caldav.datastore.util import AttachmentRetrievalTransport, \
</span><span class="cx">     normalizationLookup
</span><span class="cx"> from txdav.caldav.datastore.util import CalendarObjectBase
</span><span class="lines">@@ -1993,8 +1995,6 @@
</span><span class="cx">             if groupRecord is not None:
</span><span class="cx">                 groupUID = groupRecord.uid
</span><span class="cx">             else:
</span><del>-                #FIXME:  here to avoid circular import
-                from txdav.who.util import uidFromCalendarUserAddress
</del><span class="cx">                 groupUID = uidFromCalendarUserAddress(groupCUA)
</span><span class="cx">             groupID, _ignore_name, membershipHash, _ignore_modDate = yield self._txn.groupByUID(groupUID)
</span><span class="cx"> 
</span><span class="lines">@@ -3931,10 +3931,9 @@
</span><span class="cx">             raise InvalidSplit()
</span><span class="cx"> 
</span><span class="cx">         # Cannot be attendee
</span><del>-        ownerPrincipal = yield self.calendar().ownerHome().directoryRecord()
</del><span class="cx">         organizer = component.getOrganizer()
</span><del>-        organizerPrincipal = (yield self.directoryService().recordWithCalendarUserAddress(organizer)) if organizer else None
-        if organizer is not None and organizerPrincipal.uid != ownerPrincipal.uid:
</del><ins>+        organizerAddress = (yield calendarUserFromCalendarUserAddress(organizer, self._txn)) if organizer else None
+        if organizer is not None and organizerAddress.record.uid != self.calendar().ownerHome().uid():
</ins><span class="cx">             raise InvalidSplit()
</span><span class="cx"> 
</span><span class="cx">         # Determine valid split point
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoretesttest_sqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -146,6 +146,27 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><ins>+    def test_purgingHome(self):
+        &quot;&quot;&quot;
+        Purging a calendar homes changes its status.
+        &quot;&quot;&quot;
+
+        home = yield self.homeUnderTest(name=&quot;home1&quot;)
+        self.assertFalse(home.purging())
+        self.assertFalse(home.external())
+
+        yield home.purge()
+        self.assertTrue(home.purging())
+        self.assertFalse(home.external())
+
+        yield self.commit()
+
+        home = yield self.homeUnderTest(name=&quot;home1&quot;)
+        self.assertTrue(home is not None)
+        yield self.commit()
+
+
+    @inlineCallbacks
</ins><span class="cx">     def test_migrateCalendarFromFile(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         C{_migrateCalendar()} can migrate a file-backed calendar to a database-
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoretestutilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/test/util.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/test/util.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/util.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -49,7 +49,14 @@
</span><span class="cx">             self.recordsByCUA[cuaddr] = record
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def removeRecord(self, uid):
+        record = self.records[uid]
+        del self.records[uid]
+        for cuaddr in record.calendarUserAddresses:
+            del self.recordsByCUA[cuaddr]
</ins><span class="cx"> 
</span><ins>+
+
</ins><span class="cx"> class TestCalendarStoreDirectoryRecord(TestStoreDirectoryRecord):
</span><span class="cx"> 
</span><span class="cx">     implements(ICalendarStoreDirectoryRecord)
</span></span></pre></div>
<a id="CalendarServertrunktxdavcarddavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/carddav/datastore/sql.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -246,7 +246,7 @@
</span><span class="cx">         Get the owner home for a shared child ID
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         # addressbook and home have same resourceID
</span><del>-        ownerHome = yield self._txn.homeWithResourceID(self._homeType, resourceID, create=True)
</del><ins>+        ownerHome = yield self._txn.homeWithResourceID(self._homeType, resourceID)
</ins><span class="cx">         returnValue(ownerHome)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -1120,7 +1120,7 @@
</span><span class="cx">         for groupRow in groupRows:
</span><span class="cx">             bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
</span><span class="cx">             ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
</span><del>-            ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerAddressBookID, create=True)
</del><ins>+            ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerAddressBookID)
</ins><span class="cx">             names |= set([ownerHome.uid()])
</span><span class="cx"> 
</span><span class="cx">         returnValue(tuple(names))
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/sql.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -64,7 +64,8 @@
</span><span class="cx"> from txdav.common.datastore.sql_tables import _BIND_MODE_OWN, \
</span><span class="cx">     _BIND_STATUS_ACCEPTED, _BIND_STATUS_DECLINED, _BIND_STATUS_INVALID, \
</span><span class="cx">     _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_DELETED, \
</span><del>-    _BIND_MODE_INDIRECT, _HOME_STATUS_NORMAL, _HOME_STATUS_EXTERNAL
</del><ins>+    _BIND_MODE_INDIRECT, _HOME_STATUS_NORMAL, _HOME_STATUS_EXTERNAL, \
+    _HOME_STATUS_PURGING
</ins><span class="cx"> from txdav.common.datastore.sql_tables import schema, splitSQLString
</span><span class="cx"> from txdav.common.icommondatastore import ConcurrentModification, \
</span><span class="cx">     RecordNotAllowedError, ExternalShareFailed, ShareNotAllowed, \
</span><span class="lines">@@ -208,6 +209,13 @@
</span><span class="cx">         __import__(&quot;txdav.carddav.datastore.sql&quot;)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def availablePrimaryStoreTypes(self):
+        &quot;&quot;&quot;
+        The list of store home types supported.
+        &quot;&quot;&quot;
+        return (ECALENDARTYPE, EADDRESSBOOKTYPE,)
+
+
</ins><span class="cx">     def directoryService(self):
</span><span class="cx">         return self._directoryService
</span><span class="cx"> 
</span><span class="lines">@@ -365,7 +373,37 @@
</span><span class="cx">         return self.queryCacher is not None
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def uidInStore(self, txn, uid):
+        &quot;&quot;&quot;
+        Indicate whether the specified user UID is hosted in the current store, or
+        possibly in another pod.
</ins><span class="cx"> 
</span><ins>+        @param txn: transaction to use
+        @type txn: L{CommonStoreTransaction}
+        @param uid: the user UID to test
+        @type uid: L{str}
+
+        @return: a tuple of L{bool}, L{str} - the first indicates whether the user is
+            hosted, the second the serviceNodeUID of the pod hosting the user or
+            C{None} if on this pod.
+        @rtype: L{tuple}
+        &quot;&quot;&quot;
+
+        # Check if locally stored first
+        for storeType in self.availablePrimaryStoreTypes():
+            home = yield txn.homeWithUID(storeType, uid)
+            if home is not None:
+                if home.external():
+                    # TODO: locate the pod where the user is hosted
+                    returnValue((True, &quot;unknown&quot;,))
+                else:
+                    returnValue((True, None,))
+        else:
+            returnValue((False, None,))
+
+
+
</ins><span class="cx"> class TransactionStatsCollector(object):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Used to log each SQL query and statistics about that query during the course of a single transaction.
</span><span class="lines">@@ -698,14 +736,14 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def homeWithResourceID(self, storeType, rid, create=False):
</del><ins>+    def homeWithResourceID(self, storeType, rid):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Load a calendar or addressbook home by its integer resource ID.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         uid = (yield self._homeClass[storeType]
</span><span class="cx">                .homeUIDWithResourceID(self, rid))
</span><span class="cx">         if uid:
</span><del>-            result = (yield self.homeWithUID(storeType, uid, create))
</del><ins>+            result = (yield self.homeWithUID(storeType, uid))
</ins><span class="cx">         else:
</span><span class="cx">             result = None
</span><span class="cx">         returnValue(result)
</span><span class="lines">@@ -3108,9 +3146,33 @@
</span><span class="cx"> 
</span><span class="cx">         @return: a string.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        return False
</del><ins>+        return self._status == _HOME_STATUS_EXTERNAL
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def purging(self):
+        &quot;&quot;&quot;
+        Is this an external home.
+
+        @return: a string.
+        &quot;&quot;&quot;
+        return self._status == _HOME_STATUS_PURGING
+
+
+    @inlineCallbacks
+    def purge(self):
+        &quot;&quot;&quot;
+        Mark this home as being purged.
+        &quot;&quot;&quot;
+        # Only if normal
+        if self._status == _HOME_STATUS_NORMAL:
+            yield Update(
+                {self._homeSchema.STATUS: _HOME_STATUS_PURGING},
+                Where=(self._homeSchema.RESOURCE_ID == self._resourceID),
+            ).on(self._txn)
+            self._status = _HOME_STATUS_PURGING
+            yield self._cacher.delete(self._ownerUID)
+
+
</ins><span class="cx">     def transaction(self):
</span><span class="cx">         return self._txn
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresql_schemacurrentoracledialectsql"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -39,6 +39,7 @@
</span><span class="cx"> 
</span><span class="cx"> insert into HOME_STATUS (DESCRIPTION, ID) values ('normal', 0);
</span><span class="cx"> insert into HOME_STATUS (DESCRIPTION, ID) values ('external', 1);
</span><ins>+insert into HOME_STATUS (DESCRIPTION, ID) values ('purging', 2);
</ins><span class="cx"> create table CALENDAR (
</span><span class="cx">     &quot;RESOURCE_ID&quot; integer primary key
</span><span class="cx"> );
</span><span class="lines">@@ -555,12 +556,18 @@
</span><span class="cx">     &quot;UID&quot; nvarchar2(255)
</span><span class="cx"> );
</span><span class="cx"> 
</span><ins>+create table PRINCIPAL_PURGE_HOME_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME on delete cascade
+);
+
</ins><span class="cx"> create table CALENDARSERVER (
</span><span class="cx">     &quot;NAME&quot; nvarchar2(255) primary key,
</span><span class="cx">     &quot;VALUE&quot; nvarchar2(255)
</span><span class="cx"> );
</span><span class="cx"> 
</span><del>-insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '40');
</del><ins>+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '41');
</ins><span class="cx"> insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '6');
</span><span class="cx"> insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
</span><span class="cx"> insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
</span><span class="lines">@@ -841,6 +848,14 @@
</span><span class="cx">     JOB_ID
</span><span class="cx"> );
</span><span class="cx"> 
</span><ins>+create index PRINCIPAL_PURGE_HOME__f35eea7a on PRINCIPAL_PURGE_HOME_WORK (
+    JOB_ID
+);
+
+create index PRINCIPAL_PURGE_HOME__967e4480 on PRINCIPAL_PURGE_HOME_WORK (
+    HOME_RESOURCE_ID
+);
+
</ins><span class="cx"> -- Skipped Function next_job
</span><span class="cx"> 
</span><span class="cx"> -- Extras
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresql_schemacurrentsql"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -91,6 +91,7 @@
</span><span class="cx"> 
</span><span class="cx"> insert into HOME_STATUS values (0, 'normal' );
</span><span class="cx"> insert into HOME_STATUS values (1, 'external');
</span><ins>+insert into HOME_STATUS values (2, 'purging');
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> --------------
</span><span class="lines">@@ -1055,6 +1056,22 @@
</span><span class="cx">   PRINCIPAL_PURGE_WORK(JOB_ID);
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+--------------------------------
+-- Principal Home Remove Work --
+--------------------------------
+
+create table PRINCIPAL_PURGE_HOME_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade
+);
+
+create index PRINCIPAL_PURGE_HOME_WORK_JOB_ID on
+  PRINCIPAL_PURGE_HOME_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_HOME_HOME_RESOURCE_ID on
+  PRINCIPAL_PURGE_HOME_WORK(HOME_RESOURCE_ID);
+
+
</ins><span class="cx"> --------------------
</span><span class="cx"> -- Schema Version --
</span><span class="cx"> --------------------
</span><span class="lines">@@ -1064,7 +1081,7 @@
</span><span class="cx">   VALUE                         varchar(255)
</span><span class="cx"> );
</span><span class="cx"> 
</span><del>-insert into CALENDARSERVER values ('VERSION', '40');
</del><ins>+insert into CALENDARSERVER values ('VERSION', '41');
</ins><span class="cx"> insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '6');
</span><span class="cx"> insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
</span><span class="cx"> insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresql_schemaoldoracledialectv40sql"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v40.sql (0 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v40.sql                                (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v40.sql        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -0,0 +1,858 @@
</span><ins>+create sequence RESOURCE_ID_SEQ;
+create sequence JOB_SEQ;
+create sequence INSTANCE_ID_SEQ;
+create sequence ATTACHMENT_ID_SEQ;
+create sequence REVISION_SEQ;
+create sequence WORKITEM_SEQ;
+create table NODE_INFO (
+    &quot;HOSTNAME&quot; nvarchar2(255),
+    &quot;PID&quot; integer not null,
+    &quot;PORT&quot; integer not null,
+    &quot;TIME&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC' not null, 
+    primary key (&quot;HOSTNAME&quot;, &quot;PORT&quot;)
+);
+
+create table NAMED_LOCK (
+    &quot;LOCK_NAME&quot; nvarchar2(255) primary key
+);
+
+create table JOB (
+    &quot;JOB_ID&quot; integer primary key not null,
+    &quot;WORK_TYPE&quot; nvarchar2(255),
+    &quot;PRIORITY&quot; integer default 0,
+    &quot;WEIGHT&quot; integer default 0,
+    &quot;NOT_BEFORE&quot; timestamp default null,
+    &quot;NOT_AFTER&quot; timestamp default null
+);
+
+create table CALENDAR_HOME (
+    &quot;RESOURCE_ID&quot; integer primary key,
+    &quot;OWNER_UID&quot; nvarchar2(255) unique,
+    &quot;STATUS&quot; integer default 0 not null,
+    &quot;DATAVERSION&quot; integer default 0 not null
+);
+
+create table HOME_STATUS (
+    &quot;ID&quot; integer primary key,
+    &quot;DESCRIPTION&quot; nvarchar2(16) unique
+);
+
+insert into HOME_STATUS (DESCRIPTION, ID) values ('normal', 0);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('external', 1);
+create table CALENDAR (
+    &quot;RESOURCE_ID&quot; integer primary key
+);
+
+create table CALENDAR_HOME_METADATA (
+    &quot;RESOURCE_ID&quot; integer primary key references CALENDAR_HOME on delete cascade,
+    &quot;QUOTA_USED_BYTES&quot; integer default 0 not null,
+    &quot;DEFAULT_EVENTS&quot; integer default null references CALENDAR on delete set null,
+    &quot;DEFAULT_TASKS&quot; integer default null references CALENDAR on delete set null,
+    &quot;DEFAULT_POLLS&quot; integer default null references CALENDAR on delete set null,
+    &quot;ALARM_VEVENT_TIMED&quot; nclob default null,
+    &quot;ALARM_VEVENT_ALLDAY&quot; nclob default null,
+    &quot;ALARM_VTODO_TIMED&quot; nclob default null,
+    &quot;ALARM_VTODO_ALLDAY&quot; nclob default null,
+    &quot;AVAILABILITY&quot; nclob default null,
+    &quot;CREATED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDAR_METADATA (
+    &quot;RESOURCE_ID&quot; integer primary key references CALENDAR on delete cascade,
+    &quot;SUPPORTED_COMPONENTS&quot; nvarchar2(255) default null,
+    &quot;CREATED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table NOTIFICATION_HOME (
+    &quot;RESOURCE_ID&quot; integer primary key,
+    &quot;OWNER_UID&quot; nvarchar2(255) unique,
+    &quot;STATUS&quot; integer default 0 not null,
+    &quot;DATAVERSION&quot; integer default 0 not null
+);
+
+create table NOTIFICATION (
+    &quot;RESOURCE_ID&quot; integer primary key,
+    &quot;NOTIFICATION_HOME_RESOURCE_ID&quot; integer not null references NOTIFICATION_HOME,
+    &quot;NOTIFICATION_UID&quot; nvarchar2(255),
+    &quot;NOTIFICATION_TYPE&quot; nvarchar2(255),
+    &quot;NOTIFICATION_DATA&quot; nclob,
+    &quot;MD5&quot; nchar(32),
+    &quot;CREATED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique (&quot;NOTIFICATION_UID&quot;, &quot;NOTIFICATION_HOME_RESOURCE_ID&quot;)
+);
+
+create table CALENDAR_BIND (
+    &quot;CALENDAR_HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME,
+    &quot;CALENDAR_RESOURCE_ID&quot; integer not null references CALENDAR on delete cascade,
+    &quot;EXTERNAL_ID&quot; integer default null,
+    &quot;CALENDAR_RESOURCE_NAME&quot; nvarchar2(255),
+    &quot;BIND_MODE&quot; integer not null,
+    &quot;BIND_STATUS&quot; integer not null,
+    &quot;BIND_REVISION&quot; integer default 0 not null,
+    &quot;MESSAGE&quot; nclob,
+    &quot;TRANSP&quot; integer default 0 not null,
+    &quot;ALARM_VEVENT_TIMED&quot; nclob default null,
+    &quot;ALARM_VEVENT_ALLDAY&quot; nclob default null,
+    &quot;ALARM_VTODO_TIMED&quot; nclob default null,
+    &quot;ALARM_VTODO_ALLDAY&quot; nclob default null,
+    &quot;TIMEZONE&quot; nclob default null, 
+    primary key (&quot;CALENDAR_HOME_RESOURCE_ID&quot;, &quot;CALENDAR_RESOURCE_ID&quot;), 
+    unique (&quot;CALENDAR_HOME_RESOURCE_ID&quot;, &quot;CALENDAR_RESOURCE_NAME&quot;)
+);
+
+create table CALENDAR_BIND_MODE (
+    &quot;ID&quot; integer primary key,
+    &quot;DESCRIPTION&quot; nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('own', 0);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('write', 2);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('direct', 3);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('indirect', 4);
+create table CALENDAR_BIND_STATUS (
+    &quot;ID&quot; integer primary key,
+    &quot;DESCRIPTION&quot; nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invited', 0);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('accepted', 1);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('declined', 2);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invalid', 3);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('deleted', 4);
+create table CALENDAR_TRANSP (
+    &quot;ID&quot; integer primary key,
+    &quot;DESCRIPTION&quot; nvarchar2(16) unique
+);
+
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('opaque', 0);
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('transparent', 1);
+create table CALENDAR_OBJECT (
+    &quot;RESOURCE_ID&quot; integer primary key,
+    &quot;CALENDAR_RESOURCE_ID&quot; integer not null references CALENDAR on delete cascade,
+    &quot;RESOURCE_NAME&quot; nvarchar2(255),
+    &quot;ICALENDAR_TEXT&quot; nclob,
+    &quot;ICALENDAR_UID&quot; nvarchar2(255),
+    &quot;ICALENDAR_TYPE&quot; nvarchar2(255),
+    &quot;ATTACHMENTS_MODE&quot; integer default 0 not null,
+    &quot;DROPBOX_ID&quot; nvarchar2(255),
+    &quot;ORGANIZER&quot; nvarchar2(255),
+    &quot;RECURRANCE_MIN&quot; date,
+    &quot;RECURRANCE_MAX&quot; date,
+    &quot;ACCESS&quot; integer default 0 not null,
+    &quot;SCHEDULE_OBJECT&quot; integer default 0,
+    &quot;SCHEDULE_TAG&quot; nvarchar2(36) default null,
+    &quot;SCHEDULE_ETAGS&quot; nclob default null,
+    &quot;PRIVATE_COMMENTS&quot; integer default 0 not null,
+    &quot;MD5&quot; nchar(32),
+    &quot;CREATED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique (&quot;CALENDAR_RESOURCE_ID&quot;, &quot;RESOURCE_NAME&quot;)
+);
+
+create table CALENDAR_OBJ_ATTACHMENTS_MODE (
+    &quot;ID&quot; integer primary key,
+    &quot;DESCRIPTION&quot; nvarchar2(16) unique
+);
+
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('none', 0);
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('write', 2);
+create table CALENDAR_ACCESS_TYPE (
+    &quot;ID&quot; integer primary key,
+    &quot;DESCRIPTION&quot; nvarchar2(32) unique
+);
+
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('', 0);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('public', 1);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('private', 2);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('confidential', 3);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('restricted', 4);
+create table TIME_RANGE (
+    &quot;INSTANCE_ID&quot; integer primary key,
+    &quot;CALENDAR_RESOURCE_ID&quot; integer not null references CALENDAR on delete cascade,
+    &quot;CALENDAR_OBJECT_RESOURCE_ID&quot; integer not null references CALENDAR_OBJECT on delete cascade,
+    &quot;FLOATING&quot; integer not null,
+    &quot;START_DATE&quot; timestamp not null,
+    &quot;END_DATE&quot; timestamp not null,
+    &quot;FBTYPE&quot; integer not null,
+    &quot;TRANSPARENT&quot; integer not null
+);
+
+create table FREE_BUSY_TYPE (
+    &quot;ID&quot; integer primary key,
+    &quot;DESCRIPTION&quot; nvarchar2(16) unique
+);
+
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('unknown', 0);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('free', 1);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy', 2);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-unavailable', 3);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-tentative', 4);
+create table PERUSER (
+    &quot;TIME_RANGE_INSTANCE_ID&quot; integer not null references TIME_RANGE on delete cascade,
+    &quot;USER_ID&quot; nvarchar2(255),
+    &quot;TRANSPARENT&quot; integer not null,
+    &quot;ADJUSTED_START_DATE&quot; timestamp default null,
+    &quot;ADJUSTED_END_DATE&quot; timestamp default null
+);
+
+create table ATTACHMENT (
+    &quot;ATTACHMENT_ID&quot; integer primary key,
+    &quot;CALENDAR_HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME,
+    &quot;DROPBOX_ID&quot; nvarchar2(255),
+    &quot;CONTENT_TYPE&quot; nvarchar2(255),
+    &quot;SIZE&quot; integer not null,
+    &quot;MD5&quot; nchar(32),
+    &quot;CREATED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    &quot;PATH&quot; nvarchar2(1024)
+);
+
+create table ATTACHMENT_CALENDAR_OBJECT (
+    &quot;ATTACHMENT_ID&quot; integer not null references ATTACHMENT on delete cascade,
+    &quot;MANAGED_ID&quot; nvarchar2(255),
+    &quot;CALENDAR_OBJECT_RESOURCE_ID&quot; integer not null references CALENDAR_OBJECT on delete cascade, 
+    primary key (&quot;ATTACHMENT_ID&quot;, &quot;CALENDAR_OBJECT_RESOURCE_ID&quot;), 
+    unique (&quot;MANAGED_ID&quot;, &quot;CALENDAR_OBJECT_RESOURCE_ID&quot;)
+);
+
+create table RESOURCE_PROPERTY (
+    &quot;RESOURCE_ID&quot; integer not null,
+    &quot;NAME&quot; nvarchar2(255),
+    &quot;VALUE&quot; nclob,
+    &quot;VIEWER_UID&quot; nvarchar2(255), 
+    primary key (&quot;RESOURCE_ID&quot;, &quot;NAME&quot;, &quot;VIEWER_UID&quot;)
+);
+
+create table ADDRESSBOOK_HOME (
+    &quot;RESOURCE_ID&quot; integer primary key,
+    &quot;ADDRESSBOOK_PROPERTY_STORE_ID&quot; integer not null,
+    &quot;OWNER_UID&quot; nvarchar2(255) unique,
+    &quot;STATUS&quot; integer default 0 not null,
+    &quot;DATAVERSION&quot; integer default 0 not null
+);
+
+create table ADDRESSBOOK_HOME_METADATA (
+    &quot;RESOURCE_ID&quot; integer primary key references ADDRESSBOOK_HOME on delete cascade,
+    &quot;QUOTA_USED_BYTES&quot; integer default 0 not null,
+    &quot;CREATED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table SHARED_ADDRESSBOOK_BIND (
+    &quot;ADDRESSBOOK_HOME_RESOURCE_ID&quot; integer not null references ADDRESSBOOK_HOME,
+    &quot;OWNER_HOME_RESOURCE_ID&quot; integer not null references ADDRESSBOOK_HOME on delete cascade,
+    &quot;EXTERNAL_ID&quot; integer default null,
+    &quot;ADDRESSBOOK_RESOURCE_NAME&quot; nvarchar2(255),
+    &quot;BIND_MODE&quot; integer not null,
+    &quot;BIND_STATUS&quot; integer not null,
+    &quot;BIND_REVISION&quot; integer default 0 not null,
+    &quot;MESSAGE&quot; nclob, 
+    primary key (&quot;ADDRESSBOOK_HOME_RESOURCE_ID&quot;, &quot;OWNER_HOME_RESOURCE_ID&quot;), 
+    unique (&quot;ADDRESSBOOK_HOME_RESOURCE_ID&quot;, &quot;ADDRESSBOOK_RESOURCE_NAME&quot;)
+);
+
+create table ADDRESSBOOK_OBJECT (
+    &quot;RESOURCE_ID&quot; integer primary key,
+    &quot;ADDRESSBOOK_HOME_RESOURCE_ID&quot; integer not null references ADDRESSBOOK_HOME on delete cascade,
+    &quot;RESOURCE_NAME&quot; nvarchar2(255),
+    &quot;VCARD_TEXT&quot; nclob,
+    &quot;VCARD_UID&quot; nvarchar2(255),
+    &quot;KIND&quot; integer not null,
+    &quot;MD5&quot; nchar(32),
+    &quot;CREATED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique (&quot;ADDRESSBOOK_HOME_RESOURCE_ID&quot;, &quot;RESOURCE_NAME&quot;), 
+    unique (&quot;ADDRESSBOOK_HOME_RESOURCE_ID&quot;, &quot;VCARD_UID&quot;)
+);
+
+create table ADDRESSBOOK_OBJECT_KIND (
+    &quot;ID&quot; integer primary key,
+    &quot;DESCRIPTION&quot; nvarchar2(16) unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('person', 0);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('group', 1);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('resource', 2);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('location', 3);
+create table ABO_MEMBERS (
+    &quot;GROUP_ID&quot; integer not null,
+    &quot;ADDRESSBOOK_ID&quot; integer not null references ADDRESSBOOK_HOME on delete cascade,
+    &quot;MEMBER_ID&quot; integer not null,
+    &quot;REVISION&quot; integer not null,
+    &quot;REMOVED&quot; integer default 0 not null,
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    primary key (&quot;GROUP_ID&quot;, &quot;MEMBER_ID&quot;, &quot;REVISION&quot;)
+);
+
+create table ABO_FOREIGN_MEMBERS (
+    &quot;GROUP_ID&quot; integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+    &quot;ADDRESSBOOK_ID&quot; integer not null references ADDRESSBOOK_HOME on delete cascade,
+    &quot;MEMBER_ADDRESS&quot; nvarchar2(255), 
+    primary key (&quot;GROUP_ID&quot;, &quot;MEMBER_ADDRESS&quot;)
+);
+
+create table SHARED_GROUP_BIND (
+    &quot;ADDRESSBOOK_HOME_RESOURCE_ID&quot; integer not null references ADDRESSBOOK_HOME,
+    &quot;GROUP_RESOURCE_ID&quot; integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+    &quot;EXTERNAL_ID&quot; integer default null,
+    &quot;GROUP_ADDRESSBOOK_NAME&quot; nvarchar2(255),
+    &quot;BIND_MODE&quot; integer not null,
+    &quot;BIND_STATUS&quot; integer not null,
+    &quot;BIND_REVISION&quot; integer default 0 not null,
+    &quot;MESSAGE&quot; nclob, 
+    primary key (&quot;ADDRESSBOOK_HOME_RESOURCE_ID&quot;, &quot;GROUP_RESOURCE_ID&quot;), 
+    unique (&quot;ADDRESSBOOK_HOME_RESOURCE_ID&quot;, &quot;GROUP_ADDRESSBOOK_NAME&quot;)
+);
+
+create table CALENDAR_OBJECT_REVISIONS (
+    &quot;CALENDAR_HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME,
+    &quot;CALENDAR_RESOURCE_ID&quot; integer references CALENDAR,
+    &quot;CALENDAR_NAME&quot; nvarchar2(255) default null,
+    &quot;RESOURCE_NAME&quot; nvarchar2(255),
+    &quot;REVISION&quot; integer not null,
+    &quot;DELETED&quot; integer not null,
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+    &quot;ADDRESSBOOK_HOME_RESOURCE_ID&quot; integer not null references ADDRESSBOOK_HOME,
+    &quot;OWNER_HOME_RESOURCE_ID&quot; integer references ADDRESSBOOK_HOME,
+    &quot;ADDRESSBOOK_NAME&quot; nvarchar2(255) default null,
+    &quot;OBJECT_RESOURCE_ID&quot; integer default 0,
+    &quot;RESOURCE_NAME&quot; nvarchar2(255),
+    &quot;REVISION&quot; integer not null,
+    &quot;DELETED&quot; integer not null,
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+    &quot;NOTIFICATION_HOME_RESOURCE_ID&quot; integer not null references NOTIFICATION_HOME on delete cascade,
+    &quot;RESOURCE_NAME&quot; nvarchar2(255),
+    &quot;REVISION&quot; integer not null,
+    &quot;DELETED&quot; integer not null,
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique (&quot;NOTIFICATION_HOME_RESOURCE_ID&quot;, &quot;RESOURCE_NAME&quot;)
+);
+
+create table APN_SUBSCRIPTIONS (
+    &quot;TOKEN&quot; nvarchar2(255),
+    &quot;RESOURCE_KEY&quot; nvarchar2(255),
+    &quot;MODIFIED&quot; integer not null,
+    &quot;SUBSCRIBER_GUID&quot; nvarchar2(255),
+    &quot;USER_AGENT&quot; nvarchar2(255) default null,
+    &quot;IP_ADDR&quot; nvarchar2(255) default null, 
+    primary key (&quot;TOKEN&quot;, &quot;RESOURCE_KEY&quot;)
+);
+
+create table IMIP_TOKENS (
+    &quot;TOKEN&quot; nvarchar2(255),
+    &quot;ORGANIZER&quot; nvarchar2(255),
+    &quot;ATTENDEE&quot; nvarchar2(255),
+    &quot;ICALUID&quot; nvarchar2(255),
+    &quot;ACCESSED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    primary key (&quot;ORGANIZER&quot;, &quot;ATTENDEE&quot;, &quot;ICALUID&quot;)
+);
+
+create table IMIP_INVITATION_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;FROM_ADDR&quot; nvarchar2(255),
+    &quot;TO_ADDR&quot; nvarchar2(255),
+    &quot;ICALENDAR_TEXT&quot; nclob
+);
+
+create table IMIP_POLLING_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB
+);
+
+create table IMIP_REPLY_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;ORGANIZER&quot; nvarchar2(255),
+    &quot;ATTENDEE&quot; nvarchar2(255),
+    &quot;ICALENDAR_TEXT&quot; nclob
+);
+
+create table PUSH_NOTIFICATION_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;PUSH_ID&quot; nvarchar2(255),
+    &quot;PUSH_PRIORITY&quot; integer not null
+);
+
+create table GROUP_CACHER_POLLING_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB
+);
+
+create table GROUP_REFRESH_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;GROUP_UID&quot; nvarchar2(255)
+);
+
+create table GROUP_ATTENDEE_RECONCILE_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;RESOURCE_ID&quot; integer,
+    &quot;GROUP_ID&quot; integer
+);
+
+create table GROUPS (
+    &quot;GROUP_ID&quot; integer primary key,
+    &quot;NAME&quot; nvarchar2(255),
+    &quot;GROUP_UID&quot; nvarchar2(255),
+    &quot;MEMBERSHIP_HASH&quot; nvarchar2(255),
+    &quot;EXTANT&quot; integer default 1,
+    &quot;CREATED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table GROUP_MEMBERSHIP (
+    &quot;GROUP_ID&quot; integer not null references GROUPS on delete cascade,
+    &quot;MEMBER_UID&quot; nvarchar2(255), 
+    primary key (&quot;GROUP_ID&quot;, &quot;MEMBER_UID&quot;)
+);
+
+create table GROUP_ATTENDEE (
+    &quot;GROUP_ID&quot; integer not null references GROUPS on delete cascade,
+    &quot;RESOURCE_ID&quot; integer not null references CALENDAR_OBJECT on delete cascade,
+    &quot;MEMBERSHIP_HASH&quot; nvarchar2(255), 
+    primary key (&quot;GROUP_ID&quot;, &quot;RESOURCE_ID&quot;)
+);
+
+create table DELEGATES (
+    &quot;DELEGATOR&quot; nvarchar2(255),
+    &quot;DELEGATE&quot; nvarchar2(255),
+    &quot;READ_WRITE&quot; integer not null, 
+    primary key (&quot;DELEGATOR&quot;, &quot;READ_WRITE&quot;, &quot;DELEGATE&quot;)
+);
+
+create table DELEGATE_GROUPS (
+    &quot;DELEGATOR&quot; nvarchar2(255),
+    &quot;GROUP_ID&quot; integer not null references GROUPS on delete cascade,
+    &quot;READ_WRITE&quot; integer not null,
+    &quot;IS_EXTERNAL&quot; integer not null, 
+    primary key (&quot;DELEGATOR&quot;, &quot;READ_WRITE&quot;, &quot;GROUP_ID&quot;)
+);
+
+create table EXTERNAL_DELEGATE_GROUPS (
+    &quot;DELEGATOR&quot; nvarchar2(255) primary key,
+    &quot;GROUP_UID_READ&quot; nvarchar2(255),
+    &quot;GROUP_UID_WRITE&quot; nvarchar2(255)
+);
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;RESOURCE_ID&quot; integer not null references CALENDAR_OBJECT on delete cascade
+);
+
+create table FIND_MIN_VALID_REVISION_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB
+);
+
+create table REVISION_CLEANUP_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB
+);
+
+create table INBOX_CLEANUP_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB
+);
+
+create table CLEANUP_ONE_INBOX_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;HOME_ID&quot; integer not null unique references CALENDAR_HOME on delete cascade
+);
+
+create table SCHEDULE_REFRESH_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;ICALENDAR_UID&quot; nvarchar2(255),
+    &quot;HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME on delete cascade,
+    &quot;RESOURCE_ID&quot; integer not null references CALENDAR_OBJECT on delete cascade,
+    &quot;ATTENDEE_COUNT&quot; integer
+);
+
+create table SCHEDULE_REFRESH_ATTENDEES (
+    &quot;RESOURCE_ID&quot; integer not null references CALENDAR_OBJECT on delete cascade,
+    &quot;ATTENDEE&quot; nvarchar2(255), 
+    primary key (&quot;RESOURCE_ID&quot;, &quot;ATTENDEE&quot;)
+);
+
+create table SCHEDULE_AUTO_REPLY_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;ICALENDAR_UID&quot; nvarchar2(255),
+    &quot;HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME on delete cascade,
+    &quot;RESOURCE_ID&quot; integer not null references CALENDAR_OBJECT on delete cascade,
+    &quot;PARTSTAT&quot; nvarchar2(255)
+);
+
+create table SCHEDULE_ORGANIZER_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;ICALENDAR_UID&quot; nvarchar2(255),
+    &quot;SCHEDULE_ACTION&quot; integer not null,
+    &quot;HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME on delete cascade,
+    &quot;RESOURCE_ID&quot; integer,
+    &quot;ICALENDAR_TEXT_OLD&quot; nclob,
+    &quot;ICALENDAR_TEXT_NEW&quot; nclob,
+    &quot;ATTENDEE_COUNT&quot; integer,
+    &quot;SMART_MERGE&quot; integer
+);
+
+create table SCHEDULE_ACTION (
+    &quot;ID&quot; integer primary key,
+    &quot;DESCRIPTION&quot; nvarchar2(16) unique
+);
+
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('create', 0);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify', 1);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify-cancelled', 2);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('remove', 3);
+create table SCHEDULE_REPLY_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;ICALENDAR_UID&quot; nvarchar2(255),
+    &quot;HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME on delete cascade,
+    &quot;RESOURCE_ID&quot; integer not null references CALENDAR_OBJECT on delete cascade,
+    &quot;CHANGED_RIDS&quot; nclob
+);
+
+create table SCHEDULE_REPLY_CANCEL_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;ICALENDAR_UID&quot; nvarchar2(255),
+    &quot;HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME on delete cascade,
+    &quot;ICALENDAR_TEXT&quot; nclob
+);
+
+create table PRINCIPAL_PURGE_POLLING_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB
+);
+
+create table PRINCIPAL_PURGE_CHECK_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;UID&quot; nvarchar2(255)
+);
+
+create table PRINCIPAL_PURGE_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;UID&quot; nvarchar2(255)
+);
+
+create table CALENDARSERVER (
+    &quot;NAME&quot; nvarchar2(255) primary key,
+    &quot;VALUE&quot; nvarchar2(255)
+);
+
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '40');
+insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '6');
+insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
+insert into CALENDARSERVER (NAME, VALUE) values ('MIN-VALID-REVISION', '1');
+create index CALENDAR_HOME_METADAT_3cb9049e on CALENDAR_HOME_METADATA (
+    DEFAULT_EVENTS
+);
+
+create index CALENDAR_HOME_METADAT_d55e5548 on CALENDAR_HOME_METADATA (
+    DEFAULT_TASKS
+);
+
+create index CALENDAR_HOME_METADAT_910264ce on CALENDAR_HOME_METADATA (
+    DEFAULT_POLLS
+);
+
+create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (
+    NOTIFICATION_HOME_RESOURCE_ID
+);
+
+create index CALENDAR_BIND_RESOURC_e57964d4 on CALENDAR_BIND (
+    CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_CALEN_a9a453a9 on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_CALEN_96e83b73 on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    RECURRANCE_MAX
+);
+
+create index CALENDAR_OBJECT_ICALE_82e731d5 on CALENDAR_OBJECT (
+    ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_DROPB_de041d80 on CALENDAR_OBJECT (
+    DROPBOX_ID
+);
+
+create index TIME_RANGE_CALENDAR_R_beb6e7eb on TIME_RANGE (
+    CALENDAR_RESOURCE_ID
+);
+
+create index TIME_RANGE_CALENDAR_O_acf37bd1 on TIME_RANGE (
+    CALENDAR_OBJECT_RESOURCE_ID
+);
+
+create index PERUSER_TIME_RANGE_IN_5468a226 on PERUSER (
+    TIME_RANGE_INSTANCE_ID
+);
+
+create index ATTACHMENT_CALENDAR_H_0078845c on ATTACHMENT (
+    CALENDAR_HOME_RESOURCE_ID
+);
+
+create index ATTACHMENT_DROPBOX_ID_5073cf23 on ATTACHMENT (
+    DROPBOX_ID
+);
+
+create index ATTACHMENT_CALENDAR_O_81508484 on ATTACHMENT_CALENDAR_OBJECT (
+    CALENDAR_OBJECT_RESOURCE_ID
+);
+
+create index SHARED_ADDRESSBOOK_BI_e9a2e6d4 on SHARED_ADDRESSBOOK_BIND (
+    OWNER_HOME_RESOURCE_ID
+);
+
+create index ABO_MEMBERS_ADDRESSBO_4effa879 on ABO_MEMBERS (
+    ADDRESSBOOK_ID
+);
+
+create index ABO_MEMBERS_MEMBER_ID_8d66adcf on ABO_MEMBERS (
+    MEMBER_ID
+);
+
+create index ABO_FOREIGN_MEMBERS_A_1fd2c5e9 on ABO_FOREIGN_MEMBERS (
+    ADDRESSBOOK_ID
+);
+
+create index SHARED_GROUP_BIND_RES_cf52f95d on SHARED_GROUP_BIND (
+    GROUP_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_3a3956c4 on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_HOME_RESOURCE_ID,
+    CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_6d9d929c on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    RESOURCE_NAME,
+    DELETED,
+    REVISION
+);
+
+create index CALENDAR_OBJECT_REVIS_265c8acf on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_2bfcf757 on ADDRESSBOOK_OBJECT_REVISIONS (
+    ADDRESSBOOK_HOME_RESOURCE_ID,
+    OWNER_HOME_RESOURCE_ID
+);
+
+create index ADDRESSBOOK_OBJECT_RE_00fe8288 on ADDRESSBOOK_OBJECT_REVISIONS (
+    OWNER_HOME_RESOURCE_ID,
+    RESOURCE_NAME,
+    DELETED,
+    REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_45004780 on ADDRESSBOOK_OBJECT_REVISIONS (
+    OWNER_HOME_RESOURCE_ID,
+    REVISION
+);
+
+create index NOTIFICATION_OBJECT_R_036a9cee on NOTIFICATION_OBJECT_REVISIONS (
+    NOTIFICATION_HOME_RESOURCE_ID,
+    REVISION
+);
+
+create index APN_SUBSCRIPTIONS_RES_9610d78e on APN_SUBSCRIPTIONS (
+    RESOURCE_KEY
+);
+
+create index IMIP_TOKENS_TOKEN_e94b918f on IMIP_TOKENS (
+    TOKEN
+);
+
+create index IMIP_INVITATION_WORK__586d064c on IMIP_INVITATION_WORK (
+    JOB_ID
+);
+
+create index IMIP_POLLING_WORK_JOB_d5535891 on IMIP_POLLING_WORK (
+    JOB_ID
+);
+
+create index IMIP_REPLY_WORK_JOB_I_bf4ae73e on IMIP_REPLY_WORK (
+    JOB_ID
+);
+
+create index PUSH_NOTIFICATION_WOR_8bbab117 on PUSH_NOTIFICATION_WORK (
+    JOB_ID
+);
+
+create index GROUP_CACHER_POLLING__6eb3151c on GROUP_CACHER_POLLING_WORK (
+    JOB_ID
+);
+
+create index GROUP_REFRESH_WORK_JO_717ede20 on GROUP_REFRESH_WORK (
+    JOB_ID
+);
+
+create index GROUP_ATTENDEE_RECONC_da73d3c2 on GROUP_ATTENDEE_RECONCILE_WORK (
+    JOB_ID
+);
+
+create index GROUPS_GROUP_UID_b35cce23 on GROUPS (
+    GROUP_UID
+);
+
+create index GROUP_MEMBERSHIP_MEMB_0ca508e8 on GROUP_MEMBERSHIP (
+    MEMBER_UID
+);
+
+create index GROUP_ATTENDEE_RESOUR_855124dc on GROUP_ATTENDEE (
+    RESOURCE_ID
+);
+
+create index DELEGATE_TO_DELEGATOR_5e149b11 on DELEGATES (
+    DELEGATE,
+    READ_WRITE,
+    DELEGATOR
+);
+
+create index DELEGATE_GROUPS_GROUP_25117446 on DELEGATE_GROUPS (
+    GROUP_ID
+);
+
+create index CALENDAR_OBJECT_SPLIT_af71dcda on CALENDAR_OBJECT_SPLITTER_WORK (
+    RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_SPLIT_33603b72 on CALENDAR_OBJECT_SPLITTER_WORK (
+    JOB_ID
+);
+
+create index FIND_MIN_VALID_REVISI_78d17400 on FIND_MIN_VALID_REVISION_WORK (
+    JOB_ID
+);
+
+create index REVISION_CLEANUP_WORK_eb062686 on REVISION_CLEANUP_WORK (
+    JOB_ID
+);
+
+create index INBOX_CLEANUP_WORK_JO_799132bd on INBOX_CLEANUP_WORK (
+    JOB_ID
+);
+
+create index CLEANUP_ONE_INBOX_WOR_375dac36 on CLEANUP_ONE_INBOX_WORK (
+    JOB_ID
+);
+
+create index SCHEDULE_REFRESH_WORK_26084c7b on SCHEDULE_REFRESH_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index SCHEDULE_REFRESH_WORK_989efe54 on SCHEDULE_REFRESH_WORK (
+    RESOURCE_ID
+);
+
+create index SCHEDULE_REFRESH_WORK_3ffa2718 on SCHEDULE_REFRESH_WORK (
+    JOB_ID
+);
+
+create index SCHEDULE_REFRESH_ATTE_83053b91 on SCHEDULE_REFRESH_ATTENDEES (
+    RESOURCE_ID,
+    ATTENDEE
+);
+
+create index SCHEDULE_AUTO_REPLY_W_0256478d on SCHEDULE_AUTO_REPLY_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index SCHEDULE_AUTO_REPLY_W_0755e754 on SCHEDULE_AUTO_REPLY_WORK (
+    RESOURCE_ID
+);
+
+create index SCHEDULE_AUTO_REPLY_W_4d7bb5a8 on SCHEDULE_AUTO_REPLY_WORK (
+    JOB_ID
+);
+
+create index SCHEDULE_ORGANIZER_WO_18ce4edd on SCHEDULE_ORGANIZER_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index SCHEDULE_ORGANIZER_WO_14702035 on SCHEDULE_ORGANIZER_WORK (
+    RESOURCE_ID
+);
+
+create index SCHEDULE_ORGANIZER_WO_1e9f246d on SCHEDULE_ORGANIZER_WORK (
+    JOB_ID
+);
+
+create index SCHEDULE_REPLY_WORK_H_745af8cf on SCHEDULE_REPLY_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index SCHEDULE_REPLY_WORK_R_11bd3fbb on SCHEDULE_REPLY_WORK (
+    RESOURCE_ID
+);
+
+create index SCHEDULE_REPLY_WORK_J_5913b4a4 on SCHEDULE_REPLY_WORK (
+    JOB_ID
+);
+
+create index SCHEDULE_REPLY_CANCEL_dab513ef on SCHEDULE_REPLY_CANCEL_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index SCHEDULE_REPLY_CANCEL_94a0c766 on SCHEDULE_REPLY_CANCEL_WORK (
+    JOB_ID
+);
+
+create index PRINCIPAL_PURGE_POLLI_6383e68a on PRINCIPAL_PURGE_POLLING_WORK (
+    JOB_ID
+);
+
+create index PRINCIPAL_PURGE_CHECK_b0c024c1 on PRINCIPAL_PURGE_CHECK_WORK (
+    JOB_ID
+);
+
+create index PRINCIPAL_PURGE_WORK__7a8141a3 on PRINCIPAL_PURGE_WORK (
+    JOB_ID
+);
+
+-- Skipped Function next_job
+
+-- Extras
+
+create or replace function next_job return integer is
+declare
+  cursor c1 is select JOB_ID from JOB for update skip locked;
+  result integer;
+begin
+  open c1;
+  fetch c1 into result;
+  select JOB_ID from JOB where ID = result for update;
+  return result;
+end;
+/
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresql_schemaoldpostgresdialectv40sql"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v40.sql (0 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v40.sql                                (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v40.sql        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -0,0 +1,1071 @@
</span><ins>+-- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+
+----
+-- Copyright (c) 2010-2014 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+
+-----------------
+-- Resource ID --
+-----------------
+
+create sequence RESOURCE_ID_SEQ;
+
+
+-------------------------
+-- Cluster Bookkeeping --
+-------------------------
+
+-- Information about a process connected to this database.
+
+-- Note that this must match the node info schema in twext.enterprise.queue.
+create table NODE_INFO (
+  HOSTNAME  varchar(255) not null,
+  PID       integer      not null,
+  PORT      integer      not null,
+  TIME      timestamp    not null default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (HOSTNAME, PORT)
+);
+
+-- Unique named locks.  This table should always be empty, but rows are
+-- temporarily created in order to prevent undesirable concurrency.
+create table NAMED_LOCK (
+    LOCK_NAME varchar(255) primary key
+);
+
+
+--------------------
+-- Jobs           --
+--------------------
+
+create sequence JOB_SEQ;
+
+create table JOB (
+  JOB_ID      integer primary key default nextval('JOB_SEQ') not null, --implicit index
+  WORK_TYPE   varchar(255) not null,
+  PRIORITY    integer default 0,
+  WEIGHT      integer default 0,
+  NOT_BEFORE  timestamp default null,
+  NOT_AFTER   timestamp default null
+);
+
+create or replace function next_job() returns integer as $$
+declare
+  result integer;
+begin
+  select JOB_ID into result from JOB where pg_try_advisory_xact_lock(JOB_ID) limit 1 for update;
+  return result;
+end
+$$ LANGUAGE plpgsql;
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null unique,                                -- implicit index
+  STATUS           integer      default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION      integer      default 0 not null
+);
+
+-- Enumeration of statuses
+
+create table HOME_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into HOME_STATUS values (0, 'normal' );
+insert into HOME_STATUS values (1, 'external');
+
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+
+----------------------------
+-- Calendar Home Metadata --
+----------------------------
+
+create table CALENDAR_HOME_METADATA (
+  RESOURCE_ID              integer     primary key references CALENDAR_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES         integer     default 0 not null,
+  DEFAULT_EVENTS           integer     default null references CALENDAR on delete set null,
+  DEFAULT_TASKS            integer     default null references CALENDAR on delete set null,
+  DEFAULT_POLLS            integer     default null references CALENDAR on delete set null,
+  ALARM_VEVENT_TIMED       text        default null,
+  ALARM_VEVENT_ALLDAY      text        default null,
+  ALARM_VTODO_TIMED        text        default null,
+  ALARM_VTODO_ALLDAY       text        default null,
+  AVAILABILITY             text        default null,
+  CREATED                  timestamp   default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                 timestamp   default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+create index CALENDAR_HOME_METADATA_DEFAULT_EVENTS on
+  CALENDAR_HOME_METADATA(DEFAULT_EVENTS);
+create index CALENDAR_HOME_METADATA_DEFAULT_TASKS on
+  CALENDAR_HOME_METADATA(DEFAULT_TASKS);
+create index CALENDAR_HOME_METADATA_DEFAULT_POLLS on
+  CALENDAR_HOME_METADATA(DEFAULT_POLLS);
+
+
+-----------------------
+-- Calendar Metadata --
+-----------------------
+
+create table CALENDAR_METADATA (
+  RESOURCE_ID           integer      primary key references CALENDAR on delete cascade, -- implicit index
+  SUPPORTED_COMPONENTS  varchar(255) default null,
+  CREATED               timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID   varchar(255) not null unique,                                -- implicit index
+  STATUS      integer      default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION integer      default 0 not null
+);
+
+create table NOTIFICATION (
+  RESOURCE_ID                   integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME,
+  NOTIFICATION_UID              varchar(255) not null,
+  NOTIFICATION_TYPE             varchar(255) not null,
+  NOTIFICATION_DATA             text         not null,
+  MD5                           char(32)     not null,
+  CREATED                       timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID) -- implicit index
+);
+
+create index NOTIFICATION_NOTIFICATION_HOME_RESOURCE_ID on
+  NOTIFICATION(NOTIFICATION_HOME_RESOURCE_ID);
+
+
+-------------------
+-- Calendar Bind --
+-------------------
+
+-- Joins CALENDAR_HOME and CALENDAR
+
+create table CALENDAR_BIND (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      not null references CALENDAR on delete cascade,
+  EXTERNAL_ID               integer      default null,
+  CALENDAR_RESOURCE_NAME    varchar(255) not null,
+  BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION             integer      default 0 not null,
+  MESSAGE                   text,
+  TRANSP                    integer      default 0 not null, -- enum CALENDAR_TRANSP
+  ALARM_VEVENT_TIMED        text         default null,
+  ALARM_VEVENT_ALLDAY       text         default null,
+  ALARM_VTODO_TIMED         text         default null,
+  ALARM_VTODO_ALLDAY        text         default null,
+  TIMEZONE                  text         default null,
+
+  primary key (CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID), -- implicit index
+  unique (CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)     -- implicit index
+);
+
+create index CALENDAR_BIND_RESOURCE_ID on
+  CALENDAR_BIND(CALENDAR_RESOURCE_ID);
+
+-- Enumeration of calendar bind modes
+
+create table CALENDAR_BIND_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_MODE values (0, 'own'  );
+insert into CALENDAR_BIND_MODE values (1, 'read' );
+insert into CALENDAR_BIND_MODE values (2, 'write');
+insert into CALENDAR_BIND_MODE values (3, 'direct');
+insert into CALENDAR_BIND_MODE values (4, 'indirect');
+
+-- Enumeration of statuses
+
+create table CALENDAR_BIND_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_STATUS values (0, 'invited' );
+insert into CALENDAR_BIND_STATUS values (1, 'accepted');
+insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
+insert into CALENDAR_BIND_STATUS values (4, 'deleted');
+
+
+-- Enumeration of transparency
+
+create table CALENDAR_TRANSP (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_TRANSP values (0, 'opaque' );
+insert into CALENDAR_TRANSP values (1, 'transparent');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+  RESOURCE_ID          integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID integer      not null references CALENDAR on delete cascade,
+  RESOURCE_NAME        varchar(255) not null,
+  ICALENDAR_TEXT       text         not null,
+  ICALENDAR_UID        varchar(255) not null,
+  ICALENDAR_TYPE       varchar(255) not null,
+  ATTACHMENTS_MODE     integer      default 0 not null, -- enum CALENDAR_OBJ_ATTACHMENTS_MODE
+  DROPBOX_ID           varchar(255),
+  ORGANIZER            varchar(255),
+  RECURRANCE_MIN       date,        -- minimum date that recurrences have been expanded to.
+  RECURRANCE_MAX       date,        -- maximum date that recurrences have been expanded to.
+  ACCESS               integer      default 0 not null,
+  SCHEDULE_OBJECT      boolean      default false,
+  SCHEDULE_TAG         varchar(36)  default null,
+  SCHEDULE_ETAGS       text         default null,
+  PRIVATE_COMMENTS     boolean      default false not null,
+  MD5                  char(32)     not null,
+  CREATED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED             timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (CALENDAR_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+
+  -- since the 'inbox' is a 'calendar resource' for the purpose of storing
+  -- calendar objects, this constraint has to be selectively enforced by the
+  -- application layer.
+
+  -- unique (CALENDAR_RESOURCE_ID, ICALENDAR_UID)
+);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_AND_ICALENDAR_UID on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_RECURRANCE_MAX on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, RECURRANCE_MAX);
+
+create index CALENDAR_OBJECT_ICALENDAR_UID on
+  CALENDAR_OBJECT(ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_DROPBOX_ID on
+  CALENDAR_OBJECT(DROPBOX_ID);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJ_ATTACHMENTS_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (0, 'none' );
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (1, 'read' );
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (2, 'write');
+
+
+-- Enumeration of calendar access types
+
+create table CALENDAR_ACCESS_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(32) not null unique
+);
+
+insert into CALENDAR_ACCESS_TYPE values (0, ''             );
+insert into CALENDAR_ACCESS_TYPE values (1, 'public'       );
+insert into CALENDAR_ACCESS_TYPE values (2, 'private'      );
+insert into CALENDAR_ACCESS_TYPE values (3, 'confidential' );
+insert into CALENDAR_ACCESS_TYPE values (4, 'restricted'   );
+
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+  INSTANCE_ID                 integer        primary key default nextval('INSTANCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID        integer        not null references CALENDAR on delete cascade,
+  CALENDAR_OBJECT_RESOURCE_ID integer        not null references CALENDAR_OBJECT on delete cascade,
+  FLOATING                    boolean        not null,
+  START_DATE                  timestamp      not null,
+  END_DATE                    timestamp      not null,
+  FBTYPE                      integer        not null,
+  TRANSPARENT                 boolean        not null
+);
+
+create index TIME_RANGE_CALENDAR_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_RESOURCE_ID);
+create index TIME_RANGE_CALENDAR_OBJECT_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_OBJECT_RESOURCE_ID);
+
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown'         );
+insert into FREE_BUSY_TYPE values (1, 'free'            );
+insert into FREE_BUSY_TYPE values (2, 'busy'            );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative'  );
+
+
+-------------------
+-- Per-user data --
+-------------------
+
+create table PERUSER (
+  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
+  USER_ID                     varchar(255) not null,
+  TRANSPARENT                 boolean      not null,
+  ADJUSTED_START_DATE         timestamp           default null,
+  ADJUSTED_END_DATE           timestamp    default null
+);
+
+create index PERUSER_TIME_RANGE_INSTANCE_ID on
+  PERUSER(TIME_RANGE_INSTANCE_ID);
+
+
+----------------
+-- Attachment --
+----------------
+
+create sequence ATTACHMENT_ID_SEQ;
+
+create table ATTACHMENT (
+  ATTACHMENT_ID               integer           primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
+  CALENDAR_HOME_RESOURCE_ID   integer           not null references CALENDAR_HOME,
+  DROPBOX_ID                  varchar(255),
+  CONTENT_TYPE                varchar(255)      not null,
+  SIZE                        integer           not null,
+  MD5                         char(32)          not null,
+  CREATED                     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                    timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  PATH                        varchar(1024)     not null
+);
+
+create index ATTACHMENT_CALENDAR_HOME_RESOURCE_ID on
+  ATTACHMENT(CALENDAR_HOME_RESOURCE_ID);
+
+create index ATTACHMENT_DROPBOX_ID on
+  ATTACHMENT(DROPBOX_ID);
+
+-- Many-to-many relationship between attachments and calendar objects
+create table ATTACHMENT_CALENDAR_OBJECT (
+  ATTACHMENT_ID                  integer      not null references ATTACHMENT on delete cascade,
+  MANAGED_ID                     varchar(255) not null,
+  CALENDAR_OBJECT_RESOURCE_ID    integer      not null references CALENDAR_OBJECT on delete cascade,
+
+  primary key (ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID), -- implicit index
+  unique (MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
+);
+
+create index ATTACHMENT_CALENDAR_OBJECT_CALENDAR_OBJECT_RESOURCE_ID on
+  ATTACHMENT_CALENDAR_OBJECT(CALENDAR_OBJECT_RESOURCE_ID);
+
+-----------------------
+-- Resource Property --
+-----------------------
+
+create table RESOURCE_PROPERTY (
+  RESOURCE_ID integer      not null, -- foreign key: *.RESOURCE_ID
+  NAME        varchar(255) not null,
+  VALUE       text         not null, -- FIXME: xml?
+  VIEWER_UID  varchar(255),
+
+  primary key (RESOURCE_ID, NAME, VIEWER_UID) -- implicit index
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+  RESOURCE_ID                   integer         primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  ADDRESSBOOK_PROPERTY_STORE_ID integer         default nextval('RESOURCE_ID_SEQ') not null,    -- implicit index
+  OWNER_UID                     varchar(255)    not null unique,                                -- implicit index
+  STATUS                        integer         default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION                   integer         default 0 not null
+);
+
+
+-------------------------------
+-- AddressBook Home Metadata --
+-------------------------------
+
+create table ADDRESSBOOK_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references ADDRESSBOOK_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+-----------------------------
+-- Shared AddressBook Bind --
+-----------------------------
+
+-- Joins sharee ADDRESSBOOK_HOME and owner ADDRESSBOOK_HOME
+
+create table SHARED_ADDRESSBOOK_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID          integer         not null references ADDRESSBOOK_HOME,
+  OWNER_HOME_RESOURCE_ID                integer         not null references ADDRESSBOOK_HOME on delete cascade,
+  EXTERNAL_ID                           integer         default null,
+  ADDRESSBOOK_RESOURCE_NAME             varchar(255)    not null,
+  BIND_MODE                             integer         not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                           integer         not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION                         integer         default 0 not null,
+  MESSAGE                               text,                     -- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)     -- implicit index
+);
+
+create index SHARED_ADDRESSBOOK_BIND_RESOURCE_ID on
+  SHARED_ADDRESSBOOK_BIND(OWNER_HOME_RESOURCE_ID);
+
+
+------------------------
+-- AddressBook Object --
+------------------------
+
+create table ADDRESSBOOK_OBJECT (
+  RESOURCE_ID                   integer         primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  ADDRESSBOOK_HOME_RESOURCE_ID  integer         not null references ADDRESSBOOK_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255)    not null,
+  VCARD_TEXT                    text            not null,
+  VCARD_UID                     varchar(255)    not null,
+  KIND                          integer         not null,  -- enum ADDRESSBOOK_OBJECT_KIND
+  MD5                           char(32)        not null,
+  CREATED                       timestamp       default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp       default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, RESOURCE_NAME), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, VCARD_UID)      -- implicit index
+);
+
+
+-----------------------------
+-- AddressBook Object kind --
+-----------------------------
+
+create table ADDRESSBOOK_OBJECT_KIND (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND values (0, 'person');
+insert into ADDRESSBOOK_OBJECT_KIND values (1, 'group' );
+insert into ADDRESSBOOK_OBJECT_KIND values (2, 'resource');
+insert into ADDRESSBOOK_OBJECT_KIND values (3, 'location');
+
+
+----------------------------------
+-- Revisions, forward reference --
+----------------------------------
+
+create sequence REVISION_SEQ;
+
+---------------------------------
+-- Address Book Object Members --
+---------------------------------
+
+create table ABO_MEMBERS (
+  GROUP_ID        integer     not null, -- references ADDRESSBOOK_OBJECT on delete cascade,   -- AddressBook Object's (kind=='group') RESOURCE_ID
+  ADDRESSBOOK_ID  integer     not null references ADDRESSBOOK_HOME on delete cascade,
+  MEMBER_ID       integer     not null, -- references ADDRESSBOOK_OBJECT,                     -- member AddressBook Object's RESOURCE_ID
+  REVISION        integer     default nextval('REVISION_SEQ') not null,
+  REMOVED         boolean     default false not null,
+  MODIFIED        timestamp   default timezone('UTC', CURRENT_TIMESTAMP),
+
+    primary key (GROUP_ID, MEMBER_ID, REVISION) -- implicit index
+);
+
+create index ABO_MEMBERS_ADDRESSBOOK_ID on
+  ABO_MEMBERS(ADDRESSBOOK_ID);
+create index ABO_MEMBERS_MEMBER_ID on
+  ABO_MEMBERS(MEMBER_ID);
+
+------------------------------------------
+-- Address Book Object Foreign Members  --
+------------------------------------------
+
+create table ABO_FOREIGN_MEMBERS (
+  GROUP_ID           integer      not null references ADDRESSBOOK_OBJECT on delete cascade,  -- AddressBook Object's (kind=='group') RESOURCE_ID
+  ADDRESSBOOK_ID     integer      not null references ADDRESSBOOK_HOME on delete cascade,
+  MEMBER_ADDRESS     varchar(255) not null,                                                  -- member AddressBook Object's 'calendar' address
+
+  primary key (GROUP_ID, MEMBER_ADDRESS) -- implicit index
+);
+
+create index ABO_FOREIGN_MEMBERS_ADDRESSBOOK_ID on
+  ABO_FOREIGN_MEMBERS(ADDRESSBOOK_ID);
+
+-----------------------
+-- Shared Group Bind --
+-----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK_OBJECT (kind == group)
+
+create table SHARED_GROUP_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID      integer      not null references ADDRESSBOOK_HOME,
+  GROUP_RESOURCE_ID                 integer      not null references ADDRESSBOOK_OBJECT on delete cascade,
+  EXTERNAL_ID                       integer      default null,
+  GROUP_ADDRESSBOOK_NAME            varchar(255) not null,
+  BIND_MODE                         integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                       integer      not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION                     integer      default 0 not null,
+  MESSAGE                           text,                  -- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_ADDRESSBOOK_NAME)  -- implicit index
+);
+
+create index SHARED_GROUP_BIND_RESOURCE_ID on
+  SHARED_GROUP_BIND(GROUP_RESOURCE_ID);
+
+
+---------------
+-- Revisions --
+---------------
+
+-- create sequence REVISION_SEQ;
+
+
+-------------------------------
+-- Calendar Object Revisions --
+-------------------------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      references CALENDAR,
+  CALENDAR_NAME             varchar(255) default null,
+  RESOURCE_NAME             varchar(255),
+  REVISION                  integer      default nextval('REVISION_SEQ') not null,
+  DELETED                   boolean      not null,
+  MODIFIED                  timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+create index CALENDAR_OBJECT_REVISIONS_HOME_RESOURCE_ID_CALENDAR_RESOURCE_ID
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, REVISION);
+
+
+----------------------------------
+-- AddressBook Object Revisions --
+----------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+  ADDRESSBOOK_HOME_RESOURCE_ID  integer      not null references ADDRESSBOOK_HOME,
+  OWNER_HOME_RESOURCE_ID        integer      references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_NAME              varchar(255) default null,
+  OBJECT_RESOURCE_ID            integer      default 0,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_HOME_RESOURCE_ID_OWNER_HOME_RESOURCE_ID
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, REVISION);
+
+
+-----------------------------------
+-- Notification Object Revisions --
+-----------------------------------
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (NOTIFICATION_HOME_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+);
+
+create index NOTIFICATION_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on NOTIFICATION_OBJECT_REVISIONS(NOTIFICATION_HOME_RESOURCE_ID, REVISION);
+
+
+-------------------------------------------
+-- Apple Push Notification Subscriptions --
+-------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer      not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  USER_AGENT                    varchar(255) default null,
+  IP_ADDR                       varchar(255) default null,
+
+  primary key (TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+  on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+
+-----------------
+-- IMIP Tokens --
+-----------------
+
+create table IMIP_TOKENS (
+  TOKEN                         varchar(255) not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALUID                       varchar(255) not null,
+  ACCESSED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (ORGANIZER, ATTENDEE, ICALUID) -- implicit index
+);
+
+create index IMIP_TOKENS_TOKEN
+  on IMIP_TOKENS(TOKEN);
+
+
+----------------
+-- Work Items --
+----------------
+
+create sequence WORKITEM_SEQ;
+
+
+---------------------------
+-- IMIP Inivitation Work --
+---------------------------
+
+create table IMIP_INVITATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  FROM_ADDR                     varchar(255) not null,
+  TO_ADDR                       varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+create index IMIP_INVITATION_WORK_JOB_ID on
+  IMIP_INVITATION_WORK(JOB_ID);
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+create table IMIP_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index IMIP_POLLING_WORK_JOB_ID on
+  IMIP_POLLING_WORK(JOB_ID);
+
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+create index IMIP_REPLY_WORK_JOB_ID on
+  IMIP_REPLY_WORK(JOB_ID);
+
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  PUSH_ID                       varchar(255) not null,
+  PUSH_PRIORITY                 integer      not null -- 1:low 5:medium 10:high
+);
+
+create index PUSH_NOTIFICATION_WORK_JOB_ID on
+  PUSH_NOTIFICATION_WORK(JOB_ID);
+
+-----------------
+-- GroupCacher --
+-----------------
+
+create table GROUP_CACHER_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index GROUP_CACHER_POLLING_WORK_JOB_ID on
+  GROUP_CACHER_POLLING_WORK(JOB_ID);
+
+create table GROUP_REFRESH_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  GROUP_UID                     varchar(255) not null
+);
+
+create index GROUP_REFRESH_WORK_JOB_ID on
+  GROUP_REFRESH_WORK(JOB_ID);
+
+create table GROUP_ATTENDEE_RECONCILE_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  RESOURCE_ID                   integer,
+  GROUP_ID                      integer
+);
+
+create index GROUP_ATTENDEE_RECONCILE_WORK_JOB_ID on
+  GROUP_ATTENDEE_RECONCILE_WORK(JOB_ID);
+
+
+create table GROUPS (
+  GROUP_ID                      integer      primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  NAME                          varchar(255) not null,
+  GROUP_UID                     varchar(255) not null,
+  MEMBERSHIP_HASH               varchar(255) not null,
+  EXTANT                        integer default 1,
+  CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+create index GROUPS_GROUP_UID on
+  GROUPS(GROUP_UID);
+
+create table GROUP_MEMBERSHIP (
+  GROUP_ID                     integer not null references GROUPS on delete cascade,
+  MEMBER_UID                   varchar(255) not null,
+  
+  primary key (GROUP_ID, MEMBER_UID)
+);
+
+create index GROUP_MEMBERSHIP_MEMBER on
+  GROUP_MEMBERSHIP(MEMBER_UID);
+
+create table GROUP_ATTENDEE (
+  GROUP_ID                      integer not null references GROUPS on delete cascade,
+  RESOURCE_ID                   integer not null references CALENDAR_OBJECT on delete cascade,
+  MEMBERSHIP_HASH               varchar(255) not null,
+  
+  primary key (GROUP_ID, RESOURCE_ID)
+);
+create index GROUP_ATTENDEE_RESOURCE_ID on
+  GROUP_ATTENDEE(RESOURCE_ID);
+
+---------------
+-- Delegates --
+---------------
+
+create table DELEGATES (
+  DELEGATOR                     varchar(255) not null,
+  DELEGATE                      varchar(255) not null,
+  READ_WRITE                    integer      not null, -- 1 = ReadWrite, 0 = ReadOnly
+
+  primary key (DELEGATOR, READ_WRITE, DELEGATE)
+);
+create index DELEGATE_TO_DELEGATOR on
+  DELEGATES(DELEGATE, READ_WRITE, DELEGATOR);
+
+create table DELEGATE_GROUPS (
+  DELEGATOR                     varchar(255) not null,
+  GROUP_ID                      integer      not null references GROUPS on delete cascade,
+  READ_WRITE                    integer      not null, -- 1 = ReadWrite, 0 = ReadOnly
+  IS_EXTERNAL                   integer      not null, -- 1 = ReadWrite, 0 = ReadOnly
+
+  primary key (DELEGATOR, READ_WRITE, GROUP_ID)
+);
+create index DELEGATE_GROUPS_GROUP_ID on
+  DELEGATE_GROUPS(GROUP_ID);
+
+create table EXTERNAL_DELEGATE_GROUPS (
+  DELEGATOR                     varchar(255) primary key not null,
+  GROUP_UID_READ                varchar(255),
+  GROUP_UID_WRITE               varchar(255)
+);
+
+--------------------------
+-- Object Splitter Work --
+--------------------------
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade
+);
+
+create index CALENDAR_OBJECT_SPLITTER_WORK_RESOURCE_ID on
+  CALENDAR_OBJECT_SPLITTER_WORK(RESOURCE_ID);
+create index CALENDAR_OBJECT_SPLITTER_WORK_JOB_ID on
+  CALENDAR_OBJECT_SPLITTER_WORK(JOB_ID);
+
+---------------------------
+-- Revision Cleanup Work --
+---------------------------
+
+create table FIND_MIN_VALID_REVISION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index FIND_MIN_VALID_REVISION_WORK_JOB_ID on
+  FIND_MIN_VALID_REVISION_WORK(JOB_ID);
+
+create table REVISION_CLEANUP_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index REVISION_CLEANUP_WORK_JOB_ID on
+  REVISION_CLEANUP_WORK(JOB_ID);
+
+------------------------
+-- Inbox Cleanup Work --
+------------------------
+
+create table INBOX_CLEANUP_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index INBOX_CLEANUP_WORK_JOB_ID on
+   INBOX_CLEANUP_WORK(JOB_ID);
+
+create table CLEANUP_ONE_INBOX_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_ID                       integer      not null unique references CALENDAR_HOME on delete cascade
+);
+
+create index CLEANUP_ONE_INBOX_WORK_JOB_ID on
+  CLEANUP_ONE_INBOX_WORK(JOB_ID);
+
+---------------------------
+-- Schedule Refresh Work --
+---------------------------
+
+create table SCHEDULE_REFRESH_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ICALENDAR_UID                 varchar(255) not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  ATTENDEE_COUNT                integer
+);
+
+create index SCHEDULE_REFRESH_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_REFRESH_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_REFRESH_WORK_RESOURCE_ID on
+  SCHEDULE_REFRESH_WORK(RESOURCE_ID);
+create index SCHEDULE_REFRESH_WORK_JOB_ID on
+  SCHEDULE_REFRESH_WORK(JOB_ID);
+
+create table SCHEDULE_REFRESH_ATTENDEES (
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  ATTENDEE                      varchar(255) not null,
+  
+  primary key (RESOURCE_ID, ATTENDEE)
+);
+
+create index SCHEDULE_REFRESH_ATTENDEES_RESOURCE_ID_ATTENDEE on
+  SCHEDULE_REFRESH_ATTENDEES(RESOURCE_ID, ATTENDEE);
+
+------------------------------
+-- Schedule Auto Reply Work --
+------------------------------
+
+create table SCHEDULE_AUTO_REPLY_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ICALENDAR_UID                 varchar(255) not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  PARTSTAT                      varchar(255) not null
+);
+
+create index SCHEDULE_AUTO_REPLY_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_AUTO_REPLY_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_AUTO_REPLY_WORK_RESOURCE_ID on
+  SCHEDULE_AUTO_REPLY_WORK(RESOURCE_ID);
+create index SCHEDULE_AUTO_REPLY_WORK_JOB_ID on
+  SCHEDULE_AUTO_REPLY_WORK(JOB_ID);
+
+-----------------------------
+-- Schedule Organizer Work --
+-----------------------------
+
+create table SCHEDULE_ORGANIZER_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ICALENDAR_UID                 varchar(255) not null,
+  SCHEDULE_ACTION               integer      not null, -- Enum SCHEDULE_ACTION
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer,     -- this references a possibly non-existent CALENDR_OBJECT
+  ICALENDAR_TEXT_OLD            text,
+  ICALENDAR_TEXT_NEW            text,
+  ATTENDEE_COUNT                integer,
+  SMART_MERGE                   boolean
+);
+
+create index SCHEDULE_ORGANIZER_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_ORGANIZER_WORK_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_WORK(RESOURCE_ID);
+create index SCHEDULE_ORGANIZER_WORK_JOB_ID on
+  SCHEDULE_ORGANIZER_WORK(JOB_ID);
+
+-- Enumeration of schedule actions
+
+create table SCHEDULE_ACTION (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into SCHEDULE_ACTION values (0, 'create');
+insert into SCHEDULE_ACTION values (1, 'modify');
+insert into SCHEDULE_ACTION values (2, 'modify-cancelled');
+insert into SCHEDULE_ACTION values (3, 'remove');
+
+-------------------------
+-- Schedule Reply Work --
+-------------------------
+
+create table SCHEDULE_REPLY_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ICALENDAR_UID                 varchar(255) not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  CHANGED_RIDS                  text
+);
+
+create index SCHEDULE_REPLY_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_REPLY_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_REPLY_WORK_RESOURCE_ID on
+  SCHEDULE_REPLY_WORK(RESOURCE_ID);
+create index SCHEDULE_REPLY_WORK_JOB_ID on
+  SCHEDULE_REPLY_WORK(JOB_ID);
+
+--------------------------------
+-- Schedule Reply Cancel Work --
+--------------------------------
+
+create table SCHEDULE_REPLY_CANCEL_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ICALENDAR_UID                 varchar(255) not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  ICALENDAR_TEXT                text         not null
+);
+
+create index SCHEDULE_REPLY_CANCEL_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_REPLY_CANCEL_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_REPLY_CANCEL_WORK_JOB_ID on
+  SCHEDULE_REPLY_CANCEL_WORK(JOB_ID);
+
+----------------------------------
+-- Principal Purge Polling Work --
+----------------------------------
+
+create table PRINCIPAL_PURGE_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index PRINCIPAL_PURGE_POLLING_WORK_JOB_ID on
+  PRINCIPAL_PURGE_POLLING_WORK(JOB_ID);
+
+--------------------------------
+-- Principal Purge Check Work --
+--------------------------------
+
+create table PRINCIPAL_PURGE_CHECK_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  UID                           varchar(255) not null
+);
+
+create index PRINCIPAL_PURGE_CHECK_WORK_JOB_ID on
+  PRINCIPAL_PURGE_CHECK_WORK(JOB_ID);
+
+--------------------------
+-- Principal Purge Work --
+--------------------------
+
+create table PRINCIPAL_PURGE_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  UID                           varchar(255) not null
+);
+
+create index PRINCIPAL_PURGE_WORK_JOB_ID on
+  PRINCIPAL_PURGE_WORK(JOB_ID);
+
+
+--------------------
+-- Schema Version --
+--------------------
+
+create table CALENDARSERVER (
+  NAME                          varchar(255) primary key, -- implicit index
+  VALUE                         varchar(255)
+);
+
+insert into CALENDARSERVER values ('VERSION', '40');
+insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '6');
+insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');
+insert into CALENDARSERVER values ('MIN-VALID-REVISION', '1');
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresql_schemaupgradesoracledialectupgrade_from_40_to_41sql"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_40_to_41.sql (0 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_40_to_41.sql                                (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_40_to_41.sql        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -0,0 +1,41 @@
</span><ins>+----
+-- Copyright (c) 2012-2014 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 40 to 41 --
+---------------------------------------------------
+
+insert into HOME_STATUS (DESCRIPTION, ID) values ('purging', 2);
+
+--------------------------------
+-- Principal Home Remove Work --
+--------------------------------
+
+create table PRINCIPAL_PURGE_HOME_WORK (
+    &quot;WORK_ID&quot; integer primary key not null,
+    &quot;JOB_ID&quot; integer not null references JOB,
+    &quot;HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME on delete cascade
+);
+
+create index PRINCIPAL_PURGE_HOME__f35eea7a on PRINCIPAL_PURGE_HOME_WORK (
+    JOB_ID
+);
+create index PRINCIPAL_PURGE_HOME__967e4480 on PRINCIPAL_PURGE_HOME_WORK (
+    HOME_RESOURCE_ID
+);
+
+-- update the version
+update CALENDARSERVER set VALUE = '41' where NAME = 'VERSION';
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresql_schemaupgradespostgresdialectupgrade_from_40_to_41sql"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_40_to_41.sql (0 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_40_to_41.sql                                (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_40_to_41.sql        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -0,0 +1,39 @@
</span><ins>+----
+-- Copyright (c) 2012-2014 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 40 to 41 --
+---------------------------------------------------
+
+insert into HOME_STATUS values (2, 'purging');
+
+--------------------------------
+-- Principal Home Remove Work --
+--------------------------------
+
+create table PRINCIPAL_PURGE_HOME_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade
+);
+
+create index PRINCIPAL_PURGE_HOME_WORK_JOB_ID on
+  PRINCIPAL_PURGE_HOME_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_HOME_HOME_RESOURCE_ID on
+  PRINCIPAL_PURGE_HOME_WORK(HOME_RESOURCE_ID);
+
+-- update the version
+update CALENDARSERVER set VALUE = '41' where NAME = 'VERSION';
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresql_tablespy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/sql_tables.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql_tables.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/common/datastore/sql_tables.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -186,6 +186,7 @@
</span><span class="cx"> 
</span><span class="cx"> _HOME_STATUS_NORMAL = _homeStatus('normal')
</span><span class="cx"> _HOME_STATUS_EXTERNAL = _homeStatus('external')
</span><ins>+_HOME_STATUS_PURGING = _homeStatus('purging')
</ins><span class="cx"> 
</span><span class="cx"> _bindStatus = _schemaConstants(
</span><span class="cx">     schema.CALENDAR_BIND_STATUS.DESCRIPTION,
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoretestutilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/test/util.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/test/util.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/common/datastore/test/util.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -122,11 +122,24 @@
</span><span class="cx">         return succeed(None)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def recordWithCalendarUserAddress(self, cuaddr):
+        if cuaddr.startswith(&quot;urn:x-uid:&quot;):
+            return self.recordWithUID(cuaddr[10:])
+        elif cuaddr.startswith(&quot;urn:uuid:&quot;):
+            return self.recordWithUID(cuaddr[9:])
+        else:
+            return succeed(None)
+
+
</ins><span class="cx">     def addRecord(self, record):
</span><span class="cx">         self.records[record.uid] = record
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def removeRecord(self, uid):
+        del self.records[uid]
</ins><span class="cx"> 
</span><ins>+
+
</ins><span class="cx"> class TestStoreDirectoryRecord(object):
</span><span class="cx"> 
</span><span class="cx">     implements(IStoreDirectoryRecord)
</span><span class="lines">@@ -151,7 +164,7 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def serverURI(self):
</span><del>-        return self._server.uri
</del><ins>+        return self._server.uri if self._server else &quot;&quot;
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavwhodirectorypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/who/directory.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/directory.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/who/directory.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -30,6 +30,7 @@
</span><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.ical import Property
</span><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
</span><ins>+from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr
</ins><span class="cx"> from txdav.who.delegates import RecordType as DelegateRecordType
</span><span class="cx"> from txdav.who.idirectory import (
</span><span class="cx">     RecordType as DAVRecordType, AutoScheduleMode
</span><span class="lines">@@ -64,8 +65,6 @@
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def recordWithCalendarUserAddress(self, address):
</span><del>-        # FIXME: moved this here to avoid circular import problems
-        from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
</del><span class="cx">         address = normalizeCUAddr(address)
</span><span class="cx">         record = None
</span><span class="cx"> 
</span><span class="lines">@@ -430,7 +429,7 @@
</span><span class="cx">         if config.Scheduling.Options.AutoSchedule.Enabled:
</span><span class="cx">             if (
</span><span class="cx">                 config.Scheduling.Options.AutoSchedule.Always or
</span><del>-                self.autoScheduleMode not in (AutoScheduleMode.none, None) or  # right???
</del><ins>+                getattr(self, &quot;autoScheduleMode&quot;, None) not in (AutoScheduleMode.none, None) or  # right???
</ins><span class="cx">                 (
</span><span class="cx">                     yield self.autoAcceptFromOrganizer(organizer)
</span><span class="cx">                 )
</span></span></pre></div>
<a id="CalendarServertrunktxdavwhoutilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/who/util.py (13407 => 13408)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/util.py        2014-05-02 18:42:58 UTC (rev 13407)
+++ CalendarServer/trunk/txdav/who/util.py        2014-05-02 20:07:26 UTC (rev 13408)
</span><span class="lines">@@ -33,10 +33,10 @@
</span><span class="cx"> )
</span><span class="cx"> from twext.who.util import ConstantsContainer
</span><span class="cx"> from twisted.cred.credentials import UsernamePassword
</span><ins>+from twisted.internet.defer import succeed, inlineCallbacks
</ins><span class="cx"> from twisted.python.filepath import FilePath
</span><span class="cx"> from twisted.python.reflect import namedClass
</span><span class="cx"> from twistedcaldav.config import fullServerPath
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
</del><span class="cx"> from txdav.who.augment import AugmentedDirectoryService
</span><span class="cx"> from txdav.who.delegates import DirectoryService as DelegateDirectoryService
</span><span class="cx"> from txdav.who.idirectory import (
</span><span class="lines">@@ -45,8 +45,6 @@
</span><span class="cx"> )
</span><span class="cx"> from txdav.who.wiki import DirectoryService as WikiDirectoryService
</span><span class="cx"> from txdav.who.xml import DirectoryService as XMLDirectoryService
</span><del>-from uuid import UUID
-from twisted.internet.defer import succeed, inlineCallbacks
</del><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="lines">@@ -248,38 +246,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-def uidFromCalendarUserAddress(address):
-    &quot;&quot;&quot;
-        Get a uid from a calendar user address
-        May return None
-    &quot;&quot;&quot;
-
-    address = normalizeCUAddr(address)
-
-    if address.startswith(&quot;urn:x-uid:&quot;):
-        return address[10:]
-
-    elif address.startswith(&quot;urn:uuid:&quot;):
-        try:
-            UUID(address[9:])
-        except ValueError:
-            log.info(&quot;Invalid GUID: {guid}&quot;, guid=address[9:])
-        else:
-            return address[9:]
-
-    elif address.startswith(&quot;mailto:&quot;):
-        return address[7:]
-
-    elif address.startswith(&quot;/principals/&quot;):
-        parts = address.split(&quot;/&quot;)
-        if len(parts) == 4:
-            return parts[3]
-
-    return None
-
-
-
-
</del><span class="cx"> class InMemoryDirectoryService(IndexDirectoryService):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     An in-memory IDirectoryService.  You must call updateRecords( ) if you want
</span><span class="lines">@@ -306,7 +272,7 @@
</span><span class="cx">         recordsByUID = dict(((record.uid, record) for record in records))
</span><span class="cx">         if not create:
</span><span class="cx">             # Make sure all the records already exist
</span><del>-            for uid, record in recordsByUID.items():
</del><ins>+            for uid, _ignore_record in recordsByUID.items():
</ins><span class="cx">                 if uid not in self._index[self.fieldName.uid]:
</span><span class="cx">                     raise NoSuchRecordError(uid)
</span><span class="cx"> 
</span></span></pre>
</div>
</div>

</body>
</html>