[CalendarServer-changes] [5833] CalendarServer/branches/users/wsanchez/deployment

source_changes at macosforge.org source_changes at macosforge.org
Thu Jul 1 15:12:06 PDT 2010


Revision: 5833
          http://trac.macosforge.org/projects/calendarserver/changeset/5833
Author:   cdaboo at apple.com
Date:     2010-07-01 15:12:06 -0700 (Thu, 01 Jul 2010)
Log Message:
-----------
Merge partitioning into deployment.

Modified Paths:
--------------
    CalendarServer/branches/users/wsanchez/deployment/conf/accounts-test.xml
    CalendarServer/branches/users/wsanchez/deployment/conf/accounts.dtd
    CalendarServer/branches/users/wsanchez/deployment/conf/accounts.xml
    CalendarServer/branches/users/wsanchez/deployment/conf/caldavd-test.plist
    CalendarServer/branches/users/wsanchez/deployment/conf/caldavd.plist
    CalendarServer/branches/users/wsanchez/deployment/memcacheclient.py
    CalendarServer/branches/users/wsanchez/deployment/run
    CalendarServer/branches/users/wsanchez/deployment/support/patchapply
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/cache.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/cluster.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/config.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/customxml.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/idirectory.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/sudo.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/accounts.xml
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_aggregate.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_calendar.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_digest.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_guidchange.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectory.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_principal.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipaldb.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipalmembers.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_util.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_xmlfile.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/util.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlaccountsparser.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlfile.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/extensions.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/ical.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/index.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/log.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcachepool.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcacheprops.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcacher.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/report_common.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/notify.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/root.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/schedule.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/static.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/tap.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_cache.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_config.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_log.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcache.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcachepool.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcacher.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_resource.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_root.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_static.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_tap.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/util.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/util.py

Added Paths:
-----------
    CalendarServer/branches/users/wsanchez/deployment/bin/caldav_load_augmentdb
    CalendarServer/branches/users/wsanchez/deployment/bin/caldav_manage_augments
    CalendarServer/branches/users/wsanchez/deployment/bin/calendarserver_manage_principals
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/loadaugmentdb.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/manageaugments.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/managepostgres.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/principals.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py
    CalendarServer/branches/users/wsanchez/deployment/conf/augments-test.xml
    CalendarServer/branches/users/wsanchez/deployment/conf/augments.dtd
    CalendarServer/branches/users/wsanchez/deployment/conf/partitions-test.plist
    CalendarServer/branches/users/wsanchez/deployment/conf/partitions.plist
    CalendarServer/branches/users/wsanchez/deployment/conf/proxies-test.xml
    CalendarServer/branches/users/wsanchez/deployment/conf/proxies.dtd
    CalendarServer/branches/users/wsanchez/deployment/conf/servertoserver-test.xml
    CalendarServer/branches/users/wsanchez/deployment/conf/servertoserver.dtd
    CalendarServer/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.client.http.patch
    CalendarServer/branches/users/wsanchez/deployment/twext/
    CalendarServer/branches/users/wsanchez/deployment/twext/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twext/python/
    CalendarServer/branches/users/wsanchez/deployment/twext/python/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twext/python/log.py
    CalendarServer/branches/users/wsanchez/deployment/twext/web2/
    CalendarServer/branches/users/wsanchez/deployment/twext/web2/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/
    CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/davxml.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/pool.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/reverseproxy.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/database.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/augment.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxyloader.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments-test-default.xml
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments-test.xml
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments.xml
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/proxies.xml
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_augment.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlaugmentsparser.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/partitions.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/addressmapping.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/cuaddress.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/delivery.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/itip.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/test/
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/test/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/utils.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_database.py

Removed Paths:
-------------
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/loadaugmentdb.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/manageaugments.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/managepostgres.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/principals.py
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py
    CalendarServer/branches/users/wsanchez/deployment/twext/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twext/python/
    CalendarServer/branches/users/wsanchez/deployment/twext/python/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twext/python/log.py
    CalendarServer/branches/users/wsanchez/deployment/twext/web2/
    CalendarServer/branches/users/wsanchez/deployment/twext/web2/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/
    CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/davxml.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/pool.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/reverseproxy.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/apache.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/sqldb.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/basic
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/digest
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/groups
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_apache.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryrecords.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryschema.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_sqldb.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/itip.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/addressmapping.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/cuaddress.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/delivery.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/itip.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/test/
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/test/__init__.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/utils.py

Property Changed:
----------------
    CalendarServer/branches/users/wsanchez/deployment/


Property changes on: CalendarServer/branches/users/wsanchez/deployment
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/users/glyph/deployment-plus-sendfd:5426-5429
/CalendarServer/branches/users/sagen/deployment-inherit-fds-4571:4573-4709
/CalendarServer/branches/users/sagen/deployment-inspection:4927-4937
   + /CalendarServer/branches/users/cdaboo/deployment-partition-4524:4525-4594
/CalendarServer/branches/users/cdaboo/deployment-partition-4593:4594-4723
/CalendarServer/branches/users/cdaboo/deployment-partition-4722:4723-5830
/CalendarServer/branches/users/glyph/deployment-plus-sendfd:5426-5429
/CalendarServer/branches/users/sagen/deployment-inherit-fds-4571:4573-4709
/CalendarServer/branches/users/sagen/deployment-inspection:4927-4937
/CalendarServer/trunk:3189,3861,3941

Copied: CalendarServer/branches/users/wsanchez/deployment/bin/caldav_load_augmentdb (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/bin/caldav_load_augmentdb)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/bin/caldav_load_augmentdb	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/bin/caldav_load_augmentdb	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+    from calendarserver.tools.loadaugmentdb import main
+    main()

Copied: CalendarServer/branches/users/wsanchez/deployment/bin/caldav_manage_augments (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/bin/caldav_manage_augments)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/bin/caldav_manage_augments	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/bin/caldav_manage_augments	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+    from calendarserver.tools.manageaugments import main
+    main()

Copied: CalendarServer/branches/users/wsanchez/deployment/bin/calendarserver_manage_principals (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/bin/calendarserver_manage_principals)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/bin/calendarserver_manage_principals	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/bin/calendarserver_manage_principals	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from __future__ import with_statement
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+    from calendarserver.tools.principals import main
+    main()

Deleted: CalendarServer/branches/users/wsanchez/deployment/calendarserver/__init__.py
===================================================================
Copied: CalendarServer/branches/users/wsanchez/deployment/calendarserver/__init__.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/__init__.py)
===================================================================
Deleted: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/__init__.py
===================================================================
Copied: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/__init__.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/__init__.py)
===================================================================
Deleted: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/loadaugmentdb.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/loadaugmentdb.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/loadaugmentdb.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,185 +0,0 @@
-#!/usr/bin/env python
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from calendarserver.tools.util import loadConfig, getDirectory,\
-    autoDisableMemcached
-from getopt import getopt, GetoptError
-from grp import getgrnam
-from pwd import getpwnam
-from sys import stdout, stderr
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks
-from twisted.python.log import addObserver, removeObserver
-from twisted.python.util import switchUID
-from twistedcaldav.config import config, ConfigurationError
-from twistedcaldav.directory import augment
-from twistedcaldav.directory.augment import AugmentXMLDB
-from twistedcaldav.log import setLogLevelForNamespace
-import os
-import sys
-
-class UsageError (StandardError):
-    pass
-
-class StandardIOObserver (object):
-    """
-    Log observer that writes to standard I/O.
-    """
-    def emit(self, eventDict):
-        text = None
-
-        if eventDict["isError"]:
-            output = stderr
-            if "failure" in eventDict:
-                text = eventDict["failure"].getTraceback()
-        else:
-            output = stdout
-
-        if not text:
-            text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
-
-        output.write(text)
-        output.flush()
-
-    def start(self):
-        addObserver(self.emit)
-
-    def stop(self):
-        removeObserver(self.emit)
-
-def usage(e=None):
-    if e:
-        print e
-        print ""
-
-    name = os.path.basename(sys.argv[0])
-    print "usage: %s [options]" % (name,)
-    print ""
-    print "Populate an sqlite or PostgreSQL augments database with values"
-    print "from an XML augments file."
-    print ""
-    print "options:"
-    print "  -h --help: print this help and exit"
-    print "  -f --config: Specify caldavd.plist configuration path"
-    print "  -x --xmlfile: Specify xml augments file path"
-    print "  -r --remove: Remove all entries from the database"
-
-    if e:
-        sys.exit(64)
-    else:
-        sys.exit(0)
-
-def main():
-    try:
-        (optargs, args) = getopt(
-            sys.argv[1:], "hf:rx:", [
-                "config=",
-                "remove",
-                "xmlfile=",
-                "help",
-            ],
-        )
-    except GetoptError, e:
-        usage(e)
-
-    configFileName = None
-    xmlFileName = None
-    remove = False
-
-    for opt, arg in optargs:
-        if opt in ("-h", "--help"):
-            usage()
-
-        elif opt in ("-f", "--config"):
-            configFileName = arg
-
-        elif opt in ("-r", "--remove"):
-            remove = True
-            if raw_input("Do you really want to remove all records from the database? [y/n] ") != "y":
-                sys.exit(0)
-
-        elif opt in ("-x", "--xmlfile"):
-            xmlFileName = arg
-
-    if args:
-        usage("Too many arguments: %s" % (" ".join(args),))
-
-    observer = StandardIOObserver()
-    observer.start()
-
-    #
-    # Get configuration
-    #
-    try:
-        loadConfig(configFileName)
-        setLogLevelForNamespace(None, "warn")
-
-        # Shed privileges
-        if config.UserName and config.GroupName and os.getuid() == 0:
-            uid = getpwnam(config.UserName).pw_uid
-            gid = getgrnam(config.GroupName).gr_gid
-            switchUID(uid, uid, gid)
-
-        os.umask(config.umask)
-
-        config.directory = getDirectory()
-        autoDisableMemcached(config)
-    except ConfigurationError, e:
-        usage("Unable to start: %s" % (e,))
-
-    try:
-        dbxml = AugmentXMLDB((xmlFileName,)) if not remove else None
-    except IOError, e:
-        usage("Could not read XML augment file: %s" % (e,))
-
-    #
-    # Start the reactor
-    #
-    reactor.callLater(0, run, dbxml)
-    reactor.run()
-
- at inlineCallbacks
-def run(dbxml):
-    
-    try:
-        guids = set((yield augment.AugmentService.getAllGUIDs()))
-        added = 0
-        updated = 0
-        removed = 0
-        if dbxml:
-            for record in dbxml.db.values():
-                yield augment.AugmentService.addAugmentRecord(record, record.guid in guids)
-                if record.guid in guids:
-                    updated += 1
-                else:
-                    added += 1
-            for guid in guids.difference(dbxml.db.keys()):
-                yield augment.AugmentService.removeAugmentRecord(guid)
-                removed += 1
-        else:
-            yield augment.AugmentService.clean()
-            removed = len(guids)
-            
-        print "Changes:"
-        print "  Added: %d" % (added,)
-        print "  Changed: %d" % (updated,)
-        print "  Removed: %d" % (removed,)
-    finally:
-        #
-        # Stop the reactor
-        #
-        reactor.stop()

Copied: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/loadaugmentdb.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/loadaugmentdb.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/loadaugmentdb.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/loadaugmentdb.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,185 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from calendarserver.tools.util import loadConfig, getDirectory,\
+    autoDisableMemcached
+from getopt import getopt, GetoptError
+from grp import getgrnam
+from pwd import getpwnam
+from sys import stdout, stderr
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.log import addObserver, removeObserver
+from twisted.python.util import switchUID
+from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.directory import augment
+from twistedcaldav.directory.augment import AugmentXMLDB
+from twistedcaldav.log import setLogLevelForNamespace
+import os
+import sys
+
+class UsageError (StandardError):
+    pass
+
+class StandardIOObserver (object):
+    """
+    Log observer that writes to standard I/O.
+    """
+    def emit(self, eventDict):
+        text = None
+
+        if eventDict["isError"]:
+            output = stderr
+            if "failure" in eventDict:
+                text = eventDict["failure"].getTraceback()
+        else:
+            output = stdout
+
+        if not text:
+            text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
+
+        output.write(text)
+        output.flush()
+
+    def start(self):
+        addObserver(self.emit)
+
+    def stop(self):
+        removeObserver(self.emit)
+
+def usage(e=None):
+    if e:
+        print e
+        print ""
+
+    name = os.path.basename(sys.argv[0])
+    print "usage: %s [options]" % (name,)
+    print ""
+    print "Populate an sqlite or PostgreSQL augments database with values"
+    print "from an XML augments file."
+    print ""
+    print "options:"
+    print "  -h --help: print this help and exit"
+    print "  -f --config: Specify caldavd.plist configuration path"
+    print "  -x --xmlfile: Specify xml augments file path"
+    print "  -r --remove: Remove all entries from the database"
+
+    if e:
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
+def main():
+    try:
+        (optargs, args) = getopt(
+            sys.argv[1:], "hf:rx:", [
+                "config=",
+                "remove",
+                "xmlfile=",
+                "help",
+            ],
+        )
+    except GetoptError, e:
+        usage(e)
+
+    configFileName = None
+    xmlFileName = None
+    remove = False
+
+    for opt, arg in optargs:
+        if opt in ("-h", "--help"):
+            usage()
+
+        elif opt in ("-f", "--config"):
+            configFileName = arg
+
+        elif opt in ("-r", "--remove"):
+            remove = True
+            if raw_input("Do you really want to remove all records from the database? [y/n] ") != "y":
+                sys.exit(0)
+
+        elif opt in ("-x", "--xmlfile"):
+            xmlFileName = arg
+
+    if args:
+        usage("Too many arguments: %s" % (" ".join(args),))
+
+    observer = StandardIOObserver()
+    observer.start()
+
+    #
+    # Get configuration
+    #
+    try:
+        loadConfig(configFileName)
+        setLogLevelForNamespace(None, "warn")
+
+        # Shed privileges
+        if config.UserName and config.GroupName and os.getuid() == 0:
+            uid = getpwnam(config.UserName).pw_uid
+            gid = getgrnam(config.GroupName).gr_gid
+            switchUID(uid, uid, gid)
+
+        os.umask(config.umask)
+
+        config.directory = getDirectory()
+        autoDisableMemcached(config)
+    except ConfigurationError, e:
+        usage("Unable to start: %s" % (e,))
+
+    try:
+        dbxml = AugmentXMLDB((xmlFileName,)) if not remove else None
+    except IOError, e:
+        usage("Could not read XML augment file: %s" % (e,))
+
+    #
+    # Start the reactor
+    #
+    reactor.callLater(0, run, dbxml)
+    reactor.run()
+
+ at inlineCallbacks
+def run(dbxml):
+    
+    try:
+        guids = set((yield augment.AugmentService.getAllGUIDs()))
+        added = 0
+        updated = 0
+        removed = 0
+        if dbxml:
+            for record in dbxml.db.values():
+                yield augment.AugmentService.addAugmentRecord(record, record.guid in guids)
+                if record.guid in guids:
+                    updated += 1
+                else:
+                    added += 1
+            for guid in guids.difference(dbxml.db.keys()):
+                yield augment.AugmentService.removeAugmentRecord(guid)
+                removed += 1
+        else:
+            yield augment.AugmentService.clean()
+            removed = len(guids)
+            
+        print "Changes:"
+        print "  Added: %d" % (added,)
+        print "  Changed: %d" % (updated,)
+        print "  Removed: %d" % (removed,)
+    finally:
+        #
+        # Stop the reactor
+        #
+        reactor.stop()

Deleted: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/manageaugments.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/manageaugments.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/manageaugments.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,217 +0,0 @@
-#!/usr/bin/env python
-##
-# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from optparse import OptionParser
-from twistedcaldav.directory import xmlaugmentsparser
-from xml.etree.ElementTree import ElementTree, tostring, SubElement
-from xml.parsers.expat import ExpatError
-import sys
-import os
-
-def error(s):
-    print s
-    sys.exit(1)
-
-def readXML(xmlfile):
-
-    # Read in XML
-    try:
-        tree = ElementTree(file=xmlfile)
-    except ExpatError, e:
-        error("Unable to parse file '%s' because: %s" % (xmlfile, e,))
-
-    # Verify that top-level element is correct
-    augments_node = tree.getroot()
-    if augments_node.tag != xmlaugmentsparser.ELEMENT_AUGMENTS:
-        error("Ignoring file '%s' because it is not a augments file" % (xmlfile,))
-
-    return augments_node
-
-def writeXML(xmlfile, root):
-    
-    data = """<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE augments SYSTEM "augments.dtd">
-
-""" + tostring(root)
-
-    with open(xmlfile, "w") as f:
-        f.write(data)
-
-def addSubElement(parent, tag, text=None, indent=0):
-    
-    child = SubElement(parent, tag)
-    child.text = text
-    child.tail = "\n" + " " * indent
-    return child
-
-def changeSubElementText(parent, tag, text):
-    
-    child = parent.find(tag)
-    child.text = text
-
-def doAdd(xmlfile, guid, host, enable_calendar, auto_schedule):
-
-    augments_node = readXML(xmlfile)
-
-    # Make sure GUID is not already present
-    for child in augments_node.getchildren():
-        
-        if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
-            error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
-
-        for node in child.getchildren():
-            
-            if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
-                error("Cannot add guid '%s' because it already exists in augment file: '%s'" % (guid, xmlfile,))
-    
-    # Create new record
-    if len(augments_node.getchildren()):
-        augments_node.getchildren()[-1].tail = "\n  "
-    record = addSubElement(augments_node, xmlaugmentsparser.ELEMENT_RECORD, "\n    ")
-    addSubElement(record, xmlaugmentsparser.ELEMENT_GUID, guid, 4)
-    addSubElement(record, xmlaugmentsparser.ELEMENT_ENABLE, "true", 4)
-    addSubElement(record, xmlaugmentsparser.ELEMENT_HOSTEDAT, host, 4)
-    addSubElement(record, xmlaugmentsparser.ELEMENT_ENABLECALENDAR, "true" if enable_calendar else "false", 4)
-    addSubElement(record, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE, "true" if auto_schedule else "false", 2)
-    
-    # Modify xmlfile
-    writeXML(xmlfile, augments_node)
-    print "Added guid '%s' in augment file: '%s'" % (guid, xmlfile,)
-    
-def doModify(xmlfile, guid, host, enable_calendar, auto_schedule):
-
-    augments_node = readXML(xmlfile)
-
-    # Make sure GUID is present
-    for child in augments_node.getchildren():
-        
-        if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
-            error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
-
-        for node in child.getchildren():
-            
-            if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
-                break
-        else:
-            continue
-        break
-    else:
-        error("Cannot modify guid '%s' because it does not exist in augment file: '%s'" % (guid, xmlfile,))
-    
-    # Modify record
-    if host is not None:
-        child.find(xmlaugmentsparser.ELEMENT_HOSTEDAT).text = host
-    child.find(xmlaugmentsparser.ELEMENT_ENABLECALENDAR).text = "true" if enable_calendar else "false"
-    child.find(xmlaugmentsparser.ELEMENT_AUTOSCHEDULE).text = "true" if auto_schedule else "false"
-    
-    # Modify xmlfile
-    writeXML(xmlfile, augments_node)
-    print "Modified guid '%s' in augment file: '%s'" % (guid, xmlfile,)
-
-def doRemove(xmlfile, guid):
-
-    augments_node = readXML(xmlfile)
-
-    # Make sure GUID is present
-    for child in augments_node.getchildren():
-        
-        if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
-            error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
-
-        for node in child.getchildren():
-            
-            if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
-                break
-        else:
-            continue
-        augments_node.remove(child)
-        break
-    else:
-        error("Cannot remove guid '%s' because it does not exist in augment file: '%s'" % (guid, xmlfile,))
-    
-    # Modify xmlfile
-    writeXML(xmlfile, augments_node)
-    print "Removed guid '%s' from augment file: '%s'" % (guid, xmlfile,)
-    
-def doPrint(xmlfile):
-
-    # Read in XML
-    augments_node = readXML(xmlfile)
-
-    print tostring(augments_node)
-
-def main():
-
-    usage = "%prog [options] ACTION"
-    epilog = """
-ACTION is one of add|modify|remove|print
-
-  add:    add a user record
-  modify: modify a user record
-  remove: remove a user record
-  print:  print all user records
-"""
-    description = "Tool to manipulate CalendarServer augments XML file"
-    version = "%prog v1.0"
-    parser = OptionParser(usage=usage, description=description, version=version)
-    parser.epilog = epilog
-    parser.format_epilog = lambda _:epilog
-
-    parser.add_option("-f", "--file", dest="xmlfilename",
-                      help="XML augment file to manipulate", metavar="FILE")
-    parser.add_option("-g", "--guid", dest="guid",
-                      help="OD GUID to manipulate", metavar="GUID")
-    parser.add_option("-i", "--guidfile", dest="guidfile",
-                      help="File containing a list of GUIDs to manipulate", metavar="GUIDFILE")
-    parser.add_option("-n", "--node", dest="node",
-                      help="Partition node to assign to GUID", metavar="NODE")
-    parser.add_option("-c", "--enable-calendar", action="store_true", dest="enable_calendar",
-                      default=True, help="Enable calendaring for this GUID: %default")
-    parser.add_option("-a", "--auto-schedule", action="store_true", dest="auto_schedule",
-                      default=False, help="Enable auto-schedule for this GUID: %default")
-
-    (options, args) = parser.parse_args()
-
-    if len(args) != 1:
-        parser.error("incorrect number of arguments")
-
-    guids = []
-    if options.guid:
-        guids.append(options.guid)
-    elif options.guidfile:
-        if not os.path.exists(options.guidfile):
-            parser.error("File containing list of GUIDs does not exist")
-        with open(options.guidfile) as f:
-            for line in f:
-                guids.append(line[:-1])
-        
-    if args[0] == "add":
-        if not options.node:
-            parser.error("Partition node must be specified when adding")
-        for guid in guids:
-            doAdd(options.xmlfilename, guid, options.node, options.enable_calendar, options.auto_schedule)
-    elif args[0] == "modify":
-        for guid in guids:
-            doModify(options.xmlfilename, guid, options.node, options.enable_calendar, options.auto_schedule)
-    elif args[0] == "remove":
-        for guid in guids:
-            doRemove(options.xmlfilename, guid)
-    elif args[0] == "print":
-        doPrint(options.xmlfilename)
-
-if __name__ == '__main__':
-    main()

Copied: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/manageaugments.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/manageaugments.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/manageaugments.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/manageaugments.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,217 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from optparse import OptionParser
+from twistedcaldav.directory import xmlaugmentsparser
+from xml.etree.ElementTree import ElementTree, tostring, SubElement
+from xml.parsers.expat import ExpatError
+import sys
+import os
+
+def error(s):
+    print s
+    sys.exit(1)
+
+def readXML(xmlfile):
+
+    # Read in XML
+    try:
+        tree = ElementTree(file=xmlfile)
+    except ExpatError, e:
+        error("Unable to parse file '%s' because: %s" % (xmlfile, e,))
+
+    # Verify that top-level element is correct
+    augments_node = tree.getroot()
+    if augments_node.tag != xmlaugmentsparser.ELEMENT_AUGMENTS:
+        error("Ignoring file '%s' because it is not a augments file" % (xmlfile,))
+
+    return augments_node
+
+def writeXML(xmlfile, root):
+    
+    data = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE augments SYSTEM "augments.dtd">
+
+""" + tostring(root)
+
+    with open(xmlfile, "w") as f:
+        f.write(data)
+
+def addSubElement(parent, tag, text=None, indent=0):
+    
+    child = SubElement(parent, tag)
+    child.text = text
+    child.tail = "\n" + " " * indent
+    return child
+
+def changeSubElementText(parent, tag, text):
+    
+    child = parent.find(tag)
+    child.text = text
+
+def doAdd(xmlfile, guid, host, enable_calendar, auto_schedule):
+
+    augments_node = readXML(xmlfile)
+
+    # Make sure GUID is not already present
+    for child in augments_node.getchildren():
+        
+        if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
+            error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
+
+        for node in child.getchildren():
+            
+            if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
+                error("Cannot add guid '%s' because it already exists in augment file: '%s'" % (guid, xmlfile,))
+    
+    # Create new record
+    if len(augments_node.getchildren()):
+        augments_node.getchildren()[-1].tail = "\n  "
+    record = addSubElement(augments_node, xmlaugmentsparser.ELEMENT_RECORD, "\n    ")
+    addSubElement(record, xmlaugmentsparser.ELEMENT_GUID, guid, 4)
+    addSubElement(record, xmlaugmentsparser.ELEMENT_ENABLE, "true", 4)
+    addSubElement(record, xmlaugmentsparser.ELEMENT_HOSTEDAT, host, 4)
+    addSubElement(record, xmlaugmentsparser.ELEMENT_ENABLECALENDAR, "true" if enable_calendar else "false", 4)
+    addSubElement(record, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE, "true" if auto_schedule else "false", 2)
+    
+    # Modify xmlfile
+    writeXML(xmlfile, augments_node)
+    print "Added guid '%s' in augment file: '%s'" % (guid, xmlfile,)
+    
+def doModify(xmlfile, guid, host, enable_calendar, auto_schedule):
+
+    augments_node = readXML(xmlfile)
+
+    # Make sure GUID is present
+    for child in augments_node.getchildren():
+        
+        if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
+            error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
+
+        for node in child.getchildren():
+            
+            if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
+                break
+        else:
+            continue
+        break
+    else:
+        error("Cannot modify guid '%s' because it does not exist in augment file: '%s'" % (guid, xmlfile,))
+    
+    # Modify record
+    if host is not None:
+        child.find(xmlaugmentsparser.ELEMENT_HOSTEDAT).text = host
+    child.find(xmlaugmentsparser.ELEMENT_ENABLECALENDAR).text = "true" if enable_calendar else "false"
+    child.find(xmlaugmentsparser.ELEMENT_AUTOSCHEDULE).text = "true" if auto_schedule else "false"
+    
+    # Modify xmlfile
+    writeXML(xmlfile, augments_node)
+    print "Modified guid '%s' in augment file: '%s'" % (guid, xmlfile,)
+
+def doRemove(xmlfile, guid):
+
+    augments_node = readXML(xmlfile)
+
+    # Make sure GUID is present
+    for child in augments_node.getchildren():
+        
+        if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
+            error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
+
+        for node in child.getchildren():
+            
+            if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
+                break
+        else:
+            continue
+        augments_node.remove(child)
+        break
+    else:
+        error("Cannot remove guid '%s' because it does not exist in augment file: '%s'" % (guid, xmlfile,))
+    
+    # Modify xmlfile
+    writeXML(xmlfile, augments_node)
+    print "Removed guid '%s' from augment file: '%s'" % (guid, xmlfile,)
+    
+def doPrint(xmlfile):
+
+    # Read in XML
+    augments_node = readXML(xmlfile)
+
+    print tostring(augments_node)
+
+def main():
+
+    usage = "%prog [options] ACTION"
+    epilog = """
+ACTION is one of add|modify|remove|print
+
+  add:    add a user record
+  modify: modify a user record
+  remove: remove a user record
+  print:  print all user records
+"""
+    description = "Tool to manipulate CalendarServer augments XML file"
+    version = "%prog v1.0"
+    parser = OptionParser(usage=usage, description=description, version=version)
+    parser.epilog = epilog
+    parser.format_epilog = lambda _:epilog
+
+    parser.add_option("-f", "--file", dest="xmlfilename",
+                      help="XML augment file to manipulate", metavar="FILE")
+    parser.add_option("-g", "--guid", dest="guid",
+                      help="OD GUID to manipulate", metavar="GUID")
+    parser.add_option("-i", "--guidfile", dest="guidfile",
+                      help="File containing a list of GUIDs to manipulate", metavar="GUIDFILE")
+    parser.add_option("-n", "--node", dest="node",
+                      help="Partition node to assign to GUID", metavar="NODE")
+    parser.add_option("-c", "--enable-calendar", action="store_true", dest="enable_calendar",
+                      default=True, help="Enable calendaring for this GUID: %default")
+    parser.add_option("-a", "--auto-schedule", action="store_true", dest="auto_schedule",
+                      default=False, help="Enable auto-schedule for this GUID: %default")
+
+    (options, args) = parser.parse_args()
+
+    if len(args) != 1:
+        parser.error("incorrect number of arguments")
+
+    guids = []
+    if options.guid:
+        guids.append(options.guid)
+    elif options.guidfile:
+        if not os.path.exists(options.guidfile):
+            parser.error("File containing list of GUIDs does not exist")
+        with open(options.guidfile) as f:
+            for line in f:
+                guids.append(line[:-1])
+        
+    if args[0] == "add":
+        if not options.node:
+            parser.error("Partition node must be specified when adding")
+        for guid in guids:
+            doAdd(options.xmlfilename, guid, options.node, options.enable_calendar, options.auto_schedule)
+    elif args[0] == "modify":
+        for guid in guids:
+            doModify(options.xmlfilename, guid, options.node, options.enable_calendar, options.auto_schedule)
+    elif args[0] == "remove":
+        for guid in guids:
+            doRemove(options.xmlfilename, guid)
+    elif args[0] == "print":
+        doPrint(options.xmlfilename)
+
+if __name__ == '__main__':
+    main()

Deleted: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/managepostgres.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/managepostgres.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/managepostgres.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,114 +0,0 @@
-#!/usr/bin/env python
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from optparse import OptionParser
-import os
-import subprocess
-import sys
-import time
-
-def error(s):
-    print s
-    sys.exit(1)
-
-def cmd(s):
-    print s
-    subprocess.call(s, shell=True)
-
-def doInit(basedir):
-    
-    cmd("mkdir %s/data" % (basedir,))
-    cmd("%s/bin/initdb -D %s/data" % (basedir, basedir,))
-    
-    # Have the DB listen on all interfaces
-    with open("%s/data/postgresql.conf" % (basedir,)) as f:
-        conf = f.read()
-    conf = conf.replace("#listen_addresses = 'localhost'", "listen_addresses = '*'\t")
-    conf = conf.replace("max_connections = 20 ", "max_connections = 500")
-    with open("%s/data/postgresql.conf" % (basedir,), "w") as f:
-        f.write(conf)
-        
-    # Allow current user to auth to the DBs
-    with open("%s/data/pg_hba.conf" % (basedir,)) as f:
-        conf = f.read()
-    conf = conf.replace("127.0.0.1/32", "0.0.0.0/0   ")
-    with open("%s/data/pg_hba.conf" % (basedir,), "w") as f:
-        f.write(conf)
-
-    cmd("%s/bin/pg_ctl -D %s/data -l logfile start" % (basedir, basedir,))
-    time.sleep(5)
-    cmd("%s/bin/createdb proxies" % (basedir,))
-    cmd("%s/bin/createdb augments" % (basedir,))
-    cmd("%s/bin/pg_ctl -D %s/data -l logfile stop" % (basedir, basedir,))
-
-def doStart(basedir):
-    
-    cmd("%s/bin/pg_ctl -D %s/data -l logfile start" % (basedir, basedir,))
-
-def doStop(basedir):
-    
-    cmd("%s/bin/pg_ctl -D %s/data -l logfile stop" % (basedir, basedir,))
-
-def doRun(basedir, verbose):
-    
-    cmd("%s/bin/postgres %s -D %s/data" % (basedir, "-d 3" if verbose else "",  basedir,))
-
-def doClean(basedir):
-    
-    cmd("rm -rf %s/data" % (basedir,))
-
-if __name__ == '__main__':
-
-    usage = "%prog [options] ACTION"
-    epilog = """
-ACTION is one of init|start|stop|run
-
-  init:   initialize databases
-  start:  start postgres daemon
-  stop:   stop postgres daemon
-  run:    run postgres (non-daemon)
-  clean:  remove databases
-  
-"""
-    description = "Tool to manage PostgreSQL"
-    version = "%prog v1.0"
-    parser = OptionParser(usage=usage, description=description, version=version)
-    parser.epilog = epilog
-    parser.format_epilog = lambda _:epilog
-
-    parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
-                      default=True, help="Use debug logging for PostgreSQL")
-    parser.add_option("-d", "--base-dir", dest="basedir",
-                      default="%s/../postgresql-8.4.1/_root" % (os.getcwd(),), help="Base directory for PostgreSQL install")
-
-    (options, args) = parser.parse_args()
-
-    if len(args) != 1:
-        parser.error("incorrect number of arguments")
-
-    if args[0] == "init":
-        doInit(options.basedir)
-    elif args[0] == "start":
-        doStart(options.basedir)
-    elif args[0] == "stop":
-        doStop(options.basedir)
-    elif args[0] == "run":
-        doRun(options.basedir, options.verbose)
-    elif args[0] == "clean":
-        doClean(options.basedir)
-    else:
-        parser.error("incorrect argument '%s'" % (args[0],))

Copied: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/managepostgres.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/managepostgres.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/managepostgres.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/managepostgres.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from optparse import OptionParser
+import os
+import subprocess
+import sys
+import time
+
+def error(s):
+    print s
+    sys.exit(1)
+
+def cmd(s):
+    print s
+    subprocess.call(s, shell=True)
+
+def doInit(basedir):
+    
+    cmd("mkdir %s/data" % (basedir,))
+    cmd("%s/bin/initdb -D %s/data" % (basedir, basedir,))
+    
+    # Have the DB listen on all interfaces
+    with open("%s/data/postgresql.conf" % (basedir,)) as f:
+        conf = f.read()
+    conf = conf.replace("#listen_addresses = 'localhost'", "listen_addresses = '*'\t")
+    conf = conf.replace("max_connections = 20 ", "max_connections = 500")
+    with open("%s/data/postgresql.conf" % (basedir,), "w") as f:
+        f.write(conf)
+        
+    # Allow current user to auth to the DBs
+    with open("%s/data/pg_hba.conf" % (basedir,)) as f:
+        conf = f.read()
+    conf = conf.replace("127.0.0.1/32", "0.0.0.0/0   ")
+    with open("%s/data/pg_hba.conf" % (basedir,), "w") as f:
+        f.write(conf)
+
+    cmd("%s/bin/pg_ctl -D %s/data -l logfile start" % (basedir, basedir,))
+    time.sleep(5)
+    cmd("%s/bin/createdb proxies" % (basedir,))
+    cmd("%s/bin/createdb augments" % (basedir,))
+    cmd("%s/bin/pg_ctl -D %s/data -l logfile stop" % (basedir, basedir,))
+
+def doStart(basedir):
+    
+    cmd("%s/bin/pg_ctl -D %s/data -l logfile start" % (basedir, basedir,))
+
+def doStop(basedir):
+    
+    cmd("%s/bin/pg_ctl -D %s/data -l logfile stop" % (basedir, basedir,))
+
+def doRun(basedir, verbose):
+    
+    cmd("%s/bin/postgres %s -D %s/data" % (basedir, "-d 3" if verbose else "",  basedir,))
+
+def doClean(basedir):
+    
+    cmd("rm -rf %s/data" % (basedir,))
+
+if __name__ == '__main__':
+
+    usage = "%prog [options] ACTION"
+    epilog = """
+ACTION is one of init|start|stop|run
+
+  init:   initialize databases
+  start:  start postgres daemon
+  stop:   stop postgres daemon
+  run:    run postgres (non-daemon)
+  clean:  remove databases
+  
+"""
+    description = "Tool to manage PostgreSQL"
+    version = "%prog v1.0"
+    parser = OptionParser(usage=usage, description=description, version=version)
+    parser.epilog = epilog
+    parser.format_epilog = lambda _:epilog
+
+    parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+                      default=True, help="Use debug logging for PostgreSQL")
+    parser.add_option("-d", "--base-dir", dest="basedir",
+                      default="%s/../postgresql-8.4.1/_root" % (os.getcwd(),), help="Base directory for PostgreSQL install")
+
+    (options, args) = parser.parse_args()
+
+    if len(args) != 1:
+        parser.error("incorrect number of arguments")
+
+    if args[0] == "init":
+        doInit(options.basedir)
+    elif args[0] == "start":
+        doStart(options.basedir)
+    elif args[0] == "stop":
+        doStop(options.basedir)
+    elif args[0] == "run":
+        doRun(options.basedir, options.verbose)
+    elif args[0] == "clean":
+        doClean(options.basedir)
+    else:
+        parser.error("incorrect argument '%s'" % (args[0],))

Deleted: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/principals.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/principals.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,445 +0,0 @@
-#!/usr/bin/env python
-
-##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import sys
-import os
-from getopt import getopt, GetoptError
-from uuid import UUID
-from pwd import getpwnam
-from grp import getgrnam
-
-from twisted.python.util import switchUID
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks
-from twisted.web2.dav import davxml
-
-from twext.python.log import StandardIOObserver
-from twext.web2.dav.davxml import sname2qname, qname2sname
-
-from twistedcaldav import memcachepool
-from twistedcaldav.config import config, ConfigurationError
-from twistedcaldav.log import setLogLevelForNamespace
-from twistedcaldav.notify import installNotificationClient
-from twistedcaldav.root import RootResource
-from twistedcaldav.static import CalendarHomeProvisioningFile
-
-from calendarserver.tools.util import booleanArgument, autoDisableMemcached
-from calendarserver.tools.util import loadConfig, getDirectory
-
-def usage(e=None):
-    if e:
-        print e
-        print ""
-
-    name = os.path.basename(sys.argv[0])
-    print "usage: %s [options] actions principal [principal ...]" % (name,)
-    print ""
-    print "  Performs the given actions against the giving principals."
-    print ""
-    print "  Principals are identified by one of the following:"
-    print "    Type and shortname (eg.: users:wsanchez)"
-    #print "    A principal path (eg.: /principals/users/wsanchez/)"
-    print "    A GUID (eg.: E415DBA7-40B5-49F5-A7CC-ACC81E4DEC79)"
-    print ""
-    print "options:"
-    print "  -h --help: print this help and exit"
-    print "  -f --config <path>: Specify caldavd.plist configuration path"
-    print ""
-    print "actions:"
-    #print "  --search <search-string>: search for matching resources"
-    print "  -P, --read-property=property: read DAV property (eg.: {DAV:}group-member-set)"
-    print "  --list-read-proxies: list proxies with read-only access"
-    print "  --list-write-proxies: list proxies with read-write access"
-    print "  --list-proxies: list all proxies"
-    print "  --add-read-proxy=principal: add a read-only proxy"
-    print "  --add-write-proxy=principal: add a read-write proxy"
-    print "  --remove-proxy=principal: remove a proxy"
-    print "  --set-auto-schedule={true|false}: set auto-accept state"
-    print "  --get-auto-schedule: read auto-schedule state"
-
-    if e:
-        sys.exit(64)
-    else:
-        sys.exit(0)
-
-def main():
-    #
-    # Send logging output to stdout
-    #
-    observer = StandardIOObserver()
-    observer.start()
-
-    try:
-        (optargs, args) = getopt(
-            sys.argv[1:], "hf:P:", [
-                "help",
-                "config=",
-                "read-property=",
-                "list-read-proxies",
-                "list-write-proxies",
-                "list-proxies",
-                "add-read-proxy=",
-                "add-write-proxy=",
-                "remove-proxy=",
-                "set-auto-schedule=",
-                "get-auto-schedule",
-            ],
-        )
-    except GetoptError, e:
-        usage(e)
-
-    #
-    # Get configuration
-    #
-    configFileName = None
-    actions = []
-
-    for opt, arg in optargs:
-        if opt in ("-h", "--help"):
-            usage()
-
-        elif opt in ("-f", "--config"):
-            configFileName = arg
-
-        elif opt in ("-P", "--read-property"):
-            try:
-                qname = sname2qname(arg)
-            except ValueError, e:
-                abort(e)
-            actions.append((action_readProperty, qname))
-
-        elif opt in ("", "--list-read-proxies"):
-            actions.append((action_listProxies, "read"))
-
-        elif opt in ("", "--list-write-proxies"):
-            actions.append((action_listProxies, "write"))
-
-        elif opt in ("-L", "--list-proxies"):
-            actions.append((action_listProxies, "read", "write"))
-
-        elif opt in ("--add-read-proxy", "--add-write-proxy"):
-            if "read" in opt:
-                proxyType = "read"
-            elif "write" in opt:
-                proxyType = "write"
-            else:
-                raise AssertionError("Unknown proxy type")
-
-            try:
-                principalForPrincipalID(arg, checkOnly=True)
-            except ValueError, e:
-                abort(e)
-
-            actions.append((action_addProxy, proxyType, arg))
-
-        elif opt in ("", "--remove-proxy"):
-            try:
-                principalForPrincipalID(arg, checkOnly=True)
-            except ValueError, e:
-                abort(e)
-
-            actions.append((action_removeProxy, arg))
-
-        elif opt in ("", "--set-auto-schedule"):
-            try:
-                autoSchedule = booleanArgument(arg)
-            except ValueError, e:
-                abort(e)
-
-            actions.append((action_setAutoSchedule, autoSchedule))
-
-        elif opt in ("", "--get-auto-schedule"):
-            actions.append((action_getAutoSchedule,))
-
-        else:
-            raise NotImplementedError(opt)
-
-    if not args:
-        usage("No principals specified.")
-
-    #
-    # Get configuration
-    #
-    try:
-        loadConfig(configFileName)
-        setLogLevelForNamespace(None, "warn")
-
-        # Shed privileges
-        if config.UserName and config.GroupName and os.getuid() == 0:
-            uid = getpwnam(config.UserName).pw_uid
-            gid = getgrnam(config.GroupName).gr_gid
-            switchUID(uid, uid, gid)
-
-        os.umask(config.umask)
-
-        config.directory = getDirectory()
-        autoDisableMemcached(config)
-    except ConfigurationError, e:
-        abort(e)
-
-    #
-    # Do a quick sanity check that arguments look like principal
-    # identifiers.
-    #
-    for arg in args:
-        try:
-            principalForPrincipalID(arg, checkOnly=True)
-        except ValueError, e:
-            abort(e)
-
-    #
-    # Start the reactor
-    #
-    reactor.callLater(0, run, args, actions)
-    reactor.run()
-
- at inlineCallbacks
-def run(principalIDs, actions):
-    try:
-        #
-        # Connect to memcached, notifications
-        #
-        memcachepool.installPools(
-            config.Memcached.Pools,
-            config.Memcached.MaxClients
-        )
-        if config.Notifications.Enabled:
-            installNotificationClient(
-                config.Notifications.InternalNotificationHost,
-                config.Notifications.InternalNotificationPort,
-            )
-
-        #
-        # Wire up the resource hierarchy
-        #
-        principalCollection = config.directory.getPrincipalCollection()
-        root = RootResource(
-            config.DocumentRoot,
-            principalCollections=(principalCollection,),
-        )
-        root.putChild("principals", principalCollection)
-        calendarCollection = CalendarHomeProvisioningFile(
-            os.path.join(config.DocumentRoot, "calendars"),
-            config.directory, "/calendars/",
-        )
-        root.putChild("calendars", calendarCollection)
-
-        #
-        # Wrap root resource
-        #
-        # FIXME: not a fan -wsanchez
-        #root = ResourceWrapper(root)
-
-        for principalID in principalIDs:
-            # Resolve the given principal IDs to principals
-            try:
-                principal = principalForPrincipalID(principalID)
-            except ValueError:
-                principal = None
-
-            if principal is None:
-                sys.stderr.write("Invalid principal ID: %s\n" % (principalID,))
-                continue
-
-            # Performs requested actions
-            for action in actions:
-                (yield action[0](principal, *action[1:]))
-                print ""
-
-    finally:
-        #
-        # Stop the reactor
-        #
-        reactor.stop()
-
-def principalForPrincipalID(principalID, checkOnly=False, directory=None):
-    
-    # Allow a directory parameter to be passed in, but default to config.directory
-    # But config.directory isn't set right away, so only use it when we're doing more 
-    # than checking.
-    if not checkOnly and not directory:
-        directory = config.directory
-
-    if principalID.startswith("/"):
-        raise ValueError("Can't resolve paths yet")
-
-        if checkOnly:
-            return None
-
-    if principalID.startswith("("):
-        try:
-            i = principalID.index(")")
-
-            if checkOnly:
-                return None
-
-            recordType = principalID[1:i]
-            shortName = principalID[i+1:]
-
-            if not recordType or not shortName or "(" in recordType:
-                raise ValueError()
-
-            return directory.principalCollection.principalForShortName(recordType, shortName)
-
-        except ValueError:
-            pass
-
-    if ":" in principalID:
-        if checkOnly:
-            return None
-
-        recordType, shortName = principalID.split(":", 1)
-
-        return directory.principalCollection.principalForShortName(recordType, shortName)
-
-    try:
-        guid = UUID(principalID)
-
-        if checkOnly:
-            return None
-
-        return directory.principalCollection.principalForUID(guid)
-    except ValueError:
-        pass
-
-    raise ValueError("Invalid principal identifier: %s" % (principalID,))
-
-def proxySubprincipal(principal, proxyType):
-    return principal.getChild("calendar-proxy-" + proxyType)
-
- at inlineCallbacks
-def action_readProperty(resource, qname):
-    property = (yield resource.readProperty(qname, None))
-    print "%r on %s:" % (qname2sname(qname), resource)
-    print ""
-    print property.toxml()
-
- at inlineCallbacks
-def action_listProxies(principal, *proxyTypes):
-    for proxyType in proxyTypes:
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is None:
-            print "No %s proxies for %s" % (proxyType, principal)
-            continue
-
-        membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-
-        if membersProperty.children:
-            print "%s proxies for %s:" % (
-                {"read": "Read-only", "write": "Read/write"}[proxyType],
-                principal,
-            )
-            for member in membersProperty.children:
-                print " *", member
-        else:
-            print "No %s proxies for %s" % (proxyType, principal)
-
- at inlineCallbacks
-def action_addProxy(principal, proxyType, *proxyIDs):
-    for proxyID in proxyIDs:
-        proxyPrincipal = principalForPrincipalID(proxyID)
-        (yield action_addProxyPrincipal(principal, proxyType, proxyPrincipal))
-
- at inlineCallbacks
-def action_addProxyPrincipal(principal, proxyType, proxyPrincipal):
-    proxyURL = proxyPrincipal.url()
-
-    subPrincipal = proxySubprincipal(principal, proxyType)
-    if subPrincipal is None:
-        sys.stderr.write("Unable to edit %s proxies for %s\n" % (proxyType, principal))
-        return
-
-    membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-
-    for memberURL in membersProperty.children:
-        if str(memberURL) == proxyURL:
-            print "%s is already a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
-            break
-    else:
-        memberURLs = list(membersProperty.children)
-        memberURLs.append(davxml.HRef(proxyURL))
-        membersProperty = davxml.GroupMemberSet(*memberURLs)
-        (yield subPrincipal.writeProperty(membersProperty, None))
-        print "Added %s as a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
-
-    proxyTypes = ["read", "write"]
-    proxyTypes.remove(proxyType)
-
-    (yield action_removeProxyPrincipal(principal, proxyPrincipal, proxyTypes=proxyTypes))
-
- at inlineCallbacks
-def action_removeProxy(principal, *proxyIDs, **kwargs):
-    for proxyID in proxyIDs:
-        proxyPrincipal = principalForPrincipalID(proxyID)
-        (yield action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs))
-
- at inlineCallbacks
-def action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs):
-    proxyTypes = kwargs.get("proxyTypes", ("read", "write"))
-    for proxyType in proxyTypes:
-        proxyURL = proxyPrincipal.url()
-
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is None:
-            sys.stderr.write("Unable to edit %s proxies for %s\n" % (proxyType, principal))
-            continue
-
-        membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-
-        memberURLs = [
-            m for m in membersProperty.children
-            if str(m) != proxyURL
-        ]
-
-        if len(memberURLs) == len(membersProperty.children):
-            # No change
-            continue
-
-        membersProperty = davxml.GroupMemberSet(*memberURLs)
-        (yield subPrincipal.writeProperty(membersProperty, None))
-        print "Removed %s as a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
-
- at inlineCallbacks
-def action_setAutoSchedule(principal, autoSchedule):
-    if autoSchedule and principal.record.recordType in ("users", "groups"):
-        print "Enabling auto-schedule for %s is not allowed." % (principal,)
-    else:
-        print "Setting auto-schedule to %s for %s" % (
-            { True: "true", False: "false" }[autoSchedule],
-            principal,
-        )
-        principal.setAutoSchedule(autoSchedule)
-
- at inlineCallbacks
-def action_getAutoSchedule(principal):
-    autoSchedule = principal.getAutoSchedule()
-    print "Autoschedule for %s is %s" % (
-        principal,
-        { True: "true", False: "false" }[autoSchedule],
-    )
-
-def abort(msg, status=1):
-    sys.stdout.write("%s\n" % (msg,))
-    try:
-        reactor.stop()
-    except RuntimeError:
-        pass
-    sys.exit(status)
-
-if __name__ == "__main__":
-    main()

Copied: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/principals.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/principals.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/principals.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/principals.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,445 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+import os
+from getopt import getopt, GetoptError
+from uuid import UUID
+from pwd import getpwnam
+from grp import getgrnam
+
+from twisted.python.util import switchUID
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks
+from twisted.web2.dav import davxml
+
+from twext.python.log import StandardIOObserver
+from twext.web2.dav.davxml import sname2qname, qname2sname
+
+from twistedcaldav import memcachepool
+from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.log import setLogLevelForNamespace
+from twistedcaldav.notify import installNotificationClient
+from twistedcaldav.root import RootResource
+from twistedcaldav.static import CalendarHomeProvisioningFile
+
+from calendarserver.tools.util import booleanArgument, autoDisableMemcached
+from calendarserver.tools.util import loadConfig, getDirectory
+
+def usage(e=None):
+    if e:
+        print e
+        print ""
+
+    name = os.path.basename(sys.argv[0])
+    print "usage: %s [options] actions principal [principal ...]" % (name,)
+    print ""
+    print "  Performs the given actions against the giving principals."
+    print ""
+    print "  Principals are identified by one of the following:"
+    print "    Type and shortname (eg.: users:wsanchez)"
+    #print "    A principal path (eg.: /principals/users/wsanchez/)"
+    print "    A GUID (eg.: E415DBA7-40B5-49F5-A7CC-ACC81E4DEC79)"
+    print ""
+    print "options:"
+    print "  -h --help: print this help and exit"
+    print "  -f --config <path>: Specify caldavd.plist configuration path"
+    print ""
+    print "actions:"
+    #print "  --search <search-string>: search for matching resources"
+    print "  -P, --read-property=property: read DAV property (eg.: {DAV:}group-member-set)"
+    print "  --list-read-proxies: list proxies with read-only access"
+    print "  --list-write-proxies: list proxies with read-write access"
+    print "  --list-proxies: list all proxies"
+    print "  --add-read-proxy=principal: add a read-only proxy"
+    print "  --add-write-proxy=principal: add a read-write proxy"
+    print "  --remove-proxy=principal: remove a proxy"
+    print "  --set-auto-schedule={true|false}: set auto-accept state"
+    print "  --get-auto-schedule: read auto-schedule state"
+
+    if e:
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
+def main():
+    #
+    # Send logging output to stdout
+    #
+    observer = StandardIOObserver()
+    observer.start()
+
+    try:
+        (optargs, args) = getopt(
+            sys.argv[1:], "hf:P:", [
+                "help",
+                "config=",
+                "read-property=",
+                "list-read-proxies",
+                "list-write-proxies",
+                "list-proxies",
+                "add-read-proxy=",
+                "add-write-proxy=",
+                "remove-proxy=",
+                "set-auto-schedule=",
+                "get-auto-schedule",
+            ],
+        )
+    except GetoptError, e:
+        usage(e)
+
+    #
+    # Get configuration
+    #
+    configFileName = None
+    actions = []
+
+    for opt, arg in optargs:
+        if opt in ("-h", "--help"):
+            usage()
+
+        elif opt in ("-f", "--config"):
+            configFileName = arg
+
+        elif opt in ("-P", "--read-property"):
+            try:
+                qname = sname2qname(arg)
+            except ValueError, e:
+                abort(e)
+            actions.append((action_readProperty, qname))
+
+        elif opt in ("", "--list-read-proxies"):
+            actions.append((action_listProxies, "read"))
+
+        elif opt in ("", "--list-write-proxies"):
+            actions.append((action_listProxies, "write"))
+
+        elif opt in ("-L", "--list-proxies"):
+            actions.append((action_listProxies, "read", "write"))
+
+        elif opt in ("--add-read-proxy", "--add-write-proxy"):
+            if "read" in opt:
+                proxyType = "read"
+            elif "write" in opt:
+                proxyType = "write"
+            else:
+                raise AssertionError("Unknown proxy type")
+
+            try:
+                principalForPrincipalID(arg, checkOnly=True)
+            except ValueError, e:
+                abort(e)
+
+            actions.append((action_addProxy, proxyType, arg))
+
+        elif opt in ("", "--remove-proxy"):
+            try:
+                principalForPrincipalID(arg, checkOnly=True)
+            except ValueError, e:
+                abort(e)
+
+            actions.append((action_removeProxy, arg))
+
+        elif opt in ("", "--set-auto-schedule"):
+            try:
+                autoSchedule = booleanArgument(arg)
+            except ValueError, e:
+                abort(e)
+
+            actions.append((action_setAutoSchedule, autoSchedule))
+
+        elif opt in ("", "--get-auto-schedule"):
+            actions.append((action_getAutoSchedule,))
+
+        else:
+            raise NotImplementedError(opt)
+
+    if not args:
+        usage("No principals specified.")
+
+    #
+    # Get configuration
+    #
+    try:
+        loadConfig(configFileName)
+        setLogLevelForNamespace(None, "warn")
+
+        # Shed privileges
+        if config.UserName and config.GroupName and os.getuid() == 0:
+            uid = getpwnam(config.UserName).pw_uid
+            gid = getgrnam(config.GroupName).gr_gid
+            switchUID(uid, uid, gid)
+
+        os.umask(config.umask)
+
+        config.directory = getDirectory()
+        autoDisableMemcached(config)
+    except ConfigurationError, e:
+        abort(e)
+
+    #
+    # Do a quick sanity check that arguments look like principal
+    # identifiers.
+    #
+    for arg in args:
+        try:
+            principalForPrincipalID(arg, checkOnly=True)
+        except ValueError, e:
+            abort(e)
+
+    #
+    # Start the reactor
+    #
+    reactor.callLater(0, run, args, actions)
+    reactor.run()
+
+ at inlineCallbacks
+def run(principalIDs, actions):
+    try:
+        #
+        # Connect to memcached, notifications
+        #
+        memcachepool.installPools(
+            config.Memcached.Pools,
+            config.Memcached.MaxClients
+        )
+        if config.Notifications.Enabled:
+            installNotificationClient(
+                config.Notifications.InternalNotificationHost,
+                config.Notifications.InternalNotificationPort,
+            )
+
+        #
+        # Wire up the resource hierarchy
+        #
+        principalCollection = config.directory.getPrincipalCollection()
+        root = RootResource(
+            config.DocumentRoot,
+            principalCollections=(principalCollection,),
+        )
+        root.putChild("principals", principalCollection)
+        calendarCollection = CalendarHomeProvisioningFile(
+            os.path.join(config.DocumentRoot, "calendars"),
+            config.directory, "/calendars/",
+        )
+        root.putChild("calendars", calendarCollection)
+
+        #
+        # Wrap root resource
+        #
+        # FIXME: not a fan -wsanchez
+        #root = ResourceWrapper(root)
+
+        for principalID in principalIDs:
+            # Resolve the given principal IDs to principals
+            try:
+                principal = principalForPrincipalID(principalID)
+            except ValueError:
+                principal = None
+
+            if principal is None:
+                sys.stderr.write("Invalid principal ID: %s\n" % (principalID,))
+                continue
+
+            # Performs requested actions
+            for action in actions:
+                (yield action[0](principal, *action[1:]))
+                print ""
+
+    finally:
+        #
+        # Stop the reactor
+        #
+        reactor.stop()
+
+def principalForPrincipalID(principalID, checkOnly=False, directory=None):
+    
+    # Allow a directory parameter to be passed in, but default to config.directory
+    # But config.directory isn't set right away, so only use it when we're doing more 
+    # than checking.
+    if not checkOnly and not directory:
+        directory = config.directory
+
+    if principalID.startswith("/"):
+        raise ValueError("Can't resolve paths yet")
+
+        if checkOnly:
+            return None
+
+    if principalID.startswith("("):
+        try:
+            i = principalID.index(")")
+
+            if checkOnly:
+                return None
+
+            recordType = principalID[1:i]
+            shortName = principalID[i+1:]
+
+            if not recordType or not shortName or "(" in recordType:
+                raise ValueError()
+
+            return directory.principalCollection.principalForShortName(recordType, shortName)
+
+        except ValueError:
+            pass
+
+    if ":" in principalID:
+        if checkOnly:
+            return None
+
+        recordType, shortName = principalID.split(":", 1)
+
+        return directory.principalCollection.principalForShortName(recordType, shortName)
+
+    try:
+        guid = UUID(principalID)
+
+        if checkOnly:
+            return None
+
+        return directory.principalCollection.principalForUID(guid)
+    except ValueError:
+        pass
+
+    raise ValueError("Invalid principal identifier: %s" % (principalID,))
+
+def proxySubprincipal(principal, proxyType):
+    return principal.getChild("calendar-proxy-" + proxyType)
+
+ at inlineCallbacks
+def action_readProperty(resource, qname):
+    property = (yield resource.readProperty(qname, None))
+    print "%r on %s:" % (qname2sname(qname), resource)
+    print ""
+    print property.toxml()
+
+ at inlineCallbacks
+def action_listProxies(principal, *proxyTypes):
+    for proxyType in proxyTypes:
+        subPrincipal = proxySubprincipal(principal, proxyType)
+        if subPrincipal is None:
+            print "No %s proxies for %s" % (proxyType, principal)
+            continue
+
+        membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+
+        if membersProperty.children:
+            print "%s proxies for %s:" % (
+                {"read": "Read-only", "write": "Read/write"}[proxyType],
+                principal,
+            )
+            for member in membersProperty.children:
+                print " *", member
+        else:
+            print "No %s proxies for %s" % (proxyType, principal)
+
+ at inlineCallbacks
+def action_addProxy(principal, proxyType, *proxyIDs):
+    for proxyID in proxyIDs:
+        proxyPrincipal = principalForPrincipalID(proxyID)
+        (yield action_addProxyPrincipal(principal, proxyType, proxyPrincipal))
+
+ at inlineCallbacks
+def action_addProxyPrincipal(principal, proxyType, proxyPrincipal):
+    proxyURL = proxyPrincipal.url()
+
+    subPrincipal = proxySubprincipal(principal, proxyType)
+    if subPrincipal is None:
+        sys.stderr.write("Unable to edit %s proxies for %s\n" % (proxyType, principal))
+        return
+
+    membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+
+    for memberURL in membersProperty.children:
+        if str(memberURL) == proxyURL:
+            print "%s is already a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
+            break
+    else:
+        memberURLs = list(membersProperty.children)
+        memberURLs.append(davxml.HRef(proxyURL))
+        membersProperty = davxml.GroupMemberSet(*memberURLs)
+        (yield subPrincipal.writeProperty(membersProperty, None))
+        print "Added %s as a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
+
+    proxyTypes = ["read", "write"]
+    proxyTypes.remove(proxyType)
+
+    (yield action_removeProxyPrincipal(principal, proxyPrincipal, proxyTypes=proxyTypes))
+
+ at inlineCallbacks
+def action_removeProxy(principal, *proxyIDs, **kwargs):
+    for proxyID in proxyIDs:
+        proxyPrincipal = principalForPrincipalID(proxyID)
+        (yield action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs))
+
+ at inlineCallbacks
+def action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs):
+    proxyTypes = kwargs.get("proxyTypes", ("read", "write"))
+    for proxyType in proxyTypes:
+        proxyURL = proxyPrincipal.url()
+
+        subPrincipal = proxySubprincipal(principal, proxyType)
+        if subPrincipal is None:
+            sys.stderr.write("Unable to edit %s proxies for %s\n" % (proxyType, principal))
+            continue
+
+        membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+
+        memberURLs = [
+            m for m in membersProperty.children
+            if str(m) != proxyURL
+        ]
+
+        if len(memberURLs) == len(membersProperty.children):
+            # No change
+            continue
+
+        membersProperty = davxml.GroupMemberSet(*memberURLs)
+        (yield subPrincipal.writeProperty(membersProperty, None))
+        print "Removed %s as a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
+
+ at inlineCallbacks
+def action_setAutoSchedule(principal, autoSchedule):
+    if autoSchedule and principal.record.recordType in ("users", "groups"):
+        print "Enabling auto-schedule for %s is not allowed." % (principal,)
+    else:
+        print "Setting auto-schedule to %s for %s" % (
+            { True: "true", False: "false" }[autoSchedule],
+            principal,
+        )
+        principal.setAutoSchedule(autoSchedule)
+
+ at inlineCallbacks
+def action_getAutoSchedule(principal):
+    autoSchedule = principal.getAutoSchedule()
+    print "Autoschedule for %s is %s" % (
+        principal,
+        { True: "true", False: "false" }[autoSchedule],
+    )
+
+def abort(msg, status=1):
+    sys.stdout.write("%s\n" % (msg,))
+    try:
+        reactor.stop()
+    except RuntimeError:
+        pass
+    sys.exit(status)
+
+if __name__ == "__main__":
+    main()

Deleted: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/util.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,137 +0,0 @@
-##
-# Copyright (c) 2008-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-__all__ = [
-    "loadConfig",
-    "getDirectory",
-    "UsageError",
-    "booleanArgument",
-]
-
-import os
-
-from twisted.python.reflect import namedClass
-
-import socket
-from twistedcaldav.config import config, ConfigurationError
-from twistedcaldav.directory import augment, calendaruserproxy
-from twistedcaldav.directory.directory import DirectoryService
-
-def loadConfig(configFileName):
-    if configFileName is None:
-        configFileName = "/etc/caldav/caldavd.plist"
-
-    if not os.path.isfile(configFileName):
-        raise ConfigurationError("No config file: %s" % (configFileName,))
-
-    config.loadConfig(configFileName)
-
-    return config
-
-def getDirectory():
-    BaseDirectoryService = namedClass(config.DirectoryService.type)
-
-    class MyDirectoryService (BaseDirectoryService):
-        def getPrincipalCollection(self):
-            if not hasattr(self, "_principalCollection"):
-                #
-                # Instantiating a CalendarHomeProvisioningResource with a directory
-                # will register it with the directory (still smells like a hack).
-                #
-                # We need that in order to locate calendar homes via the directory.
-                #
-                from twistedcaldav.static import CalendarHomeProvisioningFile
-                CalendarHomeProvisioningFile(os.path.join(config.DocumentRoot, "calendars"), self, "/calendars/")
-
-                from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-                self._principalCollection = DirectoryPrincipalProvisioningResource("/principals/", self)
-
-            return self._principalCollection
-
-        def setPrincipalCollection(self, coll):
-            # See principal.py line 237:  self.directory.principalCollection = self
-            pass
-
-        principalCollection = property(getPrincipalCollection, setPrincipalCollection)
-
-        def calendarHomeForRecord(self, record):
-            principal = self.principalCollection.principalForRecord(record)
-            if principal:
-                try:
-                    return principal.calendarHome()
-                except AttributeError:
-                    pass
-            return None
-
-        def calendarHomeForShortName(self, recordType, shortName):
-            principal = self.principalCollection.principalForShortName(recordType, shortName)
-            if principal:
-                return principal.calendarHome()
-            return None
-
-        def principalForCalendarUserAddress(self, cua):
-            return self.principalCollection.principalForCalendarUserAddress(cua)
-
-    # Load augment/proxy db classes now
-    augmentClass = namedClass(config.AugmentService.type)
-    augment.AugmentService = augmentClass(**config.AugmentService.params)
-
-    proxydbClass = namedClass(config.ProxyDBService.type)
-    calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
-
-    # Wait for directory service to become available
-    directory = MyDirectoryService(**config.DirectoryService["params"])
-
-    return directory
-
-class DummyDirectoryService (DirectoryService):
-    realmName = ""
-    baseGUID = "51856FD4-5023-4890-94FE-4356C4AAC3E4"
-    def recordTypes(self): return ()
-    def listRecords(self): return ()
-    def recordWithShortName(self): return None
-
-class UsageError (StandardError):
-    pass
-
-def booleanArgument(arg):
-    if   arg in ("true",  "yes", "yup",  "uh-huh", "1", "t", "y"):
-        return True
-    elif arg in ("false", "no",  "nope", "nuh-uh", "0", "f", "n"):
-        return False
-    else:
-        raise ValueError("Not a boolean: %s" % (arg,))
-
-
-
-
-
-def autoDisableMemcached(config):
-    """
-    If memcached is not running, set config.Memcached.ClientEnabled to False
-    """
-
-    if not config.Memcached.Pools.Default.ClientEnabled:
-        return
-
-    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-
-    try:
-        s.connect((config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port))
-        s.close()
-
-    except socket.error:
-        config.Memcached.Pools.Default.ClientEnabled = False

Copied: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/util.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,137 @@
+##
+# Copyright (c) 2008-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+__all__ = [
+    "loadConfig",
+    "getDirectory",
+    "UsageError",
+    "booleanArgument",
+]
+
+import os
+
+from twisted.python.reflect import namedClass
+
+import socket
+from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.directory import augment, calendaruserproxy
+from twistedcaldav.directory.directory import DirectoryService
+
+def loadConfig(configFileName):
+    if configFileName is None:
+        configFileName = "/etc/caldav/caldavd.plist"
+
+    if not os.path.isfile(configFileName):
+        raise ConfigurationError("No config file: %s" % (configFileName,))
+
+    config.loadConfig(configFileName)
+
+    return config
+
+def getDirectory():
+    BaseDirectoryService = namedClass(config.DirectoryService.type)
+
+    class MyDirectoryService (BaseDirectoryService):
+        def getPrincipalCollection(self):
+            if not hasattr(self, "_principalCollection"):
+                #
+                # Instantiating a CalendarHomeProvisioningResource with a directory
+                # will register it with the directory (still smells like a hack).
+                #
+                # We need that in order to locate calendar homes via the directory.
+                #
+                from twistedcaldav.static import CalendarHomeProvisioningFile
+                CalendarHomeProvisioningFile(os.path.join(config.DocumentRoot, "calendars"), self, "/calendars/")
+
+                from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
+                self._principalCollection = DirectoryPrincipalProvisioningResource("/principals/", self)
+
+            return self._principalCollection
+
+        def setPrincipalCollection(self, coll):
+            # See principal.py line 237:  self.directory.principalCollection = self
+            pass
+
+        principalCollection = property(getPrincipalCollection, setPrincipalCollection)
+
+        def calendarHomeForRecord(self, record):
+            principal = self.principalCollection.principalForRecord(record)
+            if principal:
+                try:
+                    return principal.calendarHome()
+                except AttributeError:
+                    pass
+            return None
+
+        def calendarHomeForShortName(self, recordType, shortName):
+            principal = self.principalCollection.principalForShortName(recordType, shortName)
+            if principal:
+                return principal.calendarHome()
+            return None
+
+        def principalForCalendarUserAddress(self, cua):
+            return self.principalCollection.principalForCalendarUserAddress(cua)
+
+    # Load augment/proxy db classes now
+    augmentClass = namedClass(config.AugmentService.type)
+    augment.AugmentService = augmentClass(**config.AugmentService.params)
+
+    proxydbClass = namedClass(config.ProxyDBService.type)
+    calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
+
+    # Wait for directory service to become available
+    directory = MyDirectoryService(**config.DirectoryService["params"])
+
+    return directory
+
+class DummyDirectoryService (DirectoryService):
+    realmName = ""
+    baseGUID = "51856FD4-5023-4890-94FE-4356C4AAC3E4"
+    def recordTypes(self): return ()
+    def listRecords(self): return ()
+    def recordWithShortName(self): return None
+
+class UsageError (StandardError):
+    pass
+
+def booleanArgument(arg):
+    if   arg in ("true",  "yes", "yup",  "uh-huh", "1", "t", "y"):
+        return True
+    elif arg in ("false", "no",  "nope", "nuh-uh", "0", "f", "n"):
+        return False
+    else:
+        raise ValueError("Not a boolean: %s" % (arg,))
+
+
+
+
+
+def autoDisableMemcached(config):
+    """
+    If memcached is not running, set config.Memcached.ClientEnabled to False
+    """
+
+    if not config.Memcached.Pools.Default.ClientEnabled:
+        return
+
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+    try:
+        s.connect((config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port))
+        s.close()
+
+    except socket.error:
+        config.Memcached.Pools.Default.ClientEnabled = False

Modified: CalendarServer/branches/users/wsanchez/deployment/conf/accounts-test.xml
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/accounts-test.xml	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/accounts-test.xml	2010-07-01 22:12:06 UTC (rev 5833)
@@ -36,34 +36,24 @@
     <guid>user%02d</guid>
     <password>user%02d</password>
     <name>User %02d</name>
-    <cuaddr>mailto:user%02d at example.com</cuaddr>
   </user>
   <user repeat="10">
     <uid>public%02d</uid>
     <guid>public%02d</guid>
     <password>public%02d</password>
     <name>Public %02d</name>
-    <cuaddr>mailto:public%02d at example.com</cuaddr>
   </user>
   <location repeat="10">
     <uid>location%02d</uid>
     <guid>location%02d</guid>
     <password>location%02d</password>
     <name>Room %02d</name>
-    <auto-schedule/>
   </location>
   <resource repeat="10">
     <uid>resource%02d</uid>
     <guid>resource%02d</guid>
     <password>resource%02d</password>
     <name>Resource %02d</name>
-    <auto-schedule/>
-    <proxies>
-      <member type="users">user01</member>
-    </proxies>
-    <read-only-proxies>
-      <member type="users">user03</member>
-    </read-only-proxies>
   </resource>
   <group>
     <uid>group01</uid>
@@ -82,6 +72,5 @@
     <members>
       <member type="users">user01</member>
     </members>
-    <disable-calendar/>
   </group>
 </accounts>

Modified: CalendarServer/branches/users/wsanchez/deployment/conf/accounts.dtd
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/accounts.dtd	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/accounts.dtd	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 <!--
-Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,16 +19,16 @@
 <!ELEMENT accounts (user*, group*, resource*, location*) >
   <!ATTLIST accounts realm CDATA "">
 
-  <!ELEMENT user (uid, guid, password, name, cuaddr*, disable-calendar?)>
+  <!ELEMENT user (uid*, guid, password?, name?, email-address*)>
     <!ATTLIST user repeat CDATA "1">
 
-  <!ELEMENT group (uid, guid, password, name, members, cuaddr*, disable-calendar?)>
+  <!ELEMENT group (uid*, guid, password?, name?, members)>
     <!ATTLIST group repeat CDATA "1">
 
-  <!ELEMENT resource (uid, guid, password, name, cuaddr*, auto-schedule?, proxies?, read-only-proxies?)>
+  <!ELEMENT resource (uid*, guid, password?, name)>
     <!ATTLIST resource repeat CDATA "1">
 
-  <!ELEMENT location (uid, guid, password, name, cuaddr*, auto-schedule?, proxies?, read-only-proxies?)>
+  <!ELEMENT location (uid*, guid, password?, name)>
     <!ATTLIST location repeat CDATA "1">
 
   <!ELEMENT member (#PCDATA)>
@@ -38,10 +38,6 @@
   <!ELEMENT guid              (#PCDATA)>
   <!ELEMENT password          (#PCDATA)>
   <!ELEMENT name              (#PCDATA)>
-  <!ELEMENT cuaddr            (#PCDATA)>
+  <!ELEMENT email-address     (#PCDATA)>
   <!ELEMENT members           (member*)>
-  <!ELEMENT auto-schedule     EMPTY>
-  <!ELEMENT disable-calendar  EMPTY>
-  <!ELEMENT proxies           (member*)>
-  <!ELEMENT read-only-proxies (member*)>
 >

Modified: CalendarServer/branches/users/wsanchez/deployment/conf/accounts.xml
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/accounts.xml	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/accounts.xml	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -28,7 +28,6 @@
     <uid>test</uid>
     <password>test</password>
     <name>Test User</name>
-    <cuaddr>mailto:testuser at example.com</cuaddr>
   </user>
   <group>
     <uid>users</uid>
@@ -42,10 +41,6 @@
     <uid>mercury</uid>
     <password>mercury</password>
     <name>Mecury Conference Room, Building 1, 2nd Floor</name>
-    <auto-schedule/>
-    <proxies>
-      <member type="users">test</member>
-    </proxies>
   </location>
 </accounts>
 

Copied: CalendarServer/branches/users/wsanchez/deployment/conf/augments-test.xml (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/augments-test.xml)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/augments-test.xml	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/augments-test.xml	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE augments SYSTEM "augments.dtd">
+
+<augments>
+  <record>
+    <guid>admin</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>apprentice</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record repeat="99">
+    <guid>user%02d</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record repeat="10">
+    <guid>public%02d</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record repeat="10">
+    <guid>location%02d</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record repeat="10">
+    <guid>resource%02d</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record repeat="4">
+    <guid>group%02d</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>disabledgroup</guid>
+    <enable>false</enable>
+  </record>
+</augments>

Copied: CalendarServer/branches/users/wsanchez/deployment/conf/augments.dtd (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/augments.dtd)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/augments.dtd	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/augments.dtd	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,27 @@
+<!--
+Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<!ELEMENT augments (record*) >
+
+  <!ELEMENT record (guid, enable, hosted-at?, enable-calendar?, auto-schedule?)>
+    <!ATTLIST record repeat CDATA "1">
+
+  <!ELEMENT guid              (#PCDATA)>
+  <!ELEMENT enable            (#PCDATA)>
+  <!ELEMENT hosted-at         (#PCDATA)>
+  <!ELEMENT enable-calendar   (#PCDATA)>
+  <!ELEMENT auto-schedule     (#PCDATA)>
+>

Modified: CalendarServer/branches/users/wsanchez/deployment/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/caldavd-test.plist	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/caldavd-test.plist	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <!--
-Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -121,64 +121,101 @@
     <dict>
       <key>node</key>
       <string>/Search</string>
-      <key>requireComputerRecord</key>
-      <true/>
+      <key>cacheTimeout</key>
+      <integer>30</integer>
     </dict>
   </dict>
   -->
 
-  <!--  Apache-style Basic Directory Service -->
-  <!--
-  <key>DirectoryService</key>
-  <dict>
-    <key>type</key>
-    <string>twistedcaldav.directory.apache.BasicDirectoryService</string>
-  
-    <key>params</key>
+    <!--
+        Augment service
+
+        Augments for the directory service records to add calendar specific attributes.
+
+        A variety of augment services are available for use.
+        When using a partitioned server, a service that can be accessed from each host will be needed.
+      -->
+
+    <!-- XML File Augment Service -->
+    <key>AugmentService</key>
     <dict>
-      <key>userFile</key>
-      <string>conf/basic</string>
-      <key>groupFile</key>
-      <string>conf/group</string>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentXMLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>xmlFiles</key>
+        <array>
+	      <string>conf/auth/augments-test.xml</string>
+        </array>
+      </dict>
     </dict>
-  </dict>
-  -->
+    
+    <!-- Sqlite Augment Service -->
+    <!--
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentSqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>/etc/caldavd/augments.sqlite</string>
+      </dict>
+    </dict>
+     -->
 
-  <!--  Apache-style Digest Directory Service -->
-  <!--
-  <key>DirectoryService</key>
-  <dict>
-    <key>type</key>
-    <string>twistedcaldav.directory.apache.DigestDirectoryService</string>
-  
-    <key>params</key>
+    <!-- PostgreSQL Augment Service -->
+    <!--
+    <key>AugmentService</key>
     <dict>
-      <key>userFile</key>
-      <string>conf/digest</string>
-      <key>groupFile</key>
-      <string>conf/group</string>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentPostgreSQLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>augments</string>
+      </dict>
     </dict>
-  </dict>
-  -->
+     -->
 
-  <!--  SQL Directory Service -->
-  <!--
-  <key>DirectoryService</key>
-  <dict>
-    <key>type</key>
-    <string>twistedcaldav.directory.sqldb.SQLDirectoryService</string>
-  
-    <key>params</key>
+    <!-- Sqlite ProxyDB Service -->
+    <key>ProxyDBService</key>
     <dict>
-      <key>dbParentPath</key>
-      <string>twistedcaldav/test/data/</string>
-      <key>xmlFile</key>
-      <string>conf/accounts-test.xml</string>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>data/proxies.sqlite</string>
+      </dict>
     </dict>
-  </dict>
-  -->
 
+    <!-- PostgreSQL ProxyDB Service -->
+    <!--
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>proxies</string>
+      </dict>
+    </dict>
+     -->
 
+	<key>ProxyLoadFromFile</key>
+    <string>conf/auth/proxies-test.xml</string>
+
   <!--
     Special principals
 
@@ -415,6 +452,39 @@
   </dict>
 
 
+    <!--
+        Server-to-server protocol
+      -->
+
+    <key>Scheduling</key>
+    <dict>
+
+      <!-- CalDAV protocol options -->
+      <key>CalDAV</key>
+      <dict>
+        <key>EmailDomain</key>
+        <string></string>
+        <key>HTTPDomain</key>
+        <string></string>
+        <key>AddressPatterns</key>
+        <array>
+        </array>
+      </dict>
+
+      <!-- iSchedule protocol options -->
+      <key>iSchedule</key>
+      <dict>
+        <key>Enabled</key>
+        <false/>
+        <key>AddressPatterns</key>
+        <array>
+        </array>
+        <key>Servers</key>
+        <string>conf/servertoserver-test.xml</string>
+      </dict>
+
+    </dict>
+
   <!--
     Notifications
   -->
@@ -511,10 +581,6 @@
   <!-- Support for Memcached -->
   <key>Memcached</key>
   <dict>
-    <key>ServerEnabled</key>
-    <true/>
-    <key>ClientEnabled</key>
-    <true/>
     <key>MaxClients</key>
     <integer>5</integer>
     <key>memcached</key>

Modified: CalendarServer/branches/users/wsanchez/deployment/conf/caldavd.plist
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/caldavd.plist	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/caldavd.plist	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <!--
-    Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+    Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 
     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
@@ -120,13 +120,98 @@
       <dict>
         <key>node</key>
         <string>/Search</string>
-        <key>requireComputerRecord</key>
-        <true/>
+        <key>cacheTimeout</key>
+        <integer>30</integer>
       </dict>
     </dict>
 
+    <!--
+        Augment service
 
+        Augments for the directory service records to add calendar specific attributes.
+
+        A variety of augment services are available for use.
+        When using a partitioned server, a service that can be accessed from each host will be needed.
+      -->
+
+    <!-- XML File Augment Service -->
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentXMLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>xmlFiles</key>
+        <array>
+	      <string>/etc/caldavd/augments.xml</string>
+        </array>
+      </dict>
+    </dict>
+    
+    <!-- Sqlite Augment Service -->
     <!--
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentSqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>/etc/caldavd/augments.sqlite</string>
+      </dict>
+    </dict>
+     -->
+
+    <!-- PostgreSQL Augment Service -->
+    <!--
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentPostgreSQLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>augments</string>
+      </dict>
+    </dict>
+     -->
+
+    <!-- Sqlite ProxyDB Service -->
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>/etc/caldavd/proxies.sqlite</string>
+      </dict>
+    </dict>
+
+    <!-- PostgreSQL ProxyDB Service -->
+    <!--
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>proxies</string>
+      </dict>
+    </dict>
+     -->
+
+    <!--
         Special principals
 
         These principals are granted special access and/or perform
@@ -281,6 +366,40 @@
 
 
     <!--
+        Server-to-server protocol
+      -->
+
+    <key>Scheduling</key>
+    <dict>
+
+      <!-- CalDAV protocol options -->
+      <key>CalDAV</key>
+      <dict>
+        <key>EmailDomain</key>
+        <string></string>
+        <key>HTTPDomain</key>
+        <string></string>
+        <key>AddressPatterns</key>
+        <array>
+        </array>
+      </dict>
+
+      <!-- iSchedule protocol options -->
+      <key>iSchedule</key>
+      <dict>
+        <key>Enabled</key>
+        <false/>
+        <key>AddressPatterns</key>
+        <array>
+        </array>
+        <key>Servers</key>
+        <string>/etc/caldavd/servertoserver.xml</string>
+      </dict>
+
+    </dict>
+
+
+    <!--
         Notifications
       -->
 

Copied: CalendarServer/branches/users/wsanchez/deployment/conf/partitions-test.plist (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/partitions-test.plist)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/partitions-test.plist	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/partitions-test.plist	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2009 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>partitions</key>
+  <array>
+    <dict>
+      <key>uid</key>
+      <string>00001</string>
+      <key>url</key>
+      <string>http://localhost:8008</string>
+    </dict>
+    <dict>
+      <key>uid</key>
+      <string>00002</string>
+      <key>url</key>
+      <string>http://localhost:8108</string>
+    </dict>
+  </array>
+</dict>
+</plist>

Copied: CalendarServer/branches/users/wsanchez/deployment/conf/partitions.plist (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/partitions.plist)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/partitions.plist	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/partitions.plist	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2009 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>partitions</key>
+  <array>
+  <!--
+    <dict>
+      <key>uid</key>
+      <string>00001</string>
+      <key>url</key>
+      <string>http://localhost:8008</string>
+    </dict>
+  -->
+  </array>
+</dict>
+</plist>

Copied: CalendarServer/branches/users/wsanchez/deployment/conf/proxies-test.xml (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/proxies-test.xml)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/proxies-test.xml	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/proxies-test.xml	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE proxies SYSTEM "proxies.dtd">
+
+<proxies>
+  <record repeat="10">
+    <guid>resource%02d</guid>
+    <proxies>
+      <member>user01</member>
+    </proxies>
+    <read-only-proxies>
+      <member>user03</member>
+    </read-only-proxies>
+  </record>
+</proxies>

Copied: CalendarServer/branches/users/wsanchez/deployment/conf/proxies.dtd (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/proxies.dtd)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/proxies.dtd	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/proxies.dtd	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,26 @@
+<!--
+Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<!ELEMENT proxies (record*) >
+
+  <!ELEMENT record (guid, proxies?, read-only-proxies?)>
+    <!ATTLIST record repeat CDATA "1">
+
+  <!ELEMENT guid              (#PCDATA)>
+  <!ELEMENT proxies           (member*)>
+  <!ELEMENT read-only-proxies (member*)>
+  <!ELEMENT member            (#PCDATA)>
+>

Copied: CalendarServer/branches/users/wsanchez/deployment/conf/servertoserver-test.xml (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/servertoserver-test.xml)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/servertoserver-test.xml	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/servertoserver-test.xml	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE servers SYSTEM "servertoserver.dtd">
+
+<servers>
+  <server>
+    <uri>https://localhost:8543/inbox</uri>
+    <allow-requests-from/>
+    <allow-requests-to/>
+    <domains>
+    	<domain>example.org</domain>
+    </domains>
+    <hosts>
+    	<host>127.0.0.1</host>
+    </hosts>
+  </server>
+</servers>

Copied: CalendarServer/branches/users/wsanchez/deployment/conf/servertoserver.dtd (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/servertoserver.dtd)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/servertoserver.dtd	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/servertoserver.dtd	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,33 @@
+<!--
+Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<!ELEMENT servers (server*) >
+
+	<!ELEMENT server (uri, authentication?, allow-requests-from, allow-requests-to, domains?, hosts?) >
+
+		<!ELEMENT uri (#PCDATA) >
+		<!ELEMENT authentication (user, password) >
+		    <!ATTLIST authentication type (basic) "">
+		    <!ELEMENT user (#PCDATA) >
+		    <!ELEMENT password (#PCDATA) >
+
+		<!ELEMENT allow-requests-from EMPTY >
+		<!ELEMENT allow-requests-to EMPTY >
+		<!ELEMENT domains (domain*) >
+			<!ELEMENT domain (#PCDATA) >
+		<!ELEMENT hosts (host*) >
+			<!ELEMENT host (#PCDATA) >
+			
\ No newline at end of file

Copied: CalendarServer/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.client.http.patch (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/lib-patches/Twisted/twisted.web2.client.http.patch)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.client.http.patch	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.client.http.patch	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,26 @@
+Index: twisted/web2/client/http.py
+===================================================================
+--- twisted/web2/client/http.py	(revision 19773)
++++ twisted/web2/client/http.py	(working copy)
+@@ -127,15 +127,16 @@
+ 
+     def _error(self, err):
+         self.abortParse()
+-        self.responseDefer.errback(err)
++        if hasattr(self, 'stream') and self.stream is not None:
++            self.stream.finish(err)
++        else:
++            self.responseDefer.errback(err)
+ 
+     def _abortWithError(self, errcode, text):
+-        self.abortParse()
+-        self.responseDefer.errback(ProtocolError(text))
++        self._error(ProtocolError(text))
+ 
+     def connectionLost(self, reason):
+-        ### FIXME!
+-        pass
++        self._error(reason)
+     
+     def gotInitialLine(self, initialLine):
+         parts = initialLine.split(' ', 2)

Modified: CalendarServer/branches/users/wsanchez/deployment/memcacheclient.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/memcacheclient.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/memcacheclient.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -124,7 +124,7 @@
                  pickler=pickle.Pickler, unpickler=pickle.Unpickler,
                  pload=None, pid=None):
 
-        if config.Memcached.ClientEnabled:
+        if config.Memcached.Pools.Default.ClientEnabled:
             return Client(servers, debug=debug, pickleProtocol=pickleProtocol,
                 pickler=pickler, unpickler=unpickler, pload=pload, pid=pid)
         elif cls.allowTestCache:

Modified: CalendarServer/branches/users/wsanchez/deployment/run
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/run	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/run	2010-07-01 22:12:06 UTC (rev 5833)
@@ -622,7 +622,7 @@
   #
   libevent="${top}/libevent-1.4.8-stable"
 
-  www_get "libevent" "${libevent}" http://monkey.org/~provos/libevent-1.4.8-stable.tar.gz
+  www_get "libevent" "${libevent}" http://monkey.org/~provos/libevent-1.4.8-stable.tar.gz;
 
   if "${do_setup}" && (
     "${force_setup}" || [ ! -d "${libevent}/_root" ]
@@ -661,7 +661,43 @@
   export PATH="${PATH}:${top}/memcached-1.2.6/_root/bin";
 fi;
 
+if ! type psql >& /dev/null; then
+  #
+  # postgresql
+  #
+  postgresql="${top}/postgresql-8.4.1"
+
+  www_get "postgresql" "${postgresql}" http://wwwmaster.postgresql.org/redir/198/h/source/v8.4.1/postgresql-8.4.1.tar.bz2;
+
+  if "${do_setup}" && (
+    "${force_setup}" || [ ! -d "${postgresql}/_root" ]
+  ); then
+    echo "";
+    echo "Building postgresql...";
+    cd "${postgresql}";
+    ./configure --prefix="${postgresql}/_root";
+    make;
+    make install;
+  fi;
+
+  export PATH="${PATH:-}:${postgresql}/_root/bin";
+fi;
+
 #
+# PyGreSQL
+#
+
+if ! py_have_module pgdb; then
+  PyGreSQL="${top}/PyGreSQL-4.0";
+
+  www_get "PyGreSQL" "${PyGreSQL}" ftp://ftp.pygresql.org/pub/distrib/PyGreSQL.tgz;
+  py_build "PyGreSQL" "${PyGreSQL}" false;
+  py_install "PyGreSQL" "${PyGreSQL}";
+
+  export PYTHONPATH="${PYTHONPATH}:${PyGreSQL}/build/${py_platform_libdir}";
+fi;
+
+#
 # Twisted
 #
 

Modified: CalendarServer/branches/users/wsanchez/deployment/support/patchapply
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/support/patchapply	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/support/patchapply	2010-07-01 22:12:06 UTC (rev 5833)
@@ -26,24 +26,37 @@
 #
 
 #projects = ("Twisted", "vobject", "dateutil", "xattr")
-projects = ("Twisted", "vobject",)
+#projects = (("Twisted-4", "Twisted"), ("vobject-2","vobject",),)
+projects = (("Twisted-4", "Twisted"),)
+
 cwd = os.getcwd()
 libpatches = os.path.join(cwd, "lib-patches")
 
 cmd = "/usr/bin/patch"
 
 def applypatch(project, patch):
-    stat = os.system("%s -s -d ../%s/ -p0 --forward --dry-run -i %s > /dev/null" % (cmd, project, patch, ))
+
+    if isinstance(project, tuple):
+        project_path, project_name = project
+    else:
+        project_path = project_name = project
+
+    stat = os.system("%s -s -d ../%s/ -p0 --forward --dry-run -i %s > /dev/null" % (cmd, project_path, patch, ))
     if stat == 0:
-        print "+++ Patching %s with %s" % (project, patch[len(cwd) + 1:],)
-        os.system("%s -s -d ../%s/ -p0 --forward -i %s" % (cmd, project, patch, ))
+        print "+++ Patching %s with %s" % (project_path, patch[len(cwd) + 1:],)
+        os.system("%s -s -d ../%s/ -p0 --forward -i %s" % (cmd, project_path, patch, ))
     else:
-        print "*** Failed to patch %s with %s" % (project, patch[len(cwd) + 1:],)
+        print "*** Failed to patch %s with %s" % (project_path, patch[len(cwd) + 1:],)
 
 def applypatches(project):
     
+    if isinstance(project, tuple):
+        project_path, project_name = project
+    else:
+        project_path = project_name = project
+
     # Iterate over each patch file in the patches directory
-    path = os.path.join(libpatches, project)
+    path = os.path.join(libpatches, project_name)
     for file in os.listdir(path):
         fpath = os.path.join(path, file)
         if os.path.isfile(fpath) and fpath.endswith(".patch"):

Deleted: CalendarServer/branches/users/wsanchez/deployment/twext/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/__init__.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to the Twisted Framework.
-"""

Copied: CalendarServer/branches/users/wsanchez/deployment/twext/__init__.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/__init__.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twext/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to the Twisted Framework.
+"""

Deleted: CalendarServer/branches/users/wsanchez/deployment/twext/python/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/python/__init__.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/python/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to twisted.python.
-"""

Copied: CalendarServer/branches/users/wsanchez/deployment/twext/python/__init__.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/python/__init__.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twext/python/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/python/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to twisted.python.
+"""

Deleted: CalendarServer/branches/users/wsanchez/deployment/twext/python/log.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/python/log.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/python/log.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,49 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to twisted.python.log.
-"""
-
-from sys import stdout, stderr
-
-from twisted.python.log import addObserver, removeObserver
-
-class StandardIOObserver (object):
-    """
-    Log observer that writes to standard I/O.
-    """
-    def emit(self, eventDict):
-        text = None
-
-        if eventDict["isError"]:
-            output = stderr
-            if "failure" in eventDict:
-                text = eventDict["failure"].getTraceback()
-        else:
-            output = stdout
-
-        if not text:
-            text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
-
-        output.write(text)
-        output.flush()
-
-    def start(self):
-        addObserver(self.emit)
-
-    def stop(self):
-        removeObserver(self.emit)

Copied: CalendarServer/branches/users/wsanchez/deployment/twext/python/log.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/python/log.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twext/python/log.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/python/log.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,49 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to twisted.python.log.
+"""
+
+from sys import stdout, stderr
+
+from twisted.python.log import addObserver, removeObserver
+
+class StandardIOObserver (object):
+    """
+    Log observer that writes to standard I/O.
+    """
+    def emit(self, eventDict):
+        text = None
+
+        if eventDict["isError"]:
+            output = stderr
+            if "failure" in eventDict:
+                text = eventDict["failure"].getTraceback()
+        else:
+            output = stdout
+
+        if not text:
+            text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
+
+        output.write(text)
+        output.flush()
+
+    def start(self):
+        addObserver(self.emit)
+
+    def stop(self):
+        removeObserver(self.emit)

Deleted: CalendarServer/branches/users/wsanchez/deployment/twext/web2/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/__init__.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/web2/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to twisted.web2
-"""

Copied: CalendarServer/branches/users/wsanchez/deployment/twext/web2/__init__.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/__init__.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twext/web2/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/web2/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to twisted.web2
+"""

Deleted: CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/dav/__init__.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to twisted.web2.dav
-"""

Copied: CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/__init__.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/dav/__init__.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to twisted.web2.dav
+"""

Deleted: CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/davxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/dav/davxml.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/davxml.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,122 +0,0 @@
-##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to twisted.web2.dav.davxml
-"""
-
-__all__ = [
-    "sname2qname",
-    "qname2sname",
-    "ErrorResponse",
-]
-
-from twisted.web2.http import Response
-from twisted.web2.dav.http import ErrorResponse as SuperErrorResponse
-from twisted.web2.dav.davxml import twisted_dav_namespace, WebDAVTextElement
-from twisted.web2.dav.davxml import WebDAVUnknownElement, Error
-from twisted.web2.http_headers import MimeType
-
-
-def sname2qname(sname):
-    """
-    Convert an sname into a qname.
-
-    That is, parse a property name string (eg: C{"{DAV:}displayname"})
-    into a tuple (eg: C{("DAV:", "displayname")}).
-
-    @raise ValueError is input is not valid. Note, however, that this
-    function does not attempt to fully validate C{sname}.
-    """
-    def raiseIf(condition):
-        if condition:
-            raise ValueError("Invalid sname: %s" % (sname,))
-
-    raiseIf(not sname.startswith("{"))
-
-    try:
-        i = sname.index("}")
-    except ValueError:
-        raiseIf(True)
-
-    namespace = sname[1:i]
-    name = sname [i+1:]
-
-    raiseIf("{" in namespace or not name)
-
-    return namespace, name
-
-def qname2sname(qname):
-    """
-    Convert a qname into an sname.
-    """
-    try:
-        return "{%s}%s" % qname
-    except TypeError:
-        raise ValueError("Invalid qname: %r" % (qname,))
-
-
-
-
-
-class ErrorDescription(WebDAVTextElement):
-    """
-    The human-readable description of a failed precondition
-    """
-    namespace = twisted_dav_namespace
-    name = "error-description"
-    protected = True
-
-
-class ErrorResponse(SuperErrorResponse):
-    """
-    A L{Response} object which contains a status code and a L{davxml.Error}
-    element.
-    Renders itself as a DAV:error XML document.
-    """
-    error = None
-
-    def __init__(self, code, error, description=None):
-        """
-        @param code: a response code.
-        @param error: an L{davxml.WebDAVElement} identifying the error, or a
-            tuple C{(namespace, name)} with which to create an empty element
-            denoting the error.  (The latter is useful in the case of
-            preconditions ans postconditions, not all of which have defined
-            XML element classes.)
-        @param description: an optional string that, if present, will get
-            wrapped in a (twisted_dav_namespace, error-description) element.
-        """
-        if type(error) is tuple:
-            xml_namespace, xml_name = error
-            error = WebDAVUnknownElement()
-            error.namespace = xml_namespace
-            error.name = xml_name
-
-        if description:
-            output = Error(error, ErrorDescription(description)).toxml()
-        else:
-            output = Error(error).toxml()
-
-        Response.__init__(self, code=code, stream=output)
-
-        self.headers.setHeader("content-type", MimeType("text", "xml"))
-
-        self.error = error
-
-    def __repr__(self):
-        return "<%s %s %s>" % (self.__class__.__name__, self.code, self.error.sname())
-

Copied: CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/davxml.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/dav/davxml.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/davxml.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twext/web2/dav/davxml.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,122 @@
+##
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to twisted.web2.dav.davxml
+"""
+
+__all__ = [
+    "sname2qname",
+    "qname2sname",
+    "ErrorResponse",
+]
+
+from twisted.web2.http import Response
+from twisted.web2.dav.http import ErrorResponse as SuperErrorResponse
+from twisted.web2.dav.davxml import twisted_dav_namespace, WebDAVTextElement
+from twisted.web2.dav.davxml import WebDAVUnknownElement, Error
+from twisted.web2.http_headers import MimeType
+
+
+def sname2qname(sname):
+    """
+    Convert an sname into a qname.
+
+    That is, parse a property name string (eg: C{"{DAV:}displayname"})
+    into a tuple (eg: C{("DAV:", "displayname")}).
+
+    @raise ValueError is input is not valid. Note, however, that this
+    function does not attempt to fully validate C{sname}.
+    """
+    def raiseIf(condition):
+        if condition:
+            raise ValueError("Invalid sname: %s" % (sname,))
+
+    raiseIf(not sname.startswith("{"))
+
+    try:
+        i = sname.index("}")
+    except ValueError:
+        raiseIf(True)
+
+    namespace = sname[1:i]
+    name = sname [i+1:]
+
+    raiseIf("{" in namespace or not name)
+
+    return namespace, name
+
+def qname2sname(qname):
+    """
+    Convert a qname into an sname.
+    """
+    try:
+        return "{%s}%s" % qname
+    except TypeError:
+        raise ValueError("Invalid qname: %r" % (qname,))
+
+
+
+
+
+class ErrorDescription(WebDAVTextElement):
+    """
+    The human-readable description of a failed precondition
+    """
+    namespace = twisted_dav_namespace
+    name = "error-description"
+    protected = True
+
+
+class ErrorResponse(SuperErrorResponse):
+    """
+    A L{Response} object which contains a status code and a L{davxml.Error}
+    element.
+    Renders itself as a DAV:error XML document.
+    """
+    error = None
+
+    def __init__(self, code, error, description=None):
+        """
+        @param code: a response code.
+        @param error: an L{davxml.WebDAVElement} identifying the error, or a
+            tuple C{(namespace, name)} with which to create an empty element
+            denoting the error.  (The latter is useful in the case of
+            preconditions ans postconditions, not all of which have defined
+            XML element classes.)
+        @param description: an optional string that, if present, will get
+            wrapped in a (twisted_dav_namespace, error-description) element.
+        """
+        if type(error) is tuple:
+            xml_namespace, xml_name = error
+            error = WebDAVUnknownElement()
+            error.namespace = xml_namespace
+            error.name = xml_name
+
+        if description:
+            output = Error(error, ErrorDescription(description)).toxml()
+        else:
+            output = Error(error).toxml()
+
+        Response.__init__(self, code=code, stream=output)
+
+        self.headers.setHeader("content-type", MimeType("text", "xml"))
+
+        self.error = error
+
+    def __repr__(self):
+        return "<%s %s %s>" % (self.__class__.__name__, self.code, self.error.sname())
+

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/cache.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/cache.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/cache.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -28,7 +28,7 @@
 from twisted.web2.stream import MemoryStream
 
 from twistedcaldav.log import LoggingMixIn
-from twistedcaldav.memcachepool import CachePoolUserMixIn
+from twistedcaldav.memcachepool import CachePoolUserMixIn, defaultCachePool
 from twistedcaldav.config import config
 
 
@@ -65,11 +65,11 @@
 
 class MemcacheChangeNotifier(LoggingMixIn, CachePoolUserMixIn):
 
-    def __init__(self, resource, cachePool=None):
+    def __init__(self, resource, cachePool=None, cacheHandle="Default"):
         self._resource = resource
         self._cachePool = cachePool
+        self._cachePoolHandle = cacheHandle
 
-
     def _newCacheToken(self):
         return str(uuid.uuid4())
 
@@ -171,7 +171,7 @@
         self._cachePool = cachePool
 
 
-    def _tokenForURI(self, uri):
+    def _tokenForURI(self, uri, cachePoolHandle=None):
         """
         Get a property store for the given C{uri}.
 
@@ -179,13 +179,16 @@
         @return: A C{str} representing the token for the URI.
         """
 
-        return self.getCachePool().get('cacheToken:%s' % (uri,))
+        if cachePoolHandle:
+            return defaultCachePool(cachePoolHandle).get('cacheToken:%s' % (uri,))
+        else:
+            return self.getCachePool().get('cacheToken:%s' % (uri,))
 
 
     def _getTokens(self, request):
         def _tokensForURIs((pURI, rURI)):
             tokens = []
-            d1 = self._tokenForURI(pURI)
+            d1 = self._tokenForURI(pURI, "PrincipalToken")
             d1.addCallback(tokens.append)
             d1.addCallback(lambda _ign: self._getRecordForURI(pURI, request))
             d1.addCallback(lambda dToken: tokens.append(hash(dToken)))

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/__init__.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,16 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/__init__.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/__init__.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/pool.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/pool.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/pool.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,397 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-__all__ = [
-    "installPools",
-    "installPool",
-    "getHTTPClientPool",
-]
-
-from twisted.internet.address import IPv4Address
-from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
-from twisted.internet.error import ConnectionLost, ConnectionDone, ConnectError
-from twisted.internet.protocol import ClientFactory
-from twisted.internet.ssl import DefaultOpenSSLContextFactory
-from twisted.web2 import responsecode
-from twisted.web2.client import interfaces
-from twisted.web2.client.http import HTTPClientProtocol
-from twisted.web2.dav.util import allDataFromStream
-from twisted.web2.http import StatusResponse, HTTPError
-from twisted.web2.stream import MemoryStream
-from twistedcaldav.log import LoggingMixIn
-import OpenSSL
-import urlparse
-from zope.interface import implements
-
-class PooledHTTPClientFactory(ClientFactory, LoggingMixIn):
-    """
-    A client factory for HTTPClient that notifies a pool of it's state. It the connection
-    fails in the middle of a request it will retry the request.
-
-    @ivar protocol: The current instance of our protocol that we pass
-        to our connectionPool.
-    @ivar connectionPool: A managing connection pool that we notify of events.
-    """
-    protocol = HTTPClientProtocol
-    connectionPool = None
-
-    def __init__(self, manager, reactor):
-        self.manager = manager
-        self.reactor = reactor
-        self.instance = None
-        self.onConnect = Deferred()
-        self.afterConnect = Deferred()
-
-    def clientConnectionLost(self, connector, reason):
-        """
-        Notify the connectionPool that we've lost our connection.
-        """
-
-        if hasattr(self, "afterConnect"):
-            self.reactor.callLater(0, self.afterConnect.errback, reason)
-            del self.afterConnect
-
-        if self.connectionPool.shutdown_requested:
-            # The reactor is stopping; don't reconnect
-            return
-
-    def clientConnectionFailed(self, connector, reason):
-        """
-        Notify the connectionPool that we're unable to connect
-        """
-        if hasattr(self, "onConnect"):
-            self.reactor.callLater(0, self.onConnect.errback, reason)
-            del self.onConnect
-        elif hasattr(self, "afterConnect"):
-            self.reactor.callLater(0, self.afterConnect.errback, reason)
-            del self.afterConnect
-
-    def buildProtocol(self, addr):
-        self.instance = self.protocol(manager=self.manager)
-        self.reactor.callLater(0, self.onConnect.callback, self.instance)
-        del self.onConnect
-        return self.instance
-
-class HTTPClientPool(LoggingMixIn):
-    """
-    A connection pool for HTTPClientProtocol instances.
-
-    @ivar clientFactory: The L{ClientFactory} implementation that will be used
-        for each protocol.
-
-    @ivar _maxClients: A C{int} indicating the maximum number of clients.
-    @ivar _serverAddress: An L{IAddress} provider indicating the server to
-        connect to.  (Only L{IPv4Address} currently supported.)
-    @ivar _reactor: The L{IReactorTCP} provider used to initiate new
-        connections.
-
-    @ivar _busyClients: A C{set} that contains all currently busy clients.
-    @ivar _freeClients: A C{set} that contains all currently free clients.
-    @ivar _pendingConnects: A C{int} indicating how many connections are in
-        progress.
-    """
-
-    implements(interfaces.IHTTPClientManager)
-
-    clientFactory = PooledHTTPClientFactory
-    maxRetries = 2
-
-    def __init__(self, name, scheme, serverAddress, maxClients=5, reactor=None):
-        """
-        @param serverAddress: An L{IPv4Address} indicating the server to
-            connect to.
-        @param maxClients: A C{int} indicating the maximum number of clients.
-        @param reactor: An L{IReactorTCP{ provider used to initiate new
-            connections.
-        """
-        
-        self._name = name
-        self._scheme = scheme
-        self._serverAddress = serverAddress
-        self._maxClients = maxClients
-
-        if reactor is None:
-            from twisted.internet import reactor
-        self._reactor = reactor
-
-        self.shutdown_deferred = None
-        self.shutdown_requested = False
-        reactor.addSystemEventTrigger('before', 'shutdown', self._shutdownCallback)
-
-        self._busyClients = set([])
-        self._freeClients = set([])
-        self._pendingConnects = 0
-        self._pendingRequests = []
-
-    def _isIdle(self):
-        return (
-            len(self._busyClients) == 0 and
-            len(self._pendingRequests) == 0 and
-            self._pendingConnects == 0
-        )
-
-    def _shutdownCallback(self):
-        self.shutdown_requested = True
-        if self._isIdle():
-            return None
-        self.shutdown_deferred = Deferred()
-        return self.shutdown_deferred
-
-    def _newClientConnection(self):
-        """
-        Create a new client connection.
-
-        @return: A L{Deferred} that fires with the L{IProtocol} instance.
-        """
-        self._pendingConnects += 1
-
-        self.log_debug("Initiating new client connection to: %s" % (self._serverAddress,))
-        self._logClientStats()
-
-        factory = self.clientFactory(self, self._reactor)
-        factory.connectionPool = self
-
-        if self._scheme == "https":
-            from twistedcaldav.config import config
-            context = DefaultOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, sslmethod=OpenSSL.SSL.SSLv3_METHOD)
-            self._reactor.connectSSL(self._serverAddress.host, self._serverAddress.port, factory, context)
-        elif self._scheme == "http":
-            self._reactor.connectTCP(self._serverAddress.host, self._serverAddress.port, factory)
-        else:
-            raise ValueError("URL scheme for client pool not supported")
-
-        def _doneOK(client):
-            self._pendingConnects -= 1
-
-            def _goneClientAfterError(f, client):
-                f.trap(ConnectionLost, ConnectionDone, ConnectError)
-
-            d2 = factory.afterConnect
-            d2.addErrback(_goneClientAfterError, client)
-            return client
-
-        def _doneError(result):
-            self._pendingConnects -= 1
-            return result
-
-        d = factory.onConnect
-        d.addCallbacks(_doneOK, _doneError)
-        
-        return d
-
-    def _performRequestOnClient(self, client, request, *args, **kwargs):
-        """
-        Perform the given request on the given client.
-
-        @param client: A L{PooledMemCacheProtocol} that will be used to perform
-            the given request.
-
-        @param command: A C{str} representing an attribute of
-            L{MemCacheProtocol}.
-        @parma args: Any positional arguments that should be passed to
-            C{command}.
-        @param kwargs: Any keyword arguments that should be passed to
-            C{command}.
-
-        @return: A L{Deferred} that fires with the result of the given command.
-        """
-
-        return client.submitRequest(request, closeAfter=False)
-
-    @inlineCallbacks
-    def submitRequest(self, request, *args, **kwargs):
-        """
-        Select an available client and perform the given request on it.
-
-        @param command: A C{str} representing an attribute of
-            L{MemCacheProtocol}.
-        @parma args: Any positional arguments that should be passed to
-            C{command}.
-        @param kwargs: Any keyword arguments that should be passed to
-            C{command}.
-
-        @return: A L{Deferred} that fires with the result of the given command.
-        """
-
-        # Since we may need to replay the request we have to read the request.stream
-        # into memory and reset request.stream to use a MemoryStream each time we repeat
-        # the request
-        data = (yield allDataFromStream(request.stream))
-
-        # Try this maxRetries times
-        for ctr in xrange(self.maxRetries + 1):
-            try:
-                request.stream = MemoryStream(data if data is not None else "")
-                request.stream.doStartReading = None
-
-                response = (yield self._submitRequest(request, args, kwargs))
-            except (ConnectionLost, ConnectionDone, ConnectError), e:
-                self.log_error("HTTP pooled client connection error (attempt: %d) - retrying: %s" % (ctr+1, e,))
-                continue
-            
-            # TODO: find the proper cause of these assertions and fix
-            except (AssertionError,), e:
-                self.log_error("HTTP pooled client connection assertion error (attempt: %d) - retrying: %s" % (ctr+1, e,))
-                continue
-            else:
-                returnValue(response)
-        else:
-            self.log_error("HTTP pooled client connection error - exhausted retry attempts.")
-            raise HTTPError(StatusResponse(responsecode.BAD_GATEWAY, "Could not connect to HTTP pooled client host."))
-
-    def _submitRequest(self, request, *args, **kwargs):
-        """
-        Select an available client and perform the given request on it.
-
-        @param command: A C{str} representing an attribute of
-            L{MemCacheProtocol}.
-        @parma args: Any positional arguments that should be passed to
-            C{command}.
-        @param kwargs: Any keyword arguments that should be passed to
-            C{command}.
-
-        @return: A L{Deferred} that fires with the result of the given command.
-        """
-
-        if len(self._freeClients) > 0:
-            d = self._performRequestOnClient(self._freeClients.pop(), request, *args, **kwargs)
-
-        elif len(self._busyClients) + self._pendingConnects >= self._maxClients:
-            d = Deferred()
-            self._pendingRequests.append((d, request, args, kwargs))
-            self.log_debug("Request queued: %s, %r, %r" % (request, args, kwargs))
-            self._logClientStats()
-
-        else:
-            d = self._newClientConnection()
-            d.addCallback(self._performRequestOnClient, request, *args, **kwargs)
-
-        return d
-
-    def _logClientStats(self):
-        self.log_debug("Clients for '%s' #free: %d, #busy: %d, "
-                       "#pending: %d, #queued: %d" % (
-                self._name,
-                len(self._freeClients),
-                len(self._busyClients),
-                self._pendingConnects,
-                len(self._pendingRequests)
-        ))
-
-    def clientBusy(self, client):
-        """
-        Notify that the given client is being used to complete a request.
-
-        @param client: An instance of L{HTTPClientProtocol}
-        """
-        if client in self._freeClients:
-            self._freeClients.remove(client)
-
-        self._busyClients.add(client)
-
-        self.log_debug("Busied client: %r" % (client,))
-        self._logClientStats()
-    
-    def clientIdle(self, client):
-        """
-        Notify that the given client is free to handle more requests.
-
-        @param client: An instance of L{HTTPClientProtocol}.
-        """
-        if client in self._busyClients:
-            self._busyClients.remove(client)
-
-        self._freeClients.add(client)
-
-        if self.shutdown_deferred and self._isIdle():
-            self.shutdown_deferred.callback(None)
-
-        self.log_debug("Freed client: %r" % (client,))
-        self._logClientStats()
-
-        self._processPending()
-
-    def clientPipelining(self, client):
-        pass
-    
-    def clientGone(self, client):
-        """
-        Notify that the given client is to be removed from the pool completely.
-
-        @param client: An instance of L{HTTPClientProtocol}.
-        """
-        if client in self._busyClients:
-            self._busyClients.remove(client)
-
-        elif client in self._freeClients:
-            self._freeClients.remove(client)
-
-        self.log_debug("Removed client: %r" % (client,))
-        self._logClientStats()
-
-        self._processPending()
-
-    def _processPending(self):
-        if len(self._pendingRequests) > 0:
-            d, request, args, kwargs = self._pendingRequests.pop(0)
-
-            self.log_debug("Performing Queued Request: %s, %r, %r" % (
-                    request, args, kwargs))
-            self._logClientStats()
-
-            _ign_d = self._submitRequest(request, *args, **kwargs)
-
-            _ign_d.addCallbacks(d.callback, d.errback)
-
-    def suggestMaxClients(self, maxClients):
-        """
-        Suggest the maximum number of concurrently connected clients.
-
-        @param maxClients: A C{int} indicating how many client connections we
-            should keep open.
-        """
-        self._maxClients = maxClients
-
-_clientPools = {}     # Maps a host:port to a pool object
-
-def installPools(hosts, maxClients=5, reactor=None):
-    
-    for name, url in hosts:
-        installPool(
-            name,
-            url,
-            maxClients,
-            reactor,
-        )
-
-def installPool(name, url, maxClients=5, reactor=None):
-
-    parsedURL = urlparse.urlparse(url)
-    pool = HTTPClientPool(
-        name,
-        parsedURL.scheme,
-        IPv4Address(
-            "TCP",
-            parsedURL.hostname,
-            parsedURL.port,
-        ),
-        maxClients,
-        reactor,
-    )
-    _clientPools[name] = pool
-
-def getHTTPClientPool(name):
-    return _clientPools[name]

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/pool.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/pool.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/pool.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/pool.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,397 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+__all__ = [
+    "installPools",
+    "installPool",
+    "getHTTPClientPool",
+]
+
+from twisted.internet.address import IPv4Address
+from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
+from twisted.internet.error import ConnectionLost, ConnectionDone, ConnectError
+from twisted.internet.protocol import ClientFactory
+from twisted.internet.ssl import DefaultOpenSSLContextFactory
+from twisted.web2 import responsecode
+from twisted.web2.client import interfaces
+from twisted.web2.client.http import HTTPClientProtocol
+from twisted.web2.dav.util import allDataFromStream
+from twisted.web2.http import StatusResponse, HTTPError
+from twisted.web2.stream import MemoryStream
+from twistedcaldav.log import LoggingMixIn
+import OpenSSL
+import urlparse
+from zope.interface import implements
+
+class PooledHTTPClientFactory(ClientFactory, LoggingMixIn):
+    """
+    A client factory for HTTPClient that notifies a pool of it's state. It the connection
+    fails in the middle of a request it will retry the request.
+
+    @ivar protocol: The current instance of our protocol that we pass
+        to our connectionPool.
+    @ivar connectionPool: A managing connection pool that we notify of events.
+    """
+    protocol = HTTPClientProtocol
+    connectionPool = None
+
+    def __init__(self, manager, reactor):
+        self.manager = manager
+        self.reactor = reactor
+        self.instance = None
+        self.onConnect = Deferred()
+        self.afterConnect = Deferred()
+
+    def clientConnectionLost(self, connector, reason):
+        """
+        Notify the connectionPool that we've lost our connection.
+        """
+
+        if hasattr(self, "afterConnect"):
+            self.reactor.callLater(0, self.afterConnect.errback, reason)
+            del self.afterConnect
+
+        if self.connectionPool.shutdown_requested:
+            # The reactor is stopping; don't reconnect
+            return
+
+    def clientConnectionFailed(self, connector, reason):
+        """
+        Notify the connectionPool that we're unable to connect
+        """
+        if hasattr(self, "onConnect"):
+            self.reactor.callLater(0, self.onConnect.errback, reason)
+            del self.onConnect
+        elif hasattr(self, "afterConnect"):
+            self.reactor.callLater(0, self.afterConnect.errback, reason)
+            del self.afterConnect
+
+    def buildProtocol(self, addr):
+        self.instance = self.protocol(manager=self.manager)
+        self.reactor.callLater(0, self.onConnect.callback, self.instance)
+        del self.onConnect
+        return self.instance
+
+class HTTPClientPool(LoggingMixIn):
+    """
+    A connection pool for HTTPClientProtocol instances.
+
+    @ivar clientFactory: The L{ClientFactory} implementation that will be used
+        for each protocol.
+
+    @ivar _maxClients: A C{int} indicating the maximum number of clients.
+    @ivar _serverAddress: An L{IAddress} provider indicating the server to
+        connect to.  (Only L{IPv4Address} currently supported.)
+    @ivar _reactor: The L{IReactorTCP} provider used to initiate new
+        connections.
+
+    @ivar _busyClients: A C{set} that contains all currently busy clients.
+    @ivar _freeClients: A C{set} that contains all currently free clients.
+    @ivar _pendingConnects: A C{int} indicating how many connections are in
+        progress.
+    """
+
+    implements(interfaces.IHTTPClientManager)
+
+    clientFactory = PooledHTTPClientFactory
+    maxRetries = 2
+
+    def __init__(self, name, scheme, serverAddress, maxClients=5, reactor=None):
+        """
+        @param serverAddress: An L{IPv4Address} indicating the server to
+            connect to.
+        @param maxClients: A C{int} indicating the maximum number of clients.
+        @param reactor: An L{IReactorTCP{ provider used to initiate new
+            connections.
+        """
+        
+        self._name = name
+        self._scheme = scheme
+        self._serverAddress = serverAddress
+        self._maxClients = maxClients
+
+        if reactor is None:
+            from twisted.internet import reactor
+        self._reactor = reactor
+
+        self.shutdown_deferred = None
+        self.shutdown_requested = False
+        reactor.addSystemEventTrigger('before', 'shutdown', self._shutdownCallback)
+
+        self._busyClients = set([])
+        self._freeClients = set([])
+        self._pendingConnects = 0
+        self._pendingRequests = []
+
+    def _isIdle(self):
+        return (
+            len(self._busyClients) == 0 and
+            len(self._pendingRequests) == 0 and
+            self._pendingConnects == 0
+        )
+
+    def _shutdownCallback(self):
+        self.shutdown_requested = True
+        if self._isIdle():
+            return None
+        self.shutdown_deferred = Deferred()
+        return self.shutdown_deferred
+
+    def _newClientConnection(self):
+        """
+        Create a new client connection.
+
+        @return: A L{Deferred} that fires with the L{IProtocol} instance.
+        """
+        self._pendingConnects += 1
+
+        self.log_debug("Initiating new client connection to: %s" % (self._serverAddress,))
+        self._logClientStats()
+
+        factory = self.clientFactory(self, self._reactor)
+        factory.connectionPool = self
+
+        if self._scheme == "https":
+            from twistedcaldav.config import config
+            context = DefaultOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, sslmethod=OpenSSL.SSL.SSLv3_METHOD)
+            self._reactor.connectSSL(self._serverAddress.host, self._serverAddress.port, factory, context)
+        elif self._scheme == "http":
+            self._reactor.connectTCP(self._serverAddress.host, self._serverAddress.port, factory)
+        else:
+            raise ValueError("URL scheme for client pool not supported")
+
+        def _doneOK(client):
+            self._pendingConnects -= 1
+
+            def _goneClientAfterError(f, client):
+                f.trap(ConnectionLost, ConnectionDone, ConnectError)
+
+            d2 = factory.afterConnect
+            d2.addErrback(_goneClientAfterError, client)
+            return client
+
+        def _doneError(result):
+            self._pendingConnects -= 1
+            return result
+
+        d = factory.onConnect
+        d.addCallbacks(_doneOK, _doneError)
+        
+        return d
+
+    def _performRequestOnClient(self, client, request, *args, **kwargs):
+        """
+        Perform the given request on the given client.
+
+        @param client: A L{PooledMemCacheProtocol} that will be used to perform
+            the given request.
+
+        @param command: A C{str} representing an attribute of
+            L{MemCacheProtocol}.
+        @parma args: Any positional arguments that should be passed to
+            C{command}.
+        @param kwargs: Any keyword arguments that should be passed to
+            C{command}.
+
+        @return: A L{Deferred} that fires with the result of the given command.
+        """
+
+        return client.submitRequest(request, closeAfter=False)
+
+    @inlineCallbacks
+    def submitRequest(self, request, *args, **kwargs):
+        """
+        Select an available client and perform the given request on it.
+
+        @param command: A C{str} representing an attribute of
+            L{MemCacheProtocol}.
+        @parma args: Any positional arguments that should be passed to
+            C{command}.
+        @param kwargs: Any keyword arguments that should be passed to
+            C{command}.
+
+        @return: A L{Deferred} that fires with the result of the given command.
+        """
+
+        # Since we may need to replay the request we have to read the request.stream
+        # into memory and reset request.stream to use a MemoryStream each time we repeat
+        # the request
+        data = (yield allDataFromStream(request.stream))
+
+        # Try this maxRetries times
+        for ctr in xrange(self.maxRetries + 1):
+            try:
+                request.stream = MemoryStream(data if data is not None else "")
+                request.stream.doStartReading = None
+
+                response = (yield self._submitRequest(request, args, kwargs))
+            except (ConnectionLost, ConnectionDone, ConnectError), e:
+                self.log_error("HTTP pooled client connection error (attempt: %d) - retrying: %s" % (ctr+1, e,))
+                continue
+            
+            # TODO: find the proper cause of these assertions and fix
+            except (AssertionError,), e:
+                self.log_error("HTTP pooled client connection assertion error (attempt: %d) - retrying: %s" % (ctr+1, e,))
+                continue
+            else:
+                returnValue(response)
+        else:
+            self.log_error("HTTP pooled client connection error - exhausted retry attempts.")
+            raise HTTPError(StatusResponse(responsecode.BAD_GATEWAY, "Could not connect to HTTP pooled client host."))
+
+    def _submitRequest(self, request, *args, **kwargs):
+        """
+        Select an available client and perform the given request on it.
+
+        @param command: A C{str} representing an attribute of
+            L{MemCacheProtocol}.
+        @parma args: Any positional arguments that should be passed to
+            C{command}.
+        @param kwargs: Any keyword arguments that should be passed to
+            C{command}.
+
+        @return: A L{Deferred} that fires with the result of the given command.
+        """
+
+        if len(self._freeClients) > 0:
+            d = self._performRequestOnClient(self._freeClients.pop(), request, *args, **kwargs)
+
+        elif len(self._busyClients) + self._pendingConnects >= self._maxClients:
+            d = Deferred()
+            self._pendingRequests.append((d, request, args, kwargs))
+            self.log_debug("Request queued: %s, %r, %r" % (request, args, kwargs))
+            self._logClientStats()
+
+        else:
+            d = self._newClientConnection()
+            d.addCallback(self._performRequestOnClient, request, *args, **kwargs)
+
+        return d
+
+    def _logClientStats(self):
+        self.log_debug("Clients for '%s' #free: %d, #busy: %d, "
+                       "#pending: %d, #queued: %d" % (
+                self._name,
+                len(self._freeClients),
+                len(self._busyClients),
+                self._pendingConnects,
+                len(self._pendingRequests)
+        ))
+
+    def clientBusy(self, client):
+        """
+        Notify that the given client is being used to complete a request.
+
+        @param client: An instance of L{HTTPClientProtocol}
+        """
+        if client in self._freeClients:
+            self._freeClients.remove(client)
+
+        self._busyClients.add(client)
+
+        self.log_debug("Busied client: %r" % (client,))
+        self._logClientStats()
+    
+    def clientIdle(self, client):
+        """
+        Notify that the given client is free to handle more requests.
+
+        @param client: An instance of L{HTTPClientProtocol}.
+        """
+        if client in self._busyClients:
+            self._busyClients.remove(client)
+
+        self._freeClients.add(client)
+
+        if self.shutdown_deferred and self._isIdle():
+            self.shutdown_deferred.callback(None)
+
+        self.log_debug("Freed client: %r" % (client,))
+        self._logClientStats()
+
+        self._processPending()
+
+    def clientPipelining(self, client):
+        pass
+    
+    def clientGone(self, client):
+        """
+        Notify that the given client is to be removed from the pool completely.
+
+        @param client: An instance of L{HTTPClientProtocol}.
+        """
+        if client in self._busyClients:
+            self._busyClients.remove(client)
+
+        elif client in self._freeClients:
+            self._freeClients.remove(client)
+
+        self.log_debug("Removed client: %r" % (client,))
+        self._logClientStats()
+
+        self._processPending()
+
+    def _processPending(self):
+        if len(self._pendingRequests) > 0:
+            d, request, args, kwargs = self._pendingRequests.pop(0)
+
+            self.log_debug("Performing Queued Request: %s, %r, %r" % (
+                    request, args, kwargs))
+            self._logClientStats()
+
+            _ign_d = self._submitRequest(request, *args, **kwargs)
+
+            _ign_d.addCallbacks(d.callback, d.errback)
+
+    def suggestMaxClients(self, maxClients):
+        """
+        Suggest the maximum number of concurrently connected clients.
+
+        @param maxClients: A C{int} indicating how many client connections we
+            should keep open.
+        """
+        self._maxClients = maxClients
+
+_clientPools = {}     # Maps a host:port to a pool object
+
+def installPools(hosts, maxClients=5, reactor=None):
+    
+    for name, url in hosts:
+        installPool(
+            name,
+            url,
+            maxClients,
+            reactor,
+        )
+
+def installPool(name, url, maxClients=5, reactor=None):
+
+    parsedURL = urlparse.urlparse(url)
+    pool = HTTPClientPool(
+        name,
+        parsedURL.scheme,
+        IPv4Address(
+            "TCP",
+            parsedURL.hostname,
+            parsedURL.port,
+        ),
+        maxClients,
+        reactor,
+    )
+    _clientPools[name] = pool
+
+def getHTTPClientPool(name):
+    return _clientPools[name]

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/reverseproxy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/reverseproxy.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/reverseproxy.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,67 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-__all__ = [
-    "ReverseProxyResource",
-]
-
-from twisted.web2 import iweb
-from twisted.web2.client.http import ClientRequest
-from twisted.web2.resource import LeafResource
-
-from twistedcaldav.client.pool import getHTTPClientPool
-from twistedcaldav.log import LoggingMixIn
-
-import urllib
-from zope.interface.declarations import implements
-
-class ReverseProxyResource(LeafResource, LoggingMixIn):
-    """
-    A L{LeafResource} which always performs a reverse proxy operation.
-    """
-    implements(iweb.IResource)
-
-    def __init__(self, poolID, *args, **kwargs):
-        """
-        
-        @param poolID: idenitifier of the pool to use
-        @type poolID: C{str}
-        """
-        
-        self.poolID = poolID
-        self._args   = args
-        self._kwargs = kwargs
-
-    def isCollection(self):
-        return True
-
-    def exists(self):
-        return False
-
-    def renderHTTP(self, request):
-        """
-        Do the reverse proxy request and return the response.
-
-        @param request: the incoming request that needs to be proxied.
-        @type request: L{Request}
-        
-        @return: Deferred L{Response}
-        """
-            
-        self.logger.info("%s %s %s" % (request.method, urllib.unquote(request.uri), "HTTP/%s.%s" % request.clientproto))
-        clientPool = getHTTPClientPool(self.poolID)
-        proxyRequest = ClientRequest(request.method, request.uri, request.headers, request.stream)
-        return clientPool.submitRequest(proxyRequest)

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/reverseproxy.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/reverseproxy.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/reverseproxy.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/client/reverseproxy.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,67 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+__all__ = [
+    "ReverseProxyResource",
+]
+
+from twisted.web2 import iweb
+from twisted.web2.client.http import ClientRequest
+from twisted.web2.resource import LeafResource
+
+from twistedcaldav.client.pool import getHTTPClientPool
+from twistedcaldav.log import LoggingMixIn
+
+import urllib
+from zope.interface.declarations import implements
+
+class ReverseProxyResource(LeafResource, LoggingMixIn):
+    """
+    A L{LeafResource} which always performs a reverse proxy operation.
+    """
+    implements(iweb.IResource)
+
+    def __init__(self, poolID, *args, **kwargs):
+        """
+        
+        @param poolID: idenitifier of the pool to use
+        @type poolID: C{str}
+        """
+        
+        self.poolID = poolID
+        self._args   = args
+        self._kwargs = kwargs
+
+    def isCollection(self):
+        return True
+
+    def exists(self):
+        return False
+
+    def renderHTTP(self, request):
+        """
+        Do the reverse proxy request and return the response.
+
+        @param request: the incoming request that needs to be proxied.
+        @type request: L{Request}
+        
+        @return: Deferred L{Response}
+        """
+            
+        self.logger.info("%s %s %s" % (request.method, urllib.unquote(request.uri), "HTTP/%s.%s" % request.clientproto))
+        clientPool = getHTTPClientPool(self.poolID)
+        proxyRequest = ClientRequest(request.method, request.uri, request.headers, request.stream)
+        return clientPool.submitRequest(proxyRequest)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/cluster.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/cluster.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/cluster.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -160,10 +160,6 @@
              '-o', 'MultiProcess/ProcessCount=%d' % (
                     config.MultiProcess['ProcessCount'],)])
 
-        if config.Memcached["ServerEnabled"]:
-            args.extend(
-                ['-o', 'Memcached/ClientEnabled=True'])
-
         if self.ports:
             args.extend([
                     '-o',
@@ -617,22 +613,23 @@
                                      config.PythonDirector['pydir'],
                                      fname],
                            env=parentEnv)
-    if config.Memcached["ServerEnabled"]:
-        log.msg("Adding memcached service")
+    for name, pool in config.Memcached["Pools"].items():
+        if pool["ServerEnabled"]:
+            log.msg("Adding memcached service for pool: %s" % (name,))
+    
+            memcachedArgv = [
+                    config.Memcached["memcached"],
+                    '-p', str(pool["Port"]),
+                    '-l', pool["BindAddress"]]
+    
+            if config.Memcached["MaxMemory"] is not 0:
+                memcachedArgv.extend([
+                        '-m', str(config.Memcached["MaxMemory"])])
+    
+            memcachedArgv.extend(config.Memcached["Options"])
+    
+            monitor.addProcess('memcached-%s' % (name,), memcachedArgv, env=parentEnv)
 
-        memcachedArgv = [
-                config.Memcached["memcached"],
-                '-p', str(config.Memcached["Port"]),
-                '-l', config.Memcached["BindAddress"]]
-
-        if config.Memcached["MaxMemory"] is not 0:
-            memcachedArgv.extend([
-                    '-m', str(config.Memcached["MaxMemory"])])
-
-        memcachedArgv.extend(config.Memcached["Options"])
-
-        monitor.addProcess('memcached', memcachedArgv, env=parentEnv)
-
     if (config.Notifications["Enabled"] and
         config.Notifications["InternalNotificationHost"] == "localhost"):
         log.msg("Adding notification service")

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/config.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/config.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -23,7 +23,6 @@
     "config",
 ]
 
-import os
 import copy
 import re
 
@@ -33,6 +32,7 @@
 from twistedcaldav.py.plistlib import readPlist
 from twistedcaldav.log import Logger
 from twistedcaldav.log import clearLogLevels, setLogLevelForNamespace, InvalidLogLevelError
+from twistedcaldav.partitions import partitions
 
 log = Logger()
 
@@ -71,12 +71,37 @@
     },
     "twistedcaldav.directory.appleopendirectory.OpenDirectoryService": {
         "node": "/Search",
-        "requireComputerRecord": True,
         "cacheTimeout": 30,
         "signalIntervalSeconds": 10,
     },
 }
 
+augmentDefaultParams = {
+    "twistedcaldav.directory.augment.AugmentXMLDB": {
+        "xmlFiles": ["/etc/caldavd/augments.xml",],
+    },
+    "twistedcaldav.directory.augment.AugmentSqliteDB": {
+        "dbpath": "/etc/caldavd/augments.sqlite",
+    },
+    "twistedcaldav.directory.augment.AugmentPostgreSQLDB": {
+        "host": "localhost",
+        "database": "augments",
+    },
+}
+
+proxyDBDefaultParams = {
+    "twistedcaldav.directory.calendaruserproxy.ProxySqliteDB": {
+        "dbpath": "/etc/caldavd/proxies.sqlite",
+    },
+    "twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB": {
+        "host": "localhost",
+        "database": "proxies",
+        "user": "",
+        "password": "",
+    },
+}
+
+
 defaultConfig = {
     # Note: Don't use None values below; that confuses the command-line parser.
 
@@ -128,6 +153,25 @@
     },
 
     #
+    # Augment service
+    #
+    #    Augments for the directory service records to add calendar specific attributes.
+    #
+    "AugmentService": {
+        "type": "twistedcaldav.directory.augment.AugmentXMLDB",
+        "params": augmentDefaultParams["twistedcaldav.directory.augment.AugmentXMLDB"],
+    },
+
+    #
+    # Proxies
+    #
+    "ProxyDBService": {
+        "type": "twistedcaldav.directory.calendaruserproxy.ProxySqliteDB",
+        "params": proxyDBDefaultParams["twistedcaldav.directory.calendaruserproxy.ProxySqliteDB"],
+    },
+    "ProxyLoadFromFile": "",    # Allows for initialization of the proxy database from an XML file
+
+    #
     # Special principals
     #
     "AdminPrincipals": [],                       # Principals with "DAV:all" access (relative URLs)
@@ -230,6 +274,33 @@
     "EnableAutoAcceptTrigger" : False, # Manually trigger auto-accept behavior
 
     #
+    # Scheduling related options
+    #
+    "Scheduling": {
+        
+        "CalDAV": {
+            "EmailDomain"                : "",    # Domain for mailto calendar user addresses on this server
+            "HTTPDomain"                 : "",    # Domain for http calendar user addresses on this server
+            "AddressPatterns"            : [],    # Reg-ex patterns to match local calendar user addresses
+            "OldDraftCompatibility"      : True,  # Whether to maintain compatibility with non-implicit mode
+            "ScheduleTagCompatibility"   : True,  # Whether to support older clients that do not use Schedule-Tag feature
+            "EnablePrivateComments"      : True,  # Private comments from attendees to organizer
+        },
+
+        "iSchedule": {
+            "Enabled"          : False, # iSchedule protocol
+            "AddressPatterns"  : [],    # Reg-ex patterns to match iSchedule-able calendar user addresses
+            "Servers"          : "/etc/caldavd/servertoserver.xml",    # iSchedule server configurations
+        },
+
+        "Options" : {
+            "AllowGroupAsOrganizer"      : False, # Allow groups to be Organizers
+            "AllowLocationAsOrganizer"   : False, # Allow locations to be Organizers
+            "AllowResourceAsOrganizer"   : False, # Allow resources to be Organizers
+        }
+    },
+
+    #
     # Notifications
     #
     "Notifications" : {
@@ -265,6 +336,16 @@
     },
 
     #
+    # Partitioning
+    #
+    "Partitioning" : {
+        "Enabled":             False,   # Partitioning enabled or not
+        "ServerPartitionID":   "",      # Unique ID for this server's partition instance.
+        "PartitionConfigFile": "/etc/caldavd/partitions.plist", # File path for partition information
+        "MaxClients":          5,       # Pool size for connections to each partition
+    },
+
+    #
     # Performance tuning
     #
 
@@ -333,10 +414,26 @@
 
     "Memcached": {
         "MaxClients": 5,
-        "ClientEnabled": True,
-        "ServerEnabled": True,
-        "BindAddress": "127.0.0.1",
-        "Port": 11211,
+        "Pools": {
+            "Default": {
+                "ClientEnabled": True,
+                "ServerEnabled": True,
+                "BindAddress": "127.0.0.1",
+                "Port": 11211,
+                "HandleCacheTypes": [
+                    "Default",
+                ]
+            },
+#            "ProxyDB": {
+#                "ClientEnabled": True,
+#                "ServerEnabled": True,
+#                "BindAddress": "127.0.0.1",
+#                "Port": 11211,
+#                "HandleCacheTypes": [
+#                    "ProxyDB", "PrincipalToken",
+#                ]
+#            },
+        },
         "memcached": "memcached", # Find in PATH
         "MaxMemory": 0, # Megabytes
         "Options": [],
@@ -366,6 +463,7 @@
             self.updateDropBox,
             self.updateLogLevels,
             self.updateNotifications,
+            self.updatePartitions,
         ]
 
     def __str__(self):
@@ -423,6 +521,18 @@
                 if param not in serviceDefaultParams[self._data.DirectoryService.type]:
                     del self._data.DirectoryService.params[param]
 
+        if self._data.AugmentService.type in augmentDefaultParams:
+            for param in tuple(self._data.AugmentService.params):
+                if param not in augmentDefaultParams[self._data.AugmentService.type]:
+                    log.warn("Parameter %s is not supported by service %s" % (param, self._data.AugmentService.type))
+                    del self._data.AugmentService.params[param]
+
+        if self._data.ProxyDBService.type in proxyDBDefaultParams:
+            for param in tuple(self._data.ProxyDBService.params):
+                if param not in proxyDBDefaultParams[self._data.ProxyDBService.type]:
+                    log.warn("Parameter %s is not supported by service %s" % (param, self._data.ProxyDBService.type))
+                    del self._data.ProxyDBService.params[param]
+
     @staticmethod
     def updateACLs(self, items):
         #
@@ -533,6 +643,21 @@
         except InvalidLogLevelError, e:
             raise ConfigurationError("Invalid log level: %s" % (e.level))
 
+    @staticmethod
+    def updatePartitions(self, items):
+        #
+        # Partitions
+        #
+    
+        if "Partitioning" in items:
+            if items["Partitioning"]["Enabled"]:
+                partitions.setSelfPartition(items["Partitioning"]["ServerPartitionID"])
+                partitions.setMaxClients(items["Partitioning"]["MaxClients"])
+                partitions.readConfig(items["Partitioning"]["PartitionConfigFile"])
+                partitions.installReverseProxies()
+            else:
+                partitions.clear()
+
     def updateDefaults(self, items):
         _mergeData(self._defaults, items)
         self.update(items)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/customxml.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/customxml.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -315,6 +315,13 @@
     protected = True
     hidden = True
 
+class IScheduleInbox (davxml.WebDAVEmptyElement):
+    """
+    Denotes the resourcetype of a iSchedule Inbox.
+    (CalDAV-s2s-xx, section x.x.x)
+    """
+    namespace = calendarserver_namespace
+    name = "ischedule-inbox"
 
 
 
@@ -327,3 +334,4 @@
 davxml.ResourceType.calendarproxyread = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyRead())
 davxml.ResourceType.calendarproxywrite = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyWrite())
 davxml.ResourceType.timezones = davxml.ResourceType(Timezones())
+davxml.ResourceType.ischeduleinbox = davxml.ResourceType(IScheduleInbox())

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/database.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/database.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/database.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/database.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,556 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.enterprise.adbapi import ConnectionPool
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.threadpool import ThreadPool
+
+from twistedcaldav.config import ConfigurationError
+from twistedcaldav.log import Logger
+
+import thread
+
+try:
+    import pgdb
+except:
+    pgdb = None
+
+"""
+Generic ADAPI database access object.
+"""
+
+__all__ = [
+    "AbstractADBAPIDatabase",
+]
+
+log = Logger()
+
+class ConnectionClosingThreadPool(ThreadPool):
+    """
+    A ThreadPool that closes connections for each worker thread
+    """
+    
+    def _worker(self, o):
+        log.debug("Starting ADBAPI thread: %s" % (thread.get_ident(),))
+        ThreadPool._worker(self, o)
+        self._closeConnection()
+
+    def _closeConnection(self):
+        
+        tid = thread.get_ident()
+        log.debug("Closing ADBAPI thread: %s" % (tid,))
+
+        conn = self.pool.connections.get(tid)
+        self.pool._close(conn)
+        del self.pool.connections[tid]
+
+class AbstractADBAPIDatabase(object):
+    """
+    A generic SQL database.
+    """
+
+    def __init__(self, dbID, dbapiName, dbapiArgs, persistent, **kwargs):
+        """
+        
+        @param persistent: C{True} if the data in the DB must be perserved during upgrades,
+            C{False} if the DB data can be re-created from an external source.
+        @type persistent: bool
+        """
+        self.dbID = dbID
+        self.dbapiName = dbapiName
+        self.dbapiArgs = dbapiArgs
+        self.dbapikwargs = kwargs
+
+        self.persistent = persistent
+        
+        self.initialized = False
+
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.pool)
+
+    @inlineCallbacks
+    def open(self):
+        """
+        Access the underlying database.
+        @return: a db2 connection object for this index's underlying data store.
+        """
+        if not self.initialized:
+
+            self.pool = ConnectionPool(self.dbapiName, *self.dbapiArgs, **self.dbapikwargs)
+            
+            # sqlite3 is not thread safe which means we have to close the sqlite3 connections in the same thread that
+            # opened them. We need a special thread pool class that has a thread worker function that does a close
+            # when a thread is closed.
+            if self.dbapiName == "sqlite3":
+                self.pool.threadpool.stop()
+                self.pool.threadpool = ConnectionClosingThreadPool(1, 1)
+                self.pool.threadpool.start()
+                self.pool.threadpool.pool = self.pool
+
+            #
+            # Set up the schema
+            #
+            # Create CALDAV table if needed
+
+            test = (yield self._test_schema_table())
+            if test:
+                version = (yield self._db_value_for_sql("select VALUE from CALDAV where KEY = 'SCHEMA_VERSION'"))
+                dbtype = (yield self._db_value_for_sql("select VALUE from CALDAV where KEY = 'TYPE'"))
+
+                if (version != self._db_version()) or (dbtype != self._db_type()):
+
+                    if dbtype != self._db_type():
+                        log.err("Database %s has different type (%s vs. %s)"
+                                % (self.dbID, dbtype, self._db_type()))
+
+                        # Delete this index and start over
+                        yield self._db_remove()
+                        yield self._db_init()
+
+                    elif version != self._db_version():
+                        log.err("Database %s has different schema (v.%s vs. v.%s)"
+                                % (self.dbID, version, self._db_version()))
+                        
+                        # Upgrade the DB
+                        yield self._db_upgrade(version)
+
+            else:
+                yield self._db_init()
+            self.initialized = True
+
+    def close(self):
+        
+        if self.initialized:
+            self.pool.close()
+            self.pool = None
+            self.initialized = False
+
+    @inlineCallbacks
+    def clean(self):
+        
+        if not self.initialized:
+            yield self.open()
+
+        yield self._db_empty_data_tables()
+
+    @inlineCallbacks
+    def execute(self, sql, *query_params):
+        
+        if not self.initialized:
+            yield self.open()
+
+        yield self._db_execute(sql, *query_params)
+
+    @inlineCallbacks
+    def executescript(self, script):
+        
+        if not self.initialized:
+            yield self.open()
+
+        yield self._db_execute_script(script)
+
+    @inlineCallbacks
+    def query(self, sql, *query_params):
+        
+        if not self.initialized:
+            yield self.open()
+
+        result = (yield self._db_all_values_for_sql(sql, *query_params))
+        returnValue(result)
+
+    @inlineCallbacks
+    def queryList(self, sql, *query_params):
+        
+        if not self.initialized:
+            yield self.open()
+
+        result = (yield self._db_values_for_sql(sql, *query_params))
+        returnValue(result)
+
+    @inlineCallbacks
+    def queryOne(self, sql, *query_params):
+        
+        if not self.initialized:
+            yield self.open()
+
+        result = (yield self._db_value_for_sql(sql, *query_params))
+        returnValue(result)
+
+    def _db_version(self):
+        """
+        @return: the schema version assigned to this DB.
+        """
+        raise NotImplementedError
+        
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this DB.
+        """
+        raise NotImplementedError
+        
+    def _test_schema_table(self):
+        return self._test_table("CALDAV")
+
+    @inlineCallbacks
+    def _db_init(self):
+        """
+        Initialise the underlying database tables.
+        """
+        log.msg("Initializing database %s" % (self.dbID,))
+
+        # TODO we need an exclusive lock of some kind here to prevent a race condition
+        # in which multiple processes try to create the tables.
+        
+
+        yield self._db_init_schema_table()
+        yield self._db_init_data_tables()
+        yield self._db_recreate()
+
+    @inlineCallbacks
+    def _db_init_schema_table(self):
+        """
+        Initialise the underlying database tables.
+        @param db_filename: the file name of the index database.
+        @param q:           a database cursor to use.
+        """
+
+        #
+        # CALDAV table keeps track of our schema version and type
+        #
+        yield self._create_table("CALDAV", (
+            ("KEY", "text unique"),
+            ("VALUE", "text unique"),
+        ), True)
+
+        yield self._db_execute(
+            """
+            insert or ignore into CALDAV (KEY, VALUE)
+            values ('SCHEMA_VERSION', :1)
+            """, (self._db_version(),)
+        )
+        yield self._db_execute(
+            """
+            insert or ignore into CALDAV (KEY, VALUE)
+            values ('TYPE', :1)
+            """, (self._db_type(),)
+        )
+
+    def _db_init_data_tables(self):
+        """
+        Initialise the underlying database tables.
+        """
+        raise NotImplementedError
+
+    def _db_empty_data_tables(self):
+        """
+        Delete the database tables.
+        """
+
+        # Implementations can override this to re-create data
+        pass
+        
+    def _db_recreate(self):
+        """
+        Recreate the database tables.
+        """
+
+        # Implementations can override this to re-create data
+        pass
+
+    @inlineCallbacks
+    def _db_upgrade(self, old_version):
+        """
+        Upgrade the database tables.
+        """
+        
+        if self.persistent:
+            yield self._db_upgrade_data_tables(old_version)
+            yield self._db_upgrade_schema()
+        else:
+            # Non-persistent DB's by default can be removed and re-created. However, for simple
+            # DB upgrades they SHOULD override this method and handle those for better performance.
+            yield self._db_remove()
+            yield self._db_init()
+    
+    def _db_upgrade_data_tables(self, old_version):
+        """
+        Upgrade the data from an older version of the DB.
+        """
+        # Persistent DB's MUST override this method and do a proper upgrade. Their data
+        # cannot be thrown away.
+        raise NotImplementedError("Persistent databases MUST support an upgrade method.")
+
+    @inlineCallbacks
+    def _db_upgrade_schema(self):
+        """
+        Upgrade the stored schema version to the current one.
+        """
+        yield self._db_execute("insert or replace into CALDAV (KEY, VALUE) values ('SCHEMA_VERSION', :1)", (self._db_version(),))
+
+    @inlineCallbacks
+    def _db_remove(self):
+        """
+        Remove all database information (all the tables)
+        """
+        yield self._db_remove_data_tables()
+        yield self._db_remove_schema()
+
+    def _db_remove_data_tables(self):
+        """
+        Remove all the data from an older version of the DB.
+        """
+        raise NotImplementedError("Each database must remove its own tables.")
+
+    @inlineCallbacks
+    def _db_remove_schema(self):
+        """
+        Remove the stored schema version table.
+        """
+        yield self._db_execute("drop table if exists CALDAV")
+
+    @inlineCallbacks
+    def _db_all_values_for_sql(self, sql, *query_params):
+        """
+        Execute an SQL query and obtain the resulting values.
+        @param sql: the SQL query to execute.
+        @param query_params: parameters to C{sql}.
+        @return: an interable of values in the first column of each row
+            resulting from executing C{sql} with C{query_params}.
+        @raise AssertionError: if the query yields multiple columns.
+        """
+        
+        sql = self._prepare_statement(sql)
+        results = (yield self.pool.runQuery(sql, *query_params))
+        returnValue(tuple(results))
+
+    @inlineCallbacks
+    def _db_values_for_sql(self, sql, *query_params):
+        """
+        Execute an SQL query and obtain the resulting values.
+
+        @param sql: the SQL query to execute.
+        @param query_params: parameters to C{sql}.
+        @return: an interable of values in the first column of each row
+            resulting from executing C{sql} with C{query_params}.
+        @raise AssertionError: if the query yields multiple columns.
+        """
+        
+        sql = self._prepare_statement(sql)
+        results = (yield self.pool.runQuery(sql, *query_params))
+        returnValue(tuple([row[0] for row in results]))
+
+    @inlineCallbacks
+    def _db_value_for_sql(self, sql, *query_params):
+        """
+        Execute an SQL query and obtain a single value.
+
+        @param sql: the SQL query to execute.
+        @param query_params: parameters to C{sql}.
+        @return: the value resulting from the executing C{sql} with
+            C{query_params}.
+        @raise AssertionError: if the query yields multiple rows or columns.
+        """
+        value = None
+        for row in (yield self._db_values_for_sql(sql, *query_params)):
+            assert value is None, "Multiple values in DB for %s %s" % (sql, query_params)
+            value = row
+        returnValue(value)
+
+    def _db_execute(self, sql, *query_params):
+        """
+        Execute an SQL operation that returns None.
+
+        @param sql: the SQL query to execute.
+        @param query_params: parameters to C{sql}.
+        @return: an iterable of tuples for each row resulting from executing
+            C{sql} with C{query_params}.
+        """
+        
+        sql = self._prepare_statement(sql)
+        return self.pool.runOperation(sql, *query_params)
+
+    """
+    Since different databases support different types of columns and modifiers on those we need to
+    have an "abstract" way of specifying columns in our code and then map the abstract specifiers to
+    the underlying DB's allowed types.
+    
+    Types we can use are:
+    
+    integer
+    text
+    text(n)
+    date
+    serial
+    
+    The " unique" modifier can be appended to any of those.
+    """
+    def _map_column_types(self, type):
+        raise NotImplementedError
+        
+    def _create_table(self, name, columns, ifnotexists=False):
+        raise NotImplementedError
+
+    def _test_table(self, name):
+        raise NotImplementedError
+
+    def _prepare_statement(self, sql):
+        raise NotImplementedError
+        
+class ADBAPISqliteMixin(object):
+
+    @classmethod
+    def _map_column_types(self, coltype):
+        
+        result = ""
+        splits = coltype.split()
+        if splits[0] == "integer":
+            result = "integer"
+        elif splits[0] == "text":
+            result = "text"
+        elif splits[0].startswith("text("):
+            result = splits[0]
+        elif splits[0] == "date":
+            result = "date"
+        elif splits[0] == "serial":
+            result = "integer primary key autoincrement"
+        
+        if len(splits) > 1 and splits[1] == "unique":
+            result += " unique"
+        
+        return result
+
+    @inlineCallbacks
+    def _create_table(self, name, columns, ifnotexists=False):
+        
+        colDefs = ["%s %s" % (colname, self._map_column_types(coltype)) for colname, coltype in columns]
+        statement = "create table %s%s (%s)" % (
+            "if not exists " if ifnotexists else "",
+            name,
+            ", ".join(colDefs),
+        )
+        yield self._db_execute(statement)
+
+    @inlineCallbacks
+    def _test_table(self, name):
+        result = (yield self._db_value_for_sql("""
+        select (1) from SQLITE_MASTER
+         where TYPE = 'table' and NAME = '%s'
+        """ % (name,)))
+        returnValue(result)
+
+    def _prepare_statement(self, sql):
+        # We are going to use the sqlite syntax of :1, :2 etc for our
+        # internal statements so we do not need to remap those
+        return sql
+
+if pgdb:
+
+    class ADBAPIPostgreSQLMixin(object):
+        
+        @classmethod
+        def _map_column_types(self, coltype):
+            
+            result = ""
+            splits = coltype.split()
+            if splits[0] == "integer":
+                result = "integer"
+            elif splits[0] == "text":
+                result = "text"
+            elif splits[0].startswith("text("):
+                result = "char" + splits[0][4:]
+            elif splits[0] == "date":
+                result = "date"
+            elif splits[0] == "serial":
+                result = "serial"
+            
+            if len(splits) > 1 and splits[1] == "unique":
+                result += " unique"
+            
+            return result
+    
+        @inlineCallbacks
+        def _create_table(self, name, columns, ifnotexists=False):
+            
+            colDefs = ["%s %s" % (colname, self._map_column_types(coltype)) for colname, coltype in columns]
+            statement = "create table %s (%s)" % (
+                name,
+                ", ".join(colDefs),
+            )
+            
+            try:
+                yield self._db_execute(statement)
+            except pgdb.DatabaseError:
+                
+                if not ifnotexists:
+                    raise
+                
+                result = (yield self._test_table(name))
+                if not result:
+                    raise 
+    
+        @inlineCallbacks
+        def _test_table(self, name):
+            result = (yield self._db_value_for_sql("""
+            select * from pg_tables
+             where tablename = '%s'
+            """ % (name.lower(),)))
+            returnValue(result)
+    
+        @inlineCallbacks
+        def _db_init_schema_table(self):
+            """
+            Initialise the underlying database tables.
+            @param db_filename: the file name of the index database.
+            @param q:           a database cursor to use.
+            """
+    
+            #
+            # CALDAV table keeps track of our schema version and type
+            #
+            try:
+                yield self._create_table("CALDAV", (
+                    ("KEY", "text unique"),
+                    ("VALUE", "text unique"),
+                ), True)
+    
+                yield self._db_execute(
+                    """
+                    insert into CALDAV (KEY, VALUE)
+                    values ('SCHEMA_VERSION', :1)
+                    """, (self._db_version(),)
+                )
+                yield self._db_execute(
+                    """
+                    insert into CALDAV (KEY, VALUE)
+                    values ('TYPE', :1)
+                    """, (self._db_type(),)
+                )
+            except pgdb.DatabaseError:
+                pass
+    
+        def _prepare_statement(self, sql):
+            # Convert :1, :2 etc format into %s
+            ctr = 1
+            while sql.find(":%d" % (ctr,)) != -1:
+                sql = sql.replace(":%d" % (ctr,), "%s")
+                ctr += 1
+            return sql
+
+else:
+    class ADBAPIPostgreSQLMixin(object):
+        
+        def __init__(self):
+            raise ConfigurationError("PostgreSQL module not available.")

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/apache.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/apache.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/apache.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,197 +0,0 @@
-##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Apache UserFile/GroupFile compatible directory service implementation.
-"""
-
-__all__ = [
-    "BasicDirectoryService",
-    "DigestDirectoryService",
-]
-
-from crypt import crypt
-
-from twisted.python.filepath import FilePath
-from twisted.cred.credentials import UsernamePassword
-
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.directory import UnknownRecordTypeError
-
-class AbstractDirectoryService(DirectoryService):
-    """
-    Abstract Apache-compatible implementation of L{IDirectoryService}.
-    """
-    def __repr__(self):
-        return "<%s %r: %r %r>" % (self.__class__.__name__, self.realmName, self.userFile, self.groupFile)
-
-    def __init__(self, realmName, userFile, groupFile=None):
-        super(AbstractDirectoryService, self).__init__()
-
-        if type(userFile) is str:
-            userFile = FilePath(userFile)
-        if type(groupFile) is str:
-            groupFile = FilePath(groupFile)
-
-        self.realmName = realmName
-        self.userFile = userFile
-        self.groupFile = groupFile
-
-    def recordTypes(self):
-        recordTypes = (DirectoryService.recordType_users,)
-        if self.groupFile is not None:
-            recordTypes += (DirectoryService.recordType_groups,)
-        return recordTypes
-
-    def listRecords(self, recordType):
-        for entryShortName, entryData in self.entriesForRecordType(recordType):
-            if recordType == DirectoryService.recordType_users:
-                yield self.userRecordClass(
-                    service       = self,
-                    recordType    = recordType,
-                    shortName     = entryShortName,
-                    cryptPassword = entryData,
-                )
-
-            elif recordType == DirectoryService.recordType_groups:
-                yield GroupRecord(
-                    service    = self,
-                    recordType = recordType,
-                    shortName  = entryShortName,
-                    members    = entryData,
-                )
-
-            else:
-                # Subclass should cover the remaining record types
-                raise AssertionError("Unknown record type: %r" % (recordType,))
-
-    def recordWithShortName(self, recordType, shortName):
-        for entryShortName, entryData in self.entriesForRecordType(recordType):
-            if entryShortName == shortName:
-                if recordType == DirectoryService.recordType_users:
-                    return self.userRecordClass(
-                        service       = self,
-                        recordType    = recordType,
-                        shortName     = entryShortName,
-                        cryptPassword = entryData,
-                    )
-
-                if recordType == DirectoryService.recordType_groups:
-                    return GroupRecord(
-                        service    = self,
-                        recordType = recordType,
-                        shortName  = entryShortName,
-                        members    = entryData,
-                    )
-
-                # Subclass should cover the remaining record types
-                raise AssertionError("Unknown record type: %r" % (recordType,))
-
-        return None
-
-    def entriesForRecordType(self, recordType):
-        if recordType == DirectoryService.recordType_users:
-            recordFile = self.userFile
-        elif recordType == DirectoryService.recordType_groups:
-            recordFile = self.groupFile
-        else:
-            raise UnknownRecordTypeError("Unknown record type: %s" % (recordType,))
-
-        if recordFile is None:
-            return
-
-        for entry in recordFile.open():
-            if entry and entry[0] != "#":
-                shortName, rest = entry.rstrip("\n").split(":", 1)
-                yield shortName, rest
-
-class AbstractDirectoryRecord(DirectoryRecord):
-    """
-    Abstract Apache-compatible implementation of L{IDirectoryRecord}.
-    """
-    def __init__(self, service, recordType, shortName):
-        super(AbstractDirectoryRecord, self).__init__(
-            service               = service,
-            recordType            = recordType,
-            guid                  = None,
-            shortName             = shortName,
-            fullName              = None,
-            calendarUserAddresses = set(),
-            autoSchedule          = False,
-        )
-
-class AbstractUserRecord(AbstractDirectoryRecord):
-    def __init__(self, service, recordType, shortName, cryptPassword=None):
-        super(AbstractUserRecord, self).__init__(service, recordType, shortName)
-
-        self._cryptPassword = cryptPassword
-
-    def groups(self):
-        for group in self.service.listRecords(DirectoryService.recordType_groups):
-            for member in group.members():
-                if member == self:
-                    yield group
-                    continue
-
-class BasicUserRecord(AbstractUserRecord):
-    """
-    Apache UserFile implementation of L{IDirectoryRecord}.
-    """
-    def verifyCredentials(self, credentials):
-        if self._cryptPassword in ("", "*", "x"):
-            return False
-
-        if isinstance(credentials, UsernamePassword):
-            return crypt(credentials.password, self._cryptPassword) == self._cryptPassword
-
-        return super(BasicUserRecord, self).verifyCredentials(credentials)
-
-class BasicDirectoryService(AbstractDirectoryService):
-    """
-    Apache UserFile/GroupFile implementation of L{IDirectoryService}.
-    """
-    baseGUID = "DDF1E45C-CADE-4FCD-8AE6-B4B41D72B325"
-    userRecordClass = BasicUserRecord
-
-class DigestUserRecord(AbstractUserRecord):
-    """
-    Apache DigestUserFile implementation of L{IDirectoryRecord}.
-    """
-    def verifyCredentials(self, credentials):
-        raise NotImplementedError()
-
-class DigestDirectoryService(AbstractDirectoryService):
-    """
-    Apache DigestUserFile/GroupFile implementation of L{IDirectoryService}.
-    """
-    baseGUID = "0C719D1B-0A14-4074-8740-6D96A7D0C787"
-    userRecordClass = DigestUserRecord
-
-class GroupRecord(AbstractDirectoryRecord):
-    """
-    Apache GroupFile implementation of L{IDirectoryRecord}.
-    """
-    def __init__(self, service, recordType, shortName, members=()):
-        super(GroupRecord, self).__init__(service, recordType, shortName)
-
-        if type(members) is str:
-            members = tuple(m.strip() for m in members.split(","))
-
-        self._members = members
-
-    def members(self):
-        for shortName in self._members:
-            yield self.service.recordWithShortName(DirectoryService.recordType_users, shortName)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/appleopendirectory.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/appleopendirectory.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -24,13 +24,8 @@
 ]
 
 import sys
-import os
 import signal
-from random import random
-from uuid import UUID
 
-from xml.parsers.expat import ExpatError
-
 import opendirectory
 import dsattributes
 import dsquery
@@ -51,11 +46,10 @@
 from twisted.python.filepath import FilePath
 
 from twistedcaldav.config import config
+from twistedcaldav.directory import augment
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
 from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
 
-from plistlib import readPlistFromString, readPlist
-
 serverPreferences = '/Library/Preferences/com.apple.servermgr_info.plist'
 saclGroup = 'com.apple.access_calendar'
 
@@ -68,11 +62,9 @@
     def __repr__(self):
         return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
 
-    def __init__(self, node="/Search", requireComputerRecord=True, dosetup=True, doreload=True, cacheTimeout=30, signalIntervalSeconds=10, **kwds):
+    def __init__(self, node="/Search", dosetup=True, doreload=True, cacheTimeout=30, signalIntervalSeconds=10, **kwds):
         """
         @param node: an OpenDirectory node name to bind to.
-        @param requireComputerRecord: C{True} if the directory schema is to be used to determine
-            which calendar users are enabled.
         @param dosetup: if C{True} then the directory records are initialized,
                         if C{False} they are not.
                         This should only be set to C{False} when doing unit tests.
@@ -86,9 +78,6 @@
         self.realmName = node
         self.directory = directory
         self.node = node
-        self.requireComputerRecord = requireComputerRecord
-        self.computerRecords = {}
-        self.servicetags = set()
         self.cacheTimeout = cacheTimeout
         self.signalIntervalSeconds = signalIntervalSeconds
         self._records = {}
@@ -98,21 +87,6 @@
         self.isWorkgroupServer = False
 
         if dosetup:
-            if self.requireComputerRecord:
-                try:
-                    self._lookupVHostRecord()
-                except Exception, e:
-                    self.log_error("Unable to locate virtual host record: %s" % (e,))
-                    raise
-
-                if os.path.exists(serverPreferences):
-                    serverInfo = readPlist(serverPreferences)
-
-                    self.isWorkgroupServer = serverInfo.get('ServiceConfig', {}).get('IsWorkgroupServer', False)
-
-                    if self.isWorkgroupServer:
-                        self.log_info("Enabling Workgroup Server compatibility mode")
-
             if doreload:
                 for recordType in self.recordTypes():
                     self.recordsForType(recordType)
@@ -281,221 +255,11 @@
     def _getMemcacheClient(self, refresh=False):
         if refresh or not hasattr(self, "memcacheClient"):
             self.memcacheClient = memcacheclient.ClientFactory.getClient(['%s:%s' %
-                (config.Memcached.BindAddress, config.Memcached.Port)],
+                (config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port)],
                 debug=0, pickleProtocol=2)
         return self.memcacheClient
 
 
-    def _lookupVHostRecord(self):
-        """
-        Get the OD service record for this host.
-        """
-
-        # The server must have been configured with a virtual hostname.
-        vhostname = config.ServerHostName
-        if not vhostname:
-            raise OpenDirectoryInitError(
-                "There is no virtual hostname configured for the server for use with Open Directory (node=%s)"
-                % (self.realmName,)
-            )
-         
-        # Find a record in /Computers with an apple-serviceinfo attribute value equal to the virtual hostname
-        # and return some useful attributes.
-        attrs = [
-            dsattributes.kDS1AttrGeneratedUID,
-            dsattributes.kDSNAttrRecordName,
-            dsattributes.kDSNAttrMetaNodeLocation,
-            "dsAttrTypeNative:apple-serviceinfo",
-        ]
-
-        self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r)" % (
-            self.directory,
-            dsquery.match(
-                "dsAttrTypeNative:apple-serviceinfo",
-                vhostname,
-                dsattributes.eDSContains,
-            ).generate(),
-            True,    # case insentive for hostnames
-            dsattributes.kDSStdRecordTypeComputers,
-            attrs
-        ))
-        records = opendirectory.queryRecordsWithAttributes_list(
-            self.directory,
-            dsquery.match(
-                "dsAttrTypeNative:apple-serviceinfo",
-                vhostname,
-                dsattributes.eDSContains,
-            ).generate(),
-            True,    # case insentive for hostnames
-            dsattributes.kDSStdRecordTypeComputers,
-            attrs
-        )
-        self._parseComputersRecords(records, vhostname)
-
-    def _parseComputersRecords(self, records, vhostname):
-        # Must have some results
-        if len(records) == 0:
-            raise OpenDirectoryInitError(
-                "Open Directory (node=%s) has no /Computers records with a virtual hostname: %s"
-                % (self.realmName, vhostname)
-            )
-
-        # Now find all appropriate records and determine the enabled (only) service tags for each.
-        for recordname, record in records:
-            self._parseServiceInfo(vhostname, recordname, record)
-
-        # Log all the matching records
-        for key, value in self.computerRecords.iteritems():
-            _ignore_recordname, enabled, servicetag = value
-            self.log_info("Matched Directory record: %s with ServicesLocator: %s, state: %s" % (
-                key,
-                servicetag,
-                {True:"enabled", False:"disabled"}[enabled]
-            ))
-
-        # Log all the enabled service tags - or generate an error if there are none
-        if self.servicetags:
-            for tag in self.servicetags:
-                self.log_info("Enabled ServicesLocator: %s" % (tag,))
-        else:
-            raise OpenDirectoryInitError(
-                "Open Directory (node=%s) no /Computers records with an enabled and valid "
-                "calendar service were found matching virtual hostname: %s"
-                % (self.realmName, vhostname)
-            )
-
-    def _parseServiceInfo(self, vhostname, recordname, record):
-
-        # Extract some useful attributes
-        recordguid = record[dsattributes.kDS1AttrGeneratedUID]
-        recordlocation = "%s/Computers/%s" % (record[dsattributes.kDSNAttrMetaNodeLocation], recordname)
-
-        # First check for apple-serviceinfo attribute
-        plist = record.get("dsAttrTypeNative:apple-serviceinfo", None)
-        if not plist:
-            return False
-
-        # Parse the plist and look for our special entry
-        plist = readPlistFromString(plist)
-        vhosts = plist.get("com.apple.macosxserver.virtualhosts", None)
-        if not vhosts:
-            self.log_error(
-                "Open Directory (node=%s) %s record does not have a "
-                "com.apple.macosxserver.virtualhosts in its apple-serviceinfo attribute value"
-                % (self.realmName, recordlocation)
-            )
-            return False
-        
-        # Iterate over each vhost and find one that is a calendar service
-        hostguid = None
-        for key, value in vhosts.iteritems():
-            serviceTypes = value.get("serviceType", None)
-            if serviceTypes:
-                for type in serviceTypes:
-                    if type == "calendar":
-                        hostguid = key
-                        break
-                    
-        if not hostguid:
-            # We can get false positives from the query - we ignore those.
-            return False
-            
-        # Get host name
-        hostname = vhosts[hostguid].get("hostname", None)
-        if not hostname:
-            self.log_error(
-                "Open Directory (node=%s) %s record does not have "
-                "any host name in its apple-serviceinfo attribute value"
-                % (self.realmName, recordlocation)
-            )
-            return False
-        if hostname != vhostname:
-            # We can get false positives from the query - we ignore those.
-            return False
-        
-        # Get host details. At this point we only check that it is present. We actually
-        # ignore the details themselves (scheme/port) as we use our own config for that.
-        hostdetails = vhosts[hostguid].get("hostDetails", None)
-        if not hostdetails:
-            self.log_error(
-                "Open Directory (node=%s) %s record does not have "
-                "any host details in its apple-serviceinfo attribute value"
-                % (self.realmName, recordlocation)
-            )
-            return False
-        
-        # Look at the service data
-        serviceInfos = vhosts[hostguid].get("serviceInfo", None)
-        if not serviceInfos or not serviceInfos.has_key("calendar"):
-            self.log_error(
-                "Open Directory (node=%s) %s record does not have a "
-                "calendar service in its apple-serviceinfo attribute value"
-                % (self.realmName, recordlocation)
-            )
-            return False
-        serviceInfo = serviceInfos["calendar"]
-        
-        # Check that this service is enabled
-        enabled = serviceInfo.get("enabled", True)
-
-        # Create the string we will use to match users with accounts on this server
-        servicetag = "%s:%s:calendar" % (recordguid, hostguid)
-        
-        self.computerRecords[recordlocation] = (recordname, enabled, servicetag)
-        
-        if enabled:
-            self.servicetags.add(servicetag)
-        
-        return True
-    
-    def _calendarUserAddresses(self, recordType, recordName, record):
-        """
-        Extract specific attributes from the directory record for use as calendar user address.
-        
-        @param recordName: a C{str} containing the record name being operated on.
-        @param record: a C{dict} containing the attributes retrieved from the directory.
-        @return: a C{set} of C{str} for each expanded calendar user address.
-        """
-        # Now get the addresses
-        result = set()
-        
-        # Add each email address as a mailto URI
-        emails = record.get(dsattributes.kDSNAttrEMailAddress)
-        if emails is not None:
-            if isinstance(emails, str):
-                emails = [emails]
-            for email in emails:
-                result.add("mailto:%s" % (email.lower(),))
-                
-        return result
-
-    def _parseResourceInfo(self, plist, guid, recordType, shortname):
-        """
-        Parse OD ResourceInfo attribute and extract information that the server needs.
-
-        @param plist: the plist that is the attribute value.
-        @type plist: str
-        @param guid: the directory GUID of the record being parsed.
-        @type guid: str
-        @param shortname: the record shortname of the record being parsed.
-        @type shortname: str
-        @return: a C{tuple} of C{bool} for auto-accept, C{str} for proxy GUID, C{str} for read-only proxy GUID.
-        """
-        try:
-            plist = readPlistFromString(plist)
-            wpframework = plist.get("com.apple.WhitePagesFramework", {})
-            autoaccept = wpframework.get("AutoAcceptsInvitation", False)
-            proxy = wpframework.get("CalendaringDelegate", None)
-            read_only_proxy = wpframework.get("ReadOnlyCalendaringDelegate", None)
-        except (ExpatError, AttributeError), e:
-            self.log_error(
-                "Failed to parse ResourceInfo attribute of record (%s)%s (guid=%s): %s\n%s" %
-                (recordType, shortname, guid, e, plist,)
-            )
-            raise ValueError("Invalid ResourceInfo")
-
-        return (autoaccept, proxy, read_only_proxy,)
-
     def recordTypes(self):
         return (
             DirectoryService.recordType_users,
@@ -693,29 +457,16 @@
         
         if recordType == DirectoryService.recordType_groups:
             groupsForGUID = {}
-        elif recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
-            proxiesForGUID = {}
-            readOnlyProxiesForGUID = {}
 
-        def allowForACLs():
-            return recordType in (
-                DirectoryService.recordType_users,
-                DirectoryService.recordType_groups,
-            )
+        def _setFromAttribute(attribute, lower=False):
+            if attribute:
+                if isinstance(attribute, str):
+                    return set((attribute.lower() if lower else attribute,))
+                else:
+                    return set([item.lower() if lower else item for item in attribute])
+            else:
+                return ()
 
-        def disableForCalendaring(recordShortName):
-            self.log_debug(
-                "Record (%s) %s is not enabled for calendaring but may be used in ACLs"
-                % (recordType, recordShortName)
-            )
-
-        def invalidRecord(recordShortName):
-            self.log_error(
-                "Directory (incorrectly) returned a record with no applicable "
-                "ServicesLocator attribute: (%s) %s"
-                % (recordType, recordShortName)
-            )
-
         def disableRecord(record):
             self.log_warn("Record disabled due to conflict (record name and GUID must match): %s" % (record,))
 
@@ -735,36 +486,11 @@
                     del cuaddrs[cuaddr]
 
         for (recordShortName, value) in results:
-            enabledForCalendaring = True
 
-            if self.requireComputerRecord:
-                servicesLocators = value.get(dsattributes.kDSNAttrServicesLocator)
-
-                if servicesLocators:
-                    if type(servicesLocators) is str:
-                        servicesLocators = (servicesLocators,)
-
-                    for locator in servicesLocators:
-                        if locator in self.servicetags:
-                            break
-                    else:
-                        if allowForACLs():
-                            disableForCalendaring(recordShortName)
-                            enabledForCalendaring = False
-                        else:
-                            invalidRecord(recordShortName)
-                            continue
-                else:
-                    if allowForACLs():
-                        disableForCalendaring(recordShortName)
-                        enabledForCalendaring = False
-                    else:
-                        invalidRecord(recordShortName)
-                        continue
-
             # Now get useful record info.
             recordGUID     = value.get(dsattributes.kDS1AttrGeneratedUID)
             recordFullName = value.get(dsattributes.kDS1AttrDistinguishedName)
+            recordEmailAddresses = _setFromAttribute(value.get(dsattributes.kDSNAttrEMailAddress), lower=True)
             recordNodeName = value.get(dsattributes.kDSNAttrMetaNodeLocation)
 
             if not recordGUID:
@@ -772,12 +498,6 @@
                                % (recordType, recordShortName, recordNodeName))
                 continue
 
-            # Get calendar user addresses from directory record.
-            if enabledForCalendaring:
-                calendarUserAddresses = self._calendarUserAddresses(recordType, recordShortName, value)
-            else:
-                calendarUserAddresses = ()
-
             # Special case for groups, which have members.
             if recordType == DirectoryService.recordType_groups:
                 memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
@@ -793,22 +513,6 @@
             else:
                 memberGUIDs = ()
 
-            # Special case for resources and locations
-            autoSchedule = False
-            proxyGUIDs = ()
-            readOnlyProxyGUIDs = ()
-            if recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
-                resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
-                if resourceInfo is not None:
-                    try:
-                        autoSchedule, proxy, read_only_proxy = self._parseResourceInfo(resourceInfo, recordGUID, recordType, recordShortName)
-                    except ValueError:
-                        continue
-                    if proxy:
-                        proxyGUIDs = (proxy,)
-                    if read_only_proxy:
-                        readOnlyProxyGUIDs = (read_only_proxy,)
-
             record = OpenDirectoryRecord(
                 service               = self,
                 recordType            = recordType,
@@ -816,14 +520,16 @@
                 nodeName              = recordNodeName,
                 shortName             = recordShortName,
                 fullName              = recordFullName,
-                calendarUserAddresses = calendarUserAddresses,
-                autoSchedule          = autoSchedule,
-                enabledForCalendaring = enabledForCalendaring,
+                emailAddresses        = recordEmailAddresses,
                 memberGUIDs           = memberGUIDs,
-                proxyGUIDs            = proxyGUIDs,
-                readOnlyProxyGUIDs    = readOnlyProxyGUIDs,
             )
 
+            # Look up augment information
+            # TODO: this needs to be deferred but for now we hard code the deferred result because
+            # we know it is completing immediately.
+            d = augment.AugmentService.getAugmentRecord(record.guid)
+            d.addCallback(lambda x:record.addAugmentInformation(x))
+
             # Check for disabled items
             if record.shortName in disabledNames or record.guid in disabledGUIDs:
                 disableRecord(record)
@@ -851,11 +557,6 @@
                     if recordType == DirectoryService.recordType_groups:
                         self._indexGroup(record, record._memberGUIDs, groupsForGUID)
 
-                    # Do proxy indexing if needed
-                    elif recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
-                        self._indexGroup(record, record._proxyGUIDs, proxiesForGUID)
-                        self._indexGroup(record, record._readOnlyProxyGUIDs, readOnlyProxiesForGUID)
-
         #
         # Replace the entire cache
         #
@@ -873,11 +574,6 @@
         if recordType == DirectoryService.recordType_groups:
             storage["groupsForGUID"] = groupsForGUID
 
-        # Add proxy indexing if needed
-        elif recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
-            storage["proxiesForGUID"] = proxiesForGUID
-            storage["readOnlyProxiesForGUID"] = readOnlyProxiesForGUID
-
         self._records[recordType] = storage
 
         self.log_info(
@@ -894,7 +590,6 @@
             dsattributes.kDSNAttrMetaNodeLocation,
         ]
 
-        query = None
         if recordType == DirectoryService.recordType_users:
             listRecordType = dsattributes.kDSStdRecordTypeUsers
 
@@ -905,128 +600,24 @@
 
         elif recordType == DirectoryService.recordType_locations:
             listRecordType = dsattributes.kDSStdRecordTypePlaces
-            attrs.append(dsattributes.kDSNAttrResourceInfo)
         
         elif recordType == DirectoryService.recordType_resources:
             listRecordType = dsattributes.kDSStdRecordTypeResources
-            attrs.append(dsattributes.kDSNAttrResourceInfo)
         
         else:
             raise UnknownRecordTypeError("Unknown Open Directory record type: %s" % (recordType))
 
-        if self.requireComputerRecord:
-            if self.isWorkgroupServer and recordType == DirectoryService.recordType_users:
-                self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                    self.directory,
-                    dsattributes.kDSNAttrRecordName,
-                    saclGroup,
-                    dsattributes.eDSExact,
-                    False,
-                    dsattributes.kDSStdRecordTypeGroups,
-                    [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups],
-                ))
-                results = opendirectory.queryRecordsWithAttribute_list(
-                    self.directory,
-                    dsattributes.kDSNAttrRecordName,
-                    saclGroup,
-                    dsattributes.eDSExact,
-                    False,
-                    dsattributes.kDSStdRecordTypeGroups,
-                    [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
-                )
-
-                if len(results) == 1:
-                    members      = results[0][1].get(dsattributes.kDSNAttrGroupMembers, [])
-                    nestedGroups = results[0][1].get(dsattributes.kDSNAttrNestedGroups, [])
-                else:
-                    members = []
-                    nestedGroups = []
-
-                guidQueries = []
-
-                for GUID in self._expandGroupMembership(members, nestedGroups):
-                    guidQueries.append(
-                        dsquery.match(dsattributes.kDS1AttrGeneratedUID, GUID, dsattributes.eDSExact)
-                    )
-
-                if not guidQueries:
-                    self.log_warn("No SACL enabled users found.")
-                    return ()
-
-                query = dsquery.expression(dsquery.expression.OR, guidQueries)
-
-            #
-            # For users and groups, we'll load all entries, even if
-            # they don't have a services locator for this server.
-            #
-            elif (
-                recordType != DirectoryService.recordType_users and
-                recordType != DirectoryService.recordType_groups
-            ):
-                tag_queries = []
-
-                for tag in self.servicetags:
-                    tag_queries.append(dsquery.match(dsattributes.kDSNAttrServicesLocator, tag, dsattributes.eDSExact))
-
-                if len(tag_queries) == 1:
-                    subquery = tag_queries[0]
-                else:
-                    subquery = dsquery.expression(dsquery.expression.OR, tag_queries)
-
-                if query is None:
-                    query = subquery
-                else:
-                    query = dsquery.expression(dsquery.expression.AND, (subquery, query))
-
-        subquery = None
-
         try:
-            if query:
-                if isinstance(query, dsquery.match):
-                    self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                        self.directory,
-                        query.attribute,
-                        query.value,
-                        query.matchType,
-                        False,
-                        listRecordType,
-                        attrs,
-                    ))
-                    results = opendirectory.queryRecordsWithAttribute_list(
-                        self.directory,
-                        query.attribute,
-                        query.value,
-                        query.matchType,
-                        False,
-                        listRecordType,
-                        attrs,
-                    )
-                else:
-                    self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r)" % (
-                        self.directory,
-                        query.generate(),
-                        False,
-                        listRecordType,
-                        attrs,
-                    ))
-                    results = opendirectory.queryRecordsWithAttributes_list(
-                        self.directory,
-                        query.generate(),
-                        False,
-                        listRecordType,
-                        attrs,
-                    )
-            else:
-                self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
-                    self.directory,
-                    listRecordType,
-                    attrs,
-                ))
-                results = opendirectory.listAllRecordsWithAttributes_list(
-                    self.directory,
-                    listRecordType,
-                    attrs,
-                )
+            self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
+                self.directory,
+                listRecordType,
+                attrs,
+            ))
+            results = opendirectory.listAllRecordsWithAttributes_list(
+                self.directory,
+                listRecordType,
+                attrs,
+            )
         except opendirectory.ODError, ex:
             self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
             raise
@@ -1039,8 +630,7 @@
     """
     def __init__(
         self, service, recordType, guid, nodeName, shortName, fullName,
-        calendarUserAddresses, autoSchedule, enabledForCalendaring,
-        memberGUIDs, proxyGUIDs, readOnlyProxyGUIDs,
+        emailAddresses, memberGUIDs,
     ):
         super(OpenDirectoryRecord, self).__init__(
             service               = service,
@@ -1048,14 +638,10 @@
             guid                  = guid,
             shortName             = shortName,
             fullName              = fullName,
-            calendarUserAddresses = calendarUserAddresses,
-            autoSchedule          = autoSchedule,
-            enabledForCalendaring = enabledForCalendaring,
+            emailAddresses        = emailAddresses,
         )
         self.nodeName = nodeName
         self._memberGUIDs = tuple(memberGUIDs)
-        self._proxyGUIDs = tuple(proxyGUIDs)
-        self._readOnlyProxyGUIDs = tuple(readOnlyProxyGUIDs)
 
     def __repr__(self):
         if self.service.realmName == self.nodeName:
@@ -1085,41 +671,6 @@
     def groups(self):
         return self.service.groupsForGUID(self.guid)
 
-    def proxies(self):
-        if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
-            return
-
-        for guid in self._proxyGUIDs:
-            proxyRecord = self.service.recordWithGUID(guid)
-            if proxyRecord is None:
-                self.log_error("No record for proxy in %s with GUID %s" % (self.shortName, guid))
-            else:
-                yield proxyRecord
-
-    def proxyFor(self):
-        result = set()
-        result.update(self.service.proxiesForGUID(DirectoryService.recordType_resources, self.guid))
-        result.update(self.service.proxiesForGUID(DirectoryService.recordType_locations, self.guid))
-        return result
-
-    def readOnlyProxies(self):
-        if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
-            return
-
-        for guid in self._readOnlyProxyGUIDs:
-            proxyRecord = self.service.recordWithGUID(guid)
-            if proxyRecord is None:
-                self.log_error("No record for proxy in %s with GUID %s" % (self.shortName, guid))
-            else:
-                yield proxyRecord
-
-    def readOnlyProxyFor(self):
-        result = set()
-        result.update(self.service.readOnlyProxiesForGUID(DirectoryService.recordType_resources, self.guid))
-        result.update(self.service.readOnlyProxiesForGUID(DirectoryService.recordType_locations, self.guid))
-        return result
-
-
     def getMemcacheKey(self, shortName):
         key = "auth-%s" % (md5(shortName).hexdigest(),)
         return key

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/augment.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/augment.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/augment.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/augment.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,353 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin,\
+    ADBAPIPostgreSQLMixin
+from twistedcaldav.directory.xmlaugmentsparser import XMLAugmentsParser
+import copy
+import time
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+class AugmentRecord(object):
+    """
+    Augmented directory record information
+    """
+
+    def __init__(
+        self,
+        guid,
+        enabled=False,
+        hostedAt="",
+        enabledForCalendaring=False,
+        autoSchedule=False,
+    ):
+        self.guid = guid
+        self.enabled = enabled
+        self.hostedAt = hostedAt
+        self.enabledForCalendaring = enabledForCalendaring
+        self.autoSchedule = autoSchedule
+
+class AugmentDB(object):
+    """
+    Abstract base class for an augment record database.
+    """
+    
+    def __init__(self):
+        pass
+    
+    @inlineCallbacks
+    def getAugmentRecord(self, guid):
+        """
+        Get an AugmentRecord for the specified GUID or the default.
+
+        @param guid: directory GUID to lookup
+        @type guid: C{str}
+        
+        @return: L{Deferred}
+        """
+        
+        result = (yield self._lookupAugmentRecord(guid))
+        if result is None:
+            if not hasattr(self, "_defaultRecord"):
+                self._defaultRecord = (yield self._lookupAugmentRecord("Default"))
+            if self._defaultRecord is not None:
+                result = copy.deepcopy(self._defaultRecord)
+                result.guid = guid
+        returnValue(result)
+
+    @inlineCallbacks
+    def getAllGUIDs(self):
+        """
+        Get all AugmentRecord GUIDs.
+
+        @return: L{Deferred}
+        """
+        
+        raise NotImplementedError("Child class must define this.")
+
+    def _lookupAugmentRecord(self, guid):
+        """
+        Get an AugmentRecord for the specified GUID.
+
+        @param guid: directory GUID to lookup
+        @type guid: C{str}
+        
+        @return: L{Deferred}
+        """
+        
+        raise NotImplementedError("Child class must define this.")
+
+    def refresh(self):
+        """
+        Refresh any cached data.
+        """
+        pass
+        
+AugmentService = AugmentDB()   # Global augment service
+
+
+class AugmentXMLDB(AugmentDB):
+    """
+    XMLFile based augment database implementation.
+    """
+    
+    def __init__(self, xmlFiles, cacheTimeout=30):
+        
+        self.xmlFiles = xmlFiles
+        self.cacheTimeout = cacheTimeout * 60 # Value is mins we want secs
+        self.lastCached = 0
+        self.db = {}
+        
+        try:
+            self.db = self._parseXML()
+        except RuntimeError:
+            log.error("Failed to parse XML augments file - fatal error on startup")
+            raise
+            
+        self.lastCached = time.time()
+
+    @inlineCallbacks
+    def getAllGUIDs(self):
+        """
+        Get all AugmentRecord GUIDs.
+
+        @return: L{Deferred}
+        """
+        
+        return succeed(self.db.keys())
+
+    def _lookupAugmentRecord(self, guid):
+        """
+        Get an AugmentRecord for the specified GUID.
+
+        @param guid: directory GUID to lookup
+        @type guid: C{str}
+        
+        @return: L{Deferred}
+        """
+        
+        # May need to re-cache
+        if self.lastCached + self.cacheTimeout <= time.time():
+            self.refresh()
+            
+        return succeed(self.db.get(guid))
+
+    def refresh(self):
+        """
+        Refresh any cached data.
+        """
+        try:
+            self.db = self._parseXML()
+        except RuntimeError:
+            log.error("Failed to parse XML augments file during cache refresh - ignoring")
+        self.lastCached = time.time()
+
+    def _parseXML(self):
+        
+        # Do each file
+        results = {}
+        for xmlFile in self.xmlFiles:
+            
+            # Creating a parser does the parse
+            XMLAugmentsParser(xmlFile, results)
+        
+        return results
+
+class AugmentADAPI(AugmentDB, AbstractADBAPIDatabase):
+    """
+    DBAPI based augment database implementation.
+    """
+
+    schema_version = "1"
+    schema_type    = "AugmentDB"
+    
+    def __init__(self, dbID, dbapiName, dbapiArgs, **kwargs):
+        
+        self.cachedPartitions = {}
+        self.cachedHostedAt = {}
+        
+        AbstractADBAPIDatabase.__init__(self, dbID, dbapiName, dbapiArgs, True, **kwargs)
+        
+    @inlineCallbacks
+    def getAllGUIDs(self):
+        """
+        Get all AugmentRecord GUIDs.
+
+        @return: L{Deferred}
+        """
+        
+        # Query for the record information
+        results = (yield self.queryList("select GUID from AUGMENTS", ()))
+        returnValue(results)
+
+    @inlineCallbacks
+    def _lookupAugmentRecord(self, guid):
+        """
+        Get an AugmentRecord for the specified GUID.
+
+        @param guid: directory GUID to lookup
+        @type guid: C{str}
+
+        @return: L{Deferred}
+        """
+        
+        # Query for the record information
+        results = (yield self.query("select GUID, ENABLED, PARTITIONID, CALENDARING, AUTOSCHEDULE from AUGMENTS where GUID = :1", (guid,)))
+        if not results:
+            returnValue(None)
+        else:
+            guid, enabled, partitionid, enabdledForCalendaring, autoSchedule = results[0]
+            
+            record = AugmentRecord(
+                guid = guid,
+                enabled = enabled == "T",
+                hostedAt = (yield self._getPartition(partitionid)),
+                enabledForCalendaring = enabdledForCalendaring == "T",
+                autoSchedule = autoSchedule == "T",
+            )
+            
+            returnValue(record)
+
+    @inlineCallbacks
+    def addAugmentRecord(self, record, update=False):
+
+        partitionid = (yield self._getPartitionID(record.hostedAt))
+        
+        if update:
+            yield self.execute(
+                """update AUGMENTS set
+                   (GUID, ENABLED, PARTITIONID, CALENDARING, AUTOSCHEDULE) =
+                   (:1, :2, :3, :4, :5) where GUID = :6""",
+                (
+                    record.guid,
+                    "T" if record.enabled else "F",
+                    partitionid,
+                    "T" if record.enabledForCalendaring else "F",
+                    "T" if record.autoSchedule else "F",
+                    record.guid,
+                )
+            )
+        else:
+            yield self.execute(
+                """insert into AUGMENTS
+                   (GUID, ENABLED, PARTITIONID, CALENDARING, AUTOSCHEDULE)
+                   values (:1, :2, :3, :4, :5)""",
+                (
+                    record.guid,
+                    "T" if record.enabled else "F",
+                    partitionid,
+                    "T" if record.enabledForCalendaring else "F",
+                    "T" if record.autoSchedule else "F",
+                )
+            )
+
+    def removeAugmentRecord(self, guid):
+
+        return self.query("delete from AUGMENTS where GUID = :1", (guid,))
+
+    @inlineCallbacks
+    def _getPartitionID(self, hostedat, createIfMissing=True):
+        
+        # We will use a cache for these as we do not expect changes whilst running
+        try:
+            returnValue(self.cachedHostedAt[hostedat])
+        except KeyError:
+            pass
+
+        partitionid = (yield self.queryOne("select PARTITIONID from PARTITIONS where HOSTEDAT = :1", (hostedat,)))
+        if partitionid == None:
+            yield self.execute("insert into PARTITIONS (HOSTEDAT) values (:1)", (hostedat,))
+            partitionid = (yield self.queryOne("select PARTITIONID from PARTITIONS where HOSTEDAT = :1", (hostedat,)))
+        self.cachedHostedAt[hostedat] = partitionid
+        returnValue(partitionid)
+
+    @inlineCallbacks
+    def _getPartition(self, partitionid):
+        
+        # We will use a cache for these as we do not expect changes whilst running
+        try:
+            returnValue(self.cachedPartitions[partitionid])
+        except KeyError:
+            pass
+
+        partition = (yield self.queryOne("select HOSTEDAT from PARTITIONS where PARTITIONID = :1", (partitionid,)))
+        self.cachedPartitions[partitionid] = partition
+        returnValue(partition)
+
+    def _db_version(self):
+        """
+        @return: the schema version assigned to this index.
+        """
+        return AugmentADAPI.schema_version
+        
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this index.
+        """
+        return AugmentADAPI.schema_type
+    
+    @inlineCallbacks
+    def _db_init_data_tables(self):
+        """
+        Initialize the underlying database tables.
+        """
+
+        #
+        # TESTTYPE table
+        #
+        yield self._create_table("AUGMENTS", (
+            ("GUID",         "text unique"),
+            ("ENABLED",      "text(1)"),
+            ("PARTITIONID",  "text"),
+            ("CALENDARING",  "text(1)"),
+            ("AUTOSCHEDULE", "text(1)"),
+        ))
+
+        yield self._create_table("PARTITIONS", (
+            ("PARTITIONID",   "serial"),
+            ("HOSTEDAT",      "text"),
+        ))
+
+    @inlineCallbacks
+    def _db_empty_data_tables(self):
+        yield self._db_execute("delete from AUGMENTS")
+        yield self._db_execute("delete from PARTITIONS")
+
+class AugmentSqliteDB(ADBAPISqliteMixin, AugmentADAPI):
+    """
+    Sqlite based augment database implementation.
+    """
+
+    def __init__(self, dbpath):
+        
+        ADBAPISqliteMixin.__init__(self)
+        AugmentADAPI.__init__(self, "Augments", "sqlite3", (dbpath,))
+
+class AugmentPostgreSQLDB(ADBAPIPostgreSQLMixin, AugmentADAPI):
+    """
+    PostgreSQL based augment database implementation.
+    """
+
+    def __init__(self, host, database, user=None, password=None):
+        
+        ADBAPIPostgreSQLMixin.__init__(self)
+        AugmentADAPI.__init__(self, "Augments", "pgdb", (), host=host, database=database, user=user, password=password,)
+

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxy.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxy.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -20,6 +20,10 @@
 
 __all__ = [
     "CalendarUserProxyPrincipalResource",
+    "ProxyDB",
+    "ProxyDBService",
+    "ProxySqliteDB",
+    "ProxyPostgreSQLDB",
 ]
 
 from twisted.internet.defer import returnValue
@@ -32,15 +36,17 @@
 from twisted.web2.dav.noneprops import NonePropertyStore
 
 from twistedcaldav.config import config
+from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin,\
+    ADBAPIPostgreSQLMixin
 from twistedcaldav.extensions import DAVFile, DAVPrincipalResource
 from twistedcaldav.extensions import ReadOnlyWritePropertiesResourceMixIn
 from twistedcaldav.memcacher import Memcacher
 from twistedcaldav.resource import CalDAVComplianceMixIn
 from twistedcaldav.directory.util import NotFilePath
-from twistedcaldav.sql import AbstractSQLDatabase
+from twistedcaldav.log import LoggingMixIn
 
 import itertools
-import os
+import time
 
 class PermissionsMixIn (ReadOnlyWritePropertiesResourceMixIn):
     def defaultAccessControlList(self):
@@ -59,17 +65,19 @@
         )
 
         # Add admins
-        aces += tuple([davxml.ACE(
-                    davxml.Principal(davxml.HRef(principal)),
-                    davxml.Grant(davxml.Privilege(davxml.All())),
-                    davxml.Protected(),
-                 ) for principal in config.AdminPrincipals
-                ])
+        aces += tuple((
+            davxml.ACE(
+                davxml.Principal(davxml.HRef(principal)),
+                davxml.Grant(davxml.Privilege(davxml.All())),
+                davxml.Protected(),
+            )
+            for principal in config.AdminPrincipals
+        ))
 
         return davxml.ACL(*aces)
 
     def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
-        # Permissions here are fixed, and are not subject to inherritance rules, etc.
+        # Permissions here are fixed, and are not subject to inheritance rules, etc.
         return succeed(self.defaultAccessControlList())
 
 class CalendarUserProxyPrincipalResource (CalDAVComplianceMixIn, PermissionsMixIn, DAVPrincipalResource, DAVFile):
@@ -116,14 +124,10 @@
         """
         Return the SQL database for this group principal.
 
-        @return: the L{CalendarUserProxyDatabase} for the principal collection.
+        @return: the L{ProxyDB} for the principal collection.
         """
+        return ProxyDBService
 
-        # The db is located in the principal collection root
-        if not hasattr(self.pcollection, "calendar_user_proxy_db"):
-            setattr(self.pcollection, "calendar_user_proxy_db", CalendarUserProxyDatabase(config.DataRoot))
-        return self.pcollection.calendar_user_proxy_db
-
     def resourceType(self):
         if self.proxyType == "calendar-proxy-read":
             return davxml.ResourceType.calendarproxyread
@@ -156,15 +160,7 @@
         assert isinstance(property, davxml.WebDAVElement)
 
         if property.qname() == (dav_namespace, "group-member-set"):
-            if self.hasEditableMembership():
-                return self.setGroupMemberSet(property, request)
-            else:
-                raise HTTPError(
-                    StatusResponse(
-                        responsecode.FORBIDDEN,
-                        "Proxies cannot be changed."
-                    )
-                )
+            return self.setGroupMemberSet(property, request)
 
         return super(CalendarUserProxyPrincipalResource, self).writeProperty(property, request)
 
@@ -182,8 +178,9 @@
         # Break out the list into a set of URIs.
         members = [str(h) for h in new_members.children]
 
-        # Map the URIs to principals.
+        # Map the URIs to principals and a set of UIDs.
         principals = []
+        newUIDs = set()
         for uri in members:
             principal = self.pcollection._principalForURI(uri)
             # Invalid principals MUST result in an error.
@@ -193,14 +190,31 @@
                     "Attempt to use a non-existent principal %s as a group member of %s." % (uri, self.principalURL(),)
                 ))
             principals.append(principal)
-            yield principal.cacheNotifier.changed()
+            newUIDs.add(principal.principalUID())
 
+        # Get the old set of UIDs
+        oldUIDs = (yield self._index().getMembers(self.uid))
+        
+        # Change membership
+        yield self.setGroupMemberSetPrincipals(principals)
+        
+        # Invalidate the primary principal's cache, and any principal's whose
+        # membership status changed
+        yield self.parent.cacheNotifier.changed()
+        
+        changedUIDs = newUIDs.symmetric_difference(oldUIDs)
+        for uid in changedUIDs:
+            principal = self.pcollection.principalForUID(uid)
+            if principal:
+                yield principal.cacheNotifier.changed()
+            
+        returnValue(True)
+
+    @inlineCallbacks
+    def setGroupMemberSetPrincipals(self, principals):
         # Map the principals to UIDs.
         uids = [p.principalUID() for p in principals]
-
         yield self._index().setGroupMembers(self.uid, uids)
-        yield self.parent.cacheNotifier.changed()
-        returnValue(True)
 
     ##
     # HTTP
@@ -246,8 +260,7 @@
                 """Principal UID: %s\n"""          % (self.principalUID(),),
                 """Principal URL: %s\n"""          % (format_link(self.principalURL()),),
                 """\nAlternate URIs:\n"""          , format_list(format_link(u) for u in self.alternateURIs()),
-                """\nGroup members (%s):\n""" % ({False:"Locked", True:"Editable"}[self.hasEditableMembership()])
-                                                   , format_principals(closure["members"]),
+                """\nGroup members:\n"""           , format_principals(closure["members"]),
                 """\nGroup memberships:\n"""       , format_principals(closure["memberships"]),
                 """</pre></blockquote></div>""",
                 closure["output"]
@@ -307,17 +320,28 @@
 
     @inlineCallbacks
     def _directGroupMembers(self):
-        if self.hasEditableMembership():
-            # Get member UIDs from database and map to principal resources
-            members = yield self._index().getMembers(self.uid)
-            returnValue([p for p in [self.pcollection.principalForUID(uid) for uid in members] if p])
-        else:
-            # Fixed proxies
-            if self.proxyType == "calendar-proxy-write":
-                returnValue(self.parent.proxies())
+        # Get member UIDs from database and map to principal resources
+        members = yield self._index().getMembers(self.uid)
+        found = []
+        missing = []
+        for uid in members:
+            p = self.pcollection.principalForUID(uid)
+            if p:
+                found.append(p)
+                # Make sure any outstanding deletion timer entries for
+                # existing principals are removed
+                yield self._index().refreshPrincipal(uid)
             else:
-                returnValue(self.parent.readOnlyProxies())
+                missing.append(uid)
 
+        # Clean-up ones that are missing
+        for uid in missing:
+            cacheTimeout = config.DirectoryService.params.get("cacheTimeout", 30) * 60 # in seconds
+
+            yield self._index().removePrincipal(uid, delay=cacheTimeout*2)
+
+        returnValue(found)
+
     def groupMembers(self):
         return self._expandMemberUIDs()
 
@@ -327,10 +351,7 @@
         memberships = yield self._index().getMemberships(self.uid)
         returnValue([p for p in [self.pcollection.principalForUID(uid) for uid in memberships] if p])
 
-    def hasEditableMembership(self):
-        return self.parent.hasEditableProxyMembership()
-
-class CalendarUserProxyDatabase(AbstractSQLDatabase):
+class ProxyDB(AbstractADBAPIDatabase, LoggingMixIn):
     """
     A database to maintain calendar user proxy group memberships.
 
@@ -342,17 +363,16 @@
 
     """
 
-    dbType = "CALENDARUSERPROXY"
-    dbFilename = "calendaruserproxy.sqlite"
-    dbFormatVersion = "4"
-
+    schema_version = "4"
+    schema_type    = "ProxyDB"
+    
     class ProxyDBMemcacher(Memcacher):
         
         def setMembers(self, guid, members):
-            return self.set("members:%s" % (guid,), str(",".join(members)))
+            return self.set("members:%s" % (str(guid),), str(",".join(members)))
 
         def setMemberships(self, guid, memberships):
-            return self.set("memberships:%s" % (guid,), str(",".join(memberships)))
+            return self.set("memberships:%s" % (str(guid),), str(",".join(memberships)))
 
         def getMembers(self, guid):
             def _value(value):
@@ -362,7 +382,7 @@
                     return None
                 else:
                     return set()
-            d = self.get("members:%s" % (guid,))
+            d = self.get("members:%s" % (str(guid),))
             d.addCallback(_value)
             return d
 
@@ -374,21 +394,47 @@
                     return None
                 else:
                     return set()
-            d = self.get("memberships:%s" % (guid,))
+            d = self.get("memberships:%s" % (str(guid),))
             d.addCallback(_value)
             return d
 
         def deleteMember(self, guid):
-            return self.delete("members:%s" % (guid,))
+            return self.delete("members:%s" % (str(guid),))
 
         def deleteMembership(self, guid):
-            return self.delete("memberships:%s" % (guid,))
+            return self.delete("memberships:%s" % (str(guid),))
 
-    def __init__(self, path):
-        path = os.path.join(path, CalendarUserProxyDatabase.dbFilename)
-        super(CalendarUserProxyDatabase, self).__init__(path, True)
+        def setDeletionTimer(self, guid, delay):
+            return self.set("del:%s" % (str(guid),), str(self.getTime()+delay))
+
+        def checkDeletionTimer(self, guid):
+            # True means it's overdue, False means it's not, None means no timer
+            def _value(value):
+                if value:
+                    if int(value) <= self.getTime():
+                        return True
+                    else:
+                        return False
+                else:
+                    return None
+            d = self.get("del:%s" % (str(guid),))
+            d.addCallback(_value)
+            return d
+
+        def clearDeletionTimer(self, guid):
+            return self.delete("del:%s" % (str(guid),))
+
+        def getTime(self):
+            if hasattr(self, 'theTime'):
+                theTime = self.theTime
+            else:
+                theTime = int(time.time())
+            return theTime
+
+    def __init__(self, dbID, dbapiName, dbapiArgs, **kwargs):
+        AbstractADBAPIDatabase.__init__(self, dbID, dbapiName, dbapiArgs, True, **kwargs)
         
-        self._memcacher = CalendarUserProxyDatabase.ProxyDBMemcacher("proxyDB")
+        self._memcacher = ProxyDB.ProxyDBMemcacher("ProxyDB")
 
     @inlineCallbacks
     def setGroupMembers(self, principalUID, members):
@@ -405,21 +451,46 @@
             current_members = ()
         current_members = set(current_members)
 
-        # Remove what is there, then add it back.
-        self._delete_from_db(principalUID)
-        self._add_to_db(principalUID, members)
-        self._db_commit()
-        
-        # Update cache
+        # Find changes
         update_members = set(members)
-        
         remove_members = current_members.difference(update_members)
         add_members = update_members.difference(current_members)
+
+        yield self.changeGroupMembersInDatabase(principalUID, add_members, remove_members)
+
+        # Update cache
         for member in itertools.chain(remove_members, add_members,):
             _ignore = yield self._memcacher.deleteMembership(member)
         _ignore = yield self._memcacher.deleteMember(principalUID)
 
     @inlineCallbacks
+    def setGroupMembersInDatabase(self, principalUID, members):
+        """
+        A blocking call to add a group membership record in the database.
+
+        @param principalUID: the UID of the group principal to add.
+        @param members: a list UIDs of principals that are members of this group.
+        """
+        # Remove what is there, then add it back.
+        yield self._delete_from_db(principalUID)
+        yield self._add_to_db(principalUID, members)
+        
+    @inlineCallbacks
+    def changeGroupMembersInDatabase(self, principalUID, addMembers, removeMembers):
+        """
+        A blocking call to add a group membership record in the database.
+
+        @param principalUID: the UID of the group principal to add.
+        @param addMembers: a list UIDs of principals to be added as members of this group.
+        @param removeMembers: a list UIDs of principals to be removed as members of this group.
+        """
+        # Remove what is there, then add it back.
+        for member in removeMembers:
+            yield self._delete_from_db_one(principalUID, member)
+        for member in addMembers:
+            yield self._add_to_db_one(principalUID, member)
+        
+    @inlineCallbacks
     def removeGroup(self, principalUID):
         """
         Remove a group membership record.
@@ -427,34 +498,95 @@
         @param principalUID: the UID of the group principal to remove.
         """
 
-        self._delete_from_db(principalUID)
-        self._db_commit()
+        # Need to get the members before we do the delete
+        members = yield self.getMembers(principalUID)
+
+        yield self._delete_from_db(principalUID)
         
         # Update cache
-        members = yield self.getMembers(principalUID)
         if members:
             for member in members:
                 yield self._memcacher.deleteMembership(member)
             yield self._memcacher.deleteMember(principalUID)
 
     @inlineCallbacks
+    def removePrincipal(self, principalUID, delay=None):
+        """
+        Remove a group membership record.
+
+        @param principalUID: the UID of the principal to remove.
+        """
+
+        if delay:
+            # We are going to remove the principal only after <delay> seconds
+            # has passed since we first chose to remove it, to protect against
+            # transient directory problems.
+            # If <delay> is specified, first see if there was a timer set
+            # previously.  If the timer is more than delay seconds old, we
+            # go ahead and remove the principal.  Otherwise, do nothing.
+
+            overdue = yield self._memcacher.checkDeletionTimer(principalUID)
+
+            if overdue == False:
+                # Do nothing
+                returnValue(None)
+
+            elif overdue is None:
+                # No timer was previously set
+                self.log_debug("Delaying removal of missing proxy principal '%s'" %
+                    (principalUID,))
+                yield self._memcacher.setDeletionTimer(principalUID, delay=delay)
+                returnValue(None)
+
+        self.log_warn("Removing missing proxy principal for '%s'" %
+            (principalUID,))
+
+        for suffix in ("calendar-proxy-read", "calendar-proxy-write",):
+            groupUID = "%s#%s" % (principalUID, suffix,)
+            yield self._delete_from_db(groupUID)
+
+            # Update cache
+            members = yield self.getMembers(groupUID)
+            if members:
+                for member in members:
+                    yield self._memcacher.deleteMembership(member)
+                yield self._memcacher.deleteMember(groupUID)
+
+        memberships = (yield self.getMemberships(principalUID))
+        for groupUID in memberships:
+            yield self._memcacher.deleteMember(groupUID)
+
+        yield self._delete_from_db_member(principalUID)
+        yield self._memcacher.deleteMembership(principalUID)
+        yield self._memcacher.clearDeletionTimer(principalUID)
+
+    def refreshPrincipal(self, principalUID):
+        """
+        Bring back to life a principal that was previously deleted.
+
+        @param principalUID:
+        @type principalUID:
+        """
+        
+        return self._memcacher.clearDeletionTimer(principalUID)
+
+    @inlineCallbacks
     def getMembers(self, principalUID):
         """
         Return the list of group member UIDs for the specified principal.
-        
+
         @return: a deferred returning a C{set} of members.
         """
 
+        @inlineCallbacks
         def _members():
-            members = set()
-            for row in self._db_execute("select MEMBER from GROUPS where GROUPNAME = :1", principalUID):
-                members.add(row[0])
-            return members
+            result = set([row[0] for row in (yield self.query("select MEMBER from GROUPS where GROUPNAME = :1", (principalUID,)))])
+            returnValue(result)
 
         # Pull from cache
         result = yield self._memcacher.getMembers(principalUID)
         if result is None:
-            result = _members()
+            result = (yield _members())
             yield self._memcacher.setMembers(principalUID, result)
         returnValue(result)
 
@@ -466,19 +598,19 @@
         @return: a deferred returning a C{set} of memberships.
         """
 
+        @inlineCallbacks
         def _members():
-            members = set()
-            for row in self._db_execute("select GROUPNAME from GROUPS where MEMBER = :1", principalUID):
-                members.add(row[0])
-            return members
+            result = set([row[0] for row in (yield self.query("select GROUPNAME from GROUPS where MEMBER = :1", (principalUID,)))])
+            returnValue(result)
 
         # Pull from cache
         result = yield self._memcacher.getMemberships(principalUID)
         if result is None:
-            result = _members()
+            result = (yield _members())
             yield self._memcacher.setMemberships(principalUID, result)
         returnValue(result)
 
+    @inlineCallbacks
     def _add_to_db(self, principalUID, members):
         """
         Insert the specified entry into the database.
@@ -487,34 +619,66 @@
         @param members: a list of UIDs or principals that are members of this group.
         """
         for member in members:
-            self._db_execute(
+            yield self.execute(
                 """
                 insert into GROUPS (GROUPNAME, MEMBER)
                 values (:1, :2)
-                """, principalUID, member
+                """, (principalUID, member,)
             )
 
+    def _add_to_db_one(self, principalUID, memberUID):
+        """
+        Insert the specified entry into the database.
+
+        @param principalUID: the UID of the group principal to add.
+        @param memberUID: the UID of the principal that is being added as a member of this group.
+        """
+        return self.execute(
+            """
+            insert into GROUPS (GROUPNAME, MEMBER)
+            values (:1, :2)
+            """, (principalUID, memberUID,)
+        )
+
     def _delete_from_db(self, principalUID):
         """
         Deletes the specified entry from the database.
 
         @param principalUID: the UID of the group principal to remove.
         """
-        self._db_execute("delete from GROUPS where GROUPNAME = :1", principalUID)
+        return self.execute("delete from GROUPS where GROUPNAME = :1", (principalUID,))
 
+    def _delete_from_db_one(self, principalUID, memberUID):
+        """
+        Deletes the specified entry from the database.
+
+        @param principalUID: the UID of the group principal to remove.
+        @param memberUID: the UID of the principal that is being removed as a member of this group.
+        """
+        return self.execute("delete from GROUPS where GROUPNAME = :1 and MEMBER = :2", (principalUID, memberUID,))
+
+    def _delete_from_db_member(self, principalUID):
+        """
+        Deletes the specified member entry from the database.
+
+        @param principalUID: the UID of the member principal to remove.
+        """
+        return self.execute("delete from GROUPS where MEMBER = :1", (principalUID,))
+
     def _db_version(self):
         """
         @return: the schema version assigned to this index.
         """
-        return CalendarUserProxyDatabase.dbFormatVersion
+        return ProxyDB.schema_version
 
     def _db_type(self):
         """
         @return: the collection type assigned to this index.
         """
-        return CalendarUserProxyDatabase.dbType
+        return ProxyDB.schema_type
 
-    def _db_init_data_tables(self, q):
+    @inlineCallbacks
+    def _db_init_data_tables(self):
         """
         Initialise the underlying database tables.
         @param q:           a database cursor to use.
@@ -523,46 +687,90 @@
         #
         # GROUPS table
         #
-        q.execute(
+        yield self._create_table("GROUPS", (
+            ("GROUPNAME", "text"),
+            ("MEMBER",    "text"),
+        ))
+
+        yield self._db_execute(
             """
-            create table GROUPS (
-                GROUPNAME   text,
-                MEMBER      text
-            )
-            """
-        )
-        q.execute(
-            """
             create index GROUPNAMES on GROUPS (GROUPNAME)
             """
         )
-        q.execute(
+        yield self._db_execute(
             """
             create index MEMBERS on GROUPS (MEMBER)
             """
         )
 
-    def _db_upgrade_data_tables(self, q, old_version):
+    @inlineCallbacks
+    def _db_upgrade_data_tables(self, old_version):
         """
         Upgrade the data from an older version of the DB.
-        @param q: a database cursor to use.
         @param old_version: existing DB's version number
         @type old_version: str
         """
 
         # Add index if old version is less than "4"
         if int(old_version) < 4:
-            q.execute(
+            yield self._db_execute(
                 """
                 create index GROUPNAMES on GROUPS (GROUPNAME)
                 """
             )
-            q.execute(
+            yield self._db_execute(
                 """
                 create index MEMBERS on GROUPS (MEMBER)
                 """
             )
 
+    def _db_empty_data_tables(self):
+        """
+        Empty the underlying database tables.
+        @param q:           a database cursor to use.
+        """
+
+        #
+        # GROUPS table
+        #
+        return self._db_execute("delete from GROUPS")
+
+    @inlineCallbacks
+    def clean(self):
+        
+        if not self.initialized:
+            yield self.open()
+
+        for group in [row[0] for row in (yield self.query("select GROUPNAME from GROUPS"))]:
+            self.removeGroup(group)
+        
+        yield super(ProxyDB, self).clean()
+
+
+ProxyDBService = None   # Global proxyDB service
+
+
+class ProxySqliteDB(ADBAPISqliteMixin, ProxyDB):
+    """
+    Sqlite based proxy database implementation.
+    """
+
+    def __init__(self, dbpath):
+        
+        ADBAPISqliteMixin.__init__(self)
+        ProxyDB.__init__(self, "Proxies", "sqlite3", (dbpath,))
+
+class ProxyPostgreSQLDB(ADBAPIPostgreSQLMixin, ProxyDB):
+    """
+    PostgreSQL based augment database implementation.
+    """
+
+    def __init__(self, host, database, user=None, password=None):
+        
+        ADBAPIPostgreSQLMixin.__init__(self, )
+        ProxyDB.__init__(self, "Proxies", "pgdb", (), host=host, database=database, user=user, password=password)
+
+
 ##
 # Utilities
 ##

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxyloader.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/calendaruserproxyloader.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxyloader.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxyloader.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,137 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from xml.etree.ElementTree import ElementTree
+from xml.parsers.expat import ExpatError
+from twistedcaldav.directory import calendaruserproxy
+from twisted.internet.defer import inlineCallbacks
+import types
+
+"""
+XML based calendar user proxy loader.
+"""
+
+__all__ = [
+    "XMLCalendarUserProxyLoader",
+]
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+ELEMENT_PROXIES           = "proxies"
+ELEMENT_RECORD            = "record"
+
+ELEMENT_GUID              = "guid"
+ELEMENT_PROXIES           = "proxies"
+ELEMENT_READ_ONLY_PROXIES = "read-only-proxies"
+ELEMENT_MEMBER            = "member"
+
+ATTRIBUTE_REPEAT          = "repeat"
+
+class XMLCalendarUserProxyLoader(object):
+    """
+    XML calendar user proxy configuration file parser and loader.
+    """
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+    def __init__(self, xmlFile):
+
+        self.items = []
+        self.xmlFile = xmlFile
+
+        # Read in XML
+        try:
+            tree = ElementTree(file=self.xmlFile)
+        except ExpatError, e:
+            log.error("Unable to parse file '%s' because: %s" % (self.xmlFile, e,), raiseException=RuntimeError)
+
+        # Verify that top-level element is correct
+        proxies_node = tree.getroot()
+        if proxies_node.tag != ELEMENT_PROXIES:
+            log.error("Ignoring file '%s' because it is not a proxies file" % (self.xmlFile,), raiseException=RuntimeError)
+
+        self._parseXML(proxies_node)
+
+    def _parseXML(self, rootnode):
+        """
+        Parse the XML root node from the augments configuration document.
+        @param rootnode: the L{Element} to parse.
+        """
+        for child in rootnode.getchildren():
+            
+            if child.tag != ELEMENT_RECORD:
+                log.error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, self.xmlFile,), raiseException=RuntimeError)
+
+            repeat = int(child.get(ATTRIBUTE_REPEAT, "1"))
+
+            guid = None
+            write_proxies = set()
+            read_proxies = set()
+            for node in child.getchildren():
+                
+                if node.tag == ELEMENT_GUID:
+                    guid = node.text
+
+                elif node.tag in (
+                    ELEMENT_PROXIES,
+                    ELEMENT_READ_ONLY_PROXIES,
+                ):
+                    self._parseMembers(node, write_proxies if node.tag == ELEMENT_PROXIES else read_proxies)
+                else:
+                    log.error("Invalid element '%s' in proxies file: '%s'" % (node.tag, self.xmlFile,), raiseException=RuntimeError)
+                    
+            # Must have at least a guid
+            if not guid:
+                log.error("Invalid record '%s' without a guid in proxies file: '%s'" % (child, self.xmlFile,), raiseException=RuntimeError)
+                
+            if repeat > 1:
+                for i in xrange(1, repeat+1):
+                    self._buildRecord(guid, write_proxies, read_proxies, i)
+            else:
+                self._buildRecord(guid, write_proxies, read_proxies)
+
+    def _parseMembers(self, node, addto):
+        for child in node.getchildren():
+            if child.tag == ELEMENT_MEMBER:
+                addto.add(child.text)
+    
+    def _buildRecord(self, guid, write_proxies, read_proxies, count=None):
+
+        def expandCount(value, count):
+            
+            if type(value) in types.StringTypes:
+                return value % (count,) if count and "%" in value else value
+            else:
+                return value
+        
+        guid = expandCount(guid, count)
+        write_proxies = set([expandCount(member, count) for member in write_proxies])
+        read_proxies = set([expandCount(member, count) for member in read_proxies])
+            
+        self.items.append((guid, write_proxies, read_proxies,))
+
+    @inlineCallbacks
+    def updateProxyDB(self):
+        
+        db = calendaruserproxy.ProxyDBService
+        for item in self.items:
+            guid, write_proxies, read_proxies = item
+            for proxy in write_proxies:
+                yield db.setGroupMembers("%s#%s" % (guid, "calendar-proxy-write"), (proxy,))
+            for proxy in read_proxies:
+                yield db.setGroupMembers("%s#%s" % (guid, "calendar-proxy-read"), (proxy,))

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/directory.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/directory.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -34,9 +34,11 @@
 from twisted.cred.checkers import ICredentialsChecker
 from twisted.web2.dav.auth import IPrincipalCredentials
 
+from twistedcaldav.config import config
 from twistedcaldav.log import LoggingMixIn
 from twistedcaldav.directory.idirectory import IDirectoryService, IDirectoryRecord
 from twistedcaldav.directory.util import uuidFromName
+from twistedcaldav.partitions import partitions
 
 class DirectoryService(LoggingMixIn):
     implements(IDirectoryService, ICredentialsChecker)
@@ -151,19 +153,20 @@
     implements(IDirectoryRecord)
 
     def __repr__(self):
-        return "<%s[%s@%s(%s)] %s(%s) %r>" % (
+        return "<%s[%s@%s(%s)] %s(%s) %r @ %s>" % (
             self.__class__.__name__,
             self.recordType,
             self.service.guid,
             self.service.realmName,
             self.guid,
             self.shortName,
-            self.fullName
+            self.fullName,
+            self.hostedAt,
         )
 
     def __init__(
-        self, service, recordType, guid, shortName, fullName,
-        calendarUserAddresses, autoSchedule, enabledForCalendaring=True,
+        self, service, recordType, guid,
+        shortName, fullName, emailAddresses,
     ):
         assert service.realmName is not None
         assert recordType
@@ -172,20 +175,34 @@
         if not guid:
             guid = uuidFromName(service.guid, "%s:%s" % (recordType, shortName))
 
-        if enabledForCalendaring:
-            calendarUserAddresses.add("urn:uuid:%s" % (guid,))
-        else:
-            assert len(calendarUserAddresses) == 0
-
         self.service               = service
         self.recordType            = recordType
         self.guid                  = guid
+        self.enabled               = False
+        self.hostedAt              = ""
         self.shortName             = shortName
         self.fullName              = fullName
-        self.enabledForCalendaring = enabledForCalendaring
-        self.calendarUserAddresses = calendarUserAddresses
-        self.autoSchedule          = autoSchedule
+        self.emailAddresses        = emailAddresses
+        self.enabledForCalendaring = False
+        self.autoSchedule          = False
 
+    def get_calendarUserAddresses(self):
+        """
+        Dynamically construct a calendarUserAddresses attribute which describes
+        this L{DirectoryRecord}.
+
+        @see: L{IDirectoryRecord.calendarUserAddresses}.
+        """
+        if not self.enabledForCalendaring:
+            return frozenset()
+        return frozenset(
+            ["urn:uuid:%s" % (self.guid,)] +
+            ["mailto:%s" % (emailAddress,)
+             for emailAddress in self.emailAddresses]
+        )
+
+    calendarUserAddresses = property(get_calendarUserAddresses)
+
     def __cmp__(self, other):
         if not isinstance(other, DirectoryRecord):
             return NotImplemented
@@ -199,35 +216,44 @@
     def __hash__(self):
         h = hash(self.__class__)
         for attr in ("service", "recordType", "shortName", "guid",
-                     "enabledForCalendaring"):
+                     "enabled", "enabledForCalendaring"):
             h = (h + hash(getattr(self, attr))) & sys.maxint
 
         return h
 
-    def members(self):
-        return ()
+    def addAugmentInformation(self, augment):
+        
+        if augment:
+            self.enabled = augment.enabled
+            self.hostedAt = augment.hostedAt
+            self.enabledForCalendaring = augment.enabledForCalendaring
+            self.autoSchedule = augment.autoSchedule
 
-    def groups(self):
-        return ()
+            if self.enabledForCalendaring and self.recordType == self.service.recordType_groups:
+                self.log_error("Group '%s(%s)' cannot be enabled for calendaring" % (self.guid, self.shortName,))
+                self.enabledForCalendaring = False
 
-    def proxies(self):
-        return ()
+        else:
+            # Groups are by default always enabled
+            self.enabled = (self.recordType == self.service.recordType_groups)
+            self.hostedAt = ""
+            self.enabledForCalendaring = False
 
-    def proxyFor(self):
+    def members(self):
         return ()
 
-    def readOnlyProxies(self):
+    def groups(self):
         return ()
 
-    def readOnlyProxyFor(self):
-        return ()
-
-    def hasEditableProxyMembership(self):
-        return self.recordType in (DirectoryService.recordType_users, DirectoryService.recordType_groups)
-
     def verifyCredentials(self, credentials):
         return False
 
+    def locallyHosted(self):
+        return not self.hostedAt or not config.Partitioning.Enabled or self.hostedAt == config.Partitioning.ServerPartitionID
+    
+    def hostedURL(self):
+        return partitions.getPartitionURL(self.hostedAt)
+
 class DirectoryError(RuntimeError):
     """
     Generic directory error.

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/idirectory.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/idirectory.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/idirectory.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -79,11 +79,13 @@
     service               = Attribute("The L{IDirectoryService} this record exists in.")
     recordType            = Attribute("The type of this record.")
     guid                  = Attribute("The GUID of this record.")
+    enabled               = Attribute("Determines whether this record should be provisioned as a principal.")
+    hostedAt              = Attribute("Identifies the server that actually hosts data for the record.")
     shortName             = Attribute("The name of this record.")
     fullName              = Attribute("The full name of this record.")
+    emailAddress          = Attribute("The email address of this record.")
+    enabledForCalendaring = Attribute("Determines whether this record should be provisioned with a calendar home.")
     calendarUserAddresses = Attribute("A set of calendar user addresses for this record.")
-    autoSchedule          = Attribute("Principal identified by this record should automatically accept/deny meetings.")
-    enabledForCalendaring = Attribute("Determines whether this record should be provisioned with a calendar home.")
 
     def members():
         """

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -45,7 +45,7 @@
 from twistedcaldav.config import config
 from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
 
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
+from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyPrincipalResource
 from twistedcaldav.directory.directory import DirectoryService
 from twistedcaldav.directory.util import NotFilePath
@@ -116,7 +116,7 @@
         raise NotImplementedError("Subclass must implement principalForUID()")
 
     def principalForRecord(self, record):
-        if record is None:
+        if record is None or not record.enabled:
             return None
         return self.principalForUID(record.guid)
 
@@ -145,7 +145,7 @@
         return self.getChild(uidsResourceName).getChild(uid)
 
     def _principalForURI(self, uri):
-        scheme, netloc, path, params, query, fragment = urlparse(uri)
+        scheme, netloc, path, _ignore_params, _ignore_query, _ignore_fragment = urlparse(uri)
 
         if scheme == "":
             pass
@@ -203,7 +203,7 @@
 
         # Next try looking it up in the directory
         record = self.directory.recordWithCalendarUserAddress(address)
-        if record is not None:
+        if record is not None and record.enabled:
             return self.principalForRecord(record)
 
         log.debug("No principal for calendar user address: %r" % (address,))
@@ -275,7 +275,7 @@
 
     def listChildren(self):
         if config.EnablePrincipalListings:
-            return (record.shortName for record in self.directory.listRecords(self.recordType))
+            return (record.shortName for record in self.directory.listRecords(self.recordType) if record.enabled)
         else:
             # Not a listable collection
             raise HTTPError(responsecode.FORBIDDEN)
@@ -332,7 +332,7 @@
 
         record = self.directory.recordWithGUID(primaryUID)
 
-        if record is None:
+        if record is None or not record.enabled:
             log.err("No principal found for UID: %s" % (name,))
             return None
 
@@ -370,7 +370,7 @@
         """
         super(DirectoryPrincipalResource, self).__init__(NotFilePath(isdir=True))
 
-        self.cacheNotifier = self.cacheNotifierFactory(self)
+        self.cacheNotifier = self.cacheNotifierFactory(self, cacheHandle="PrincipalToken")
 
         if self.isCollection():
             slash = "/"
@@ -442,6 +442,7 @@
             """---------------------\n"""
             """Directory GUID: %s\n"""         % (self.record.service.guid,),
             """Realm: %s\n"""                  % (self.record.service.realmName,),
+            """Hosted-At: %s\n"""              % (self.record.hostedAt,) if config.Partitioning.Enabled else "", 
             """\n"""
             """Principal Information\n"""
             """---------------------\n"""
@@ -449,6 +450,7 @@
             """Record type: %s\n"""            % (self.record.recordType,),
             """Short name: %s\n"""             % (self.record.shortName,),
             """Full name: %s\n"""              % (self.record.fullName,),
+            """Email addresses:\n"""           , format_list(self.record.emailAddresses),
             """Principal UID: %s\n"""          % (self.principalUID(),),
             """Principal URL: %s\n"""          % (format_link(self.principalURL()),),
             """\nAlternate URIs:\n"""          , format_list(format_link(u) for u in self.alternateURIs()),
@@ -481,16 +483,11 @@
         """
         Return the SQL database for calendar user proxies.
 
-        @return: the L{CalendarUserProxyDatabase} for the principal collection.
+        @return: the L{ProxyDB} for the principal collection.
         """
 
-        # Get the principal collection we are contained in
-        pcollection = self.parent.parent
-
         # The db is located in the principal collection root
-        if not hasattr(pcollection, "calendar_user_proxy_db"):
-            setattr(pcollection, "calendar_user_proxy_db", CalendarUserProxyDatabase(config.DataRoot))
-        return pcollection.calendar_user_proxy_db
+        return calendaruserproxy.ProxyDBService
 
     def alternateURIs(self):
         # FIXME: Add API to IDirectoryRecord for getting a record URI?
@@ -512,7 +509,6 @@
 
         if record not in records:
             records.add(record)
-            myRecordType = self.record.recordType
             for relative in getattr(record, method)():
                 if relative not in records:
                     found = self.parent.principalForRecord(relative)
@@ -538,10 +534,6 @@
         groups = self._getRelatives("groups")
 
         if config.EnableProxyPrincipals:
-            # Get any directory specified proxies
-            groups.update(self._getRelatives("proxyFor", proxy='read-write'))
-            groups.update(self._getRelatives("readOnlyProxyFor", proxy='read-only'))
-
             # Get proxy group UIDs and map to principal resources
             proxies = []
             d = waitForDeferred(self._calendar_user_proxy_index().getMemberships(self.principalUID()))
@@ -551,6 +543,10 @@
                 subprincipal = self.parent.principalForUID(uid)
                 if subprincipal:
                     proxies.append(subprincipal)
+                else:
+                    d = waitForDeferred(self._calendar_user_proxy_index().removeGroup(uid))
+                    yield d
+                    d.getResult()
 
             groups.update(proxies)
 
@@ -569,13 +565,6 @@
                 proxyFors.update(results)
 
         if config.EnableProxyPrincipals:
-            # Get any directory specified proxies
-            if read_write:
-                directoryProxies = self._getRelatives("proxyFor", proxy='read-write')
-            else:
-                directoryProxies = self._getRelatives("readOnlyProxyFor", proxy='read-only')
-            proxyFors.update([subprincipal.parent for subprincipal in directoryProxies])
-
             # Get proxy group UIDs and map to principal resources
             proxies = []
             d = waitForDeferred(self._calendar_user_proxy_index().getMemberships(self.principalUID()))
@@ -588,6 +577,13 @@
 
             proxyFors.update(proxies)
 
+        uids = set()
+        for principal in tuple(proxyFors):
+            if principal.principalUID() in uids:
+                proxyFors.remove(principal)
+            else:
+                uids.add(principal.principalUID())
+
         yield proxyFors
 
     def principalCollections(self):
@@ -596,7 +592,23 @@
     def principalUID(self):
         return self.record.guid
 
+    def locallyHosted(self):
+        return self.record.locallyHosted()
+    
+    def hostedURL(self):
+        return self.record.hostedURL()
+
     ##
+    # Extra resource info
+    ##
+
+    def setAutoSchedule(self, autoSchedule):
+        self.record.autoSchedule = autoSchedule
+
+    def getAutoSchedule(self):
+        return self.record.autoSchedule
+
+    ##
     # Static
     ##
 
@@ -655,6 +667,7 @@
             """---------------------\n"""
             """Directory GUID: %s\n"""         % (self.record.service.guid,),
             """Realm: %s\n"""                  % (self.record.service.realmName,),
+            """Hosted-At: %s\n"""              % (self.record.hostedAt,) if config.Partitioning.Enabled else "", 
             """\n"""
             """Principal Information\n"""
             """---------------------\n"""
@@ -699,15 +712,6 @@
     def autoSchedule(self):
         return self.record.autoSchedule
 
-    def proxies(self):
-        return self._getRelatives("proxies")
-
-    def readOnlyProxies(self):
-        return self._getRelatives("readOnlyProxies")
-
-    def hasEditableProxyMembership(self):
-        return self.record.hasEditableProxyMembership()
-
     def scheduleInbox(self, request):
         home = self._calendarHome()
         if home is None:

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/sqldb.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/sqldb.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/sqldb.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,377 +0,0 @@
-##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-SQL (sqlite) based user/group/resource directory service implementation.
-"""
-
-"""
-SCHEMA:
-
-User Database:
-
-ROW: RECORD_TYPE, SHORT_NAME (unique), PASSWORD, NAME
-
-Group Database:
-
-ROW: SHORT_NAME, MEMBER_SHORT_NAME
-
-CUAddress database:
-
-ROW: ADDRESS (unqiue), SHORT_NAME
-
-"""
-
-__all__ = [
-    "SQLDirectoryService",
-]
-
-from twisted.cred.credentials import UsernamePassword
-from twisted.python.filepath import FilePath
-
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
-from twistedcaldav.sql import AbstractSQLDatabase
-from twistedcaldav.sql import db_prefix
-
-import os
-
-class SQLDirectoryManager(AbstractSQLDatabase):
-    """
-    House keeping operations on the SQL DB, including loading from XML file,
-    and record dumping. This can be used as a standalong DB management tool.
-    """
-    dbType = "DIRECTORYSERVICE"
-    dbFilename = db_prefix + "accounts"
-    dbFormatVersion = "3"
-
-    def __init__(self, path):
-        path = os.path.join(path, SQLDirectoryManager.dbFilename)
-        super(SQLDirectoryManager, self).__init__(path, True)
-
-    def loadFromXML(self, xmlFile):
-        parser = XMLAccountsParser(xmlFile)
-       
-        # Totally wipe existing DB and start from scratch
-        if os.path.exists(self.dbpath):
-            os.remove(self.dbpath)
-
-        self._db_execute("insert into SERVICE (REALM) values (:1)", parser.realm)
-
-        # Now add records to db
-        for item in parser.items.values():
-            for entry in item.itervalues():
-                self._add_to_db(entry)
-        self._db_commit()
-
-    def getRealm(self):
-        for realm in self._db_execute("select REALM from SERVICE"):
-            return realm[0].decode("utf-8")
-        else:
-            return ""
-
-    def listRecords(self, recordType):
-        # Get each account record
-        for shortName, guid, password, name in self._db_execute(
-            """
-            select SHORT_NAME, GUID, PASSWORD, NAME
-              from ACCOUNTS
-             where RECORD_TYPE = :1
-            """, recordType
-        ):
-            # See if we have members
-            members = self.members(shortName)
-                
-            # See if we are a member of any groups
-            groups = self.groups(shortName)
-                
-            # Get calendar user addresses
-            calendarUserAddresses = self.calendarUserAddresses(shortName)
-            
-            # TODO: need this for Resources and Locations
-            autoSchedule = False
-                
-            yield shortName, guid, password, name, members, groups, calendarUserAddresses, autoSchedule
-
-    def getRecord(self, recordType, shortName):
-        # Get individual account record
-        for shortName, guid, password, name in self._db_execute(
-            """
-            select SHORT_NAME, GUID, PASSWORD, NAME
-              from ACCOUNTS
-             where RECORD_TYPE = :1
-               and SHORT_NAME  = :2
-            """, recordType, shortName
-        ):
-            break
-        else:
-            return None
-        
-        # See if we have members
-        members = self.members(shortName)
-            
-        # See if we are a member of any groups
-        groups = self.groups(shortName)
-            
-        # Get calendar user addresses
-        calendarUserAddresses = self.calendarUserAddresses(shortName)
-        
-        # TODO: need this for Resources and Locations
-        autoSchedule = False
-            
-        return shortName, guid, password, name, members, groups, calendarUserAddresses, autoSchedule
-            
-    def members(self, shortName):
-        members = set()
-        for member in self._db_execute(
-            """
-            select MEMBER_RECORD_TYPE, MEMBER_SHORT_NAME
-              from GROUPS
-             where SHORT_NAME = :1
-            """, shortName
-        ):
-            members.add(tuple(member))
-        return members
-
-    def groups(self, shortName):
-        groups = set()
-        for (name,) in self._db_execute(
-            """
-            select SHORT_NAME
-              from GROUPS
-             where MEMBER_SHORT_NAME = :1
-            """, shortName
-        ):
-            groups.add(name)
-        return groups
-
-    def calendarUserAddresses(self, shortName):
-        calendarUserAddresses = set()
-        for (address,) in self._db_execute(
-            """
-            select ADDRESS
-              from ADDRESSES
-             where SHORT_NAME = :1
-            """, shortName
-        ):
-            calendarUserAddresses.add(address)
-        return calendarUserAddresses
-
-    def _add_to_db(self, record):
-        # Do regular account entry
-        recordType = record.recordType
-        shortName = record.shortName
-        guid = record.guid
-        password = record.password
-        name = record.name
-
-        self._db_execute(
-            """
-            insert into ACCOUNTS (RECORD_TYPE, SHORT_NAME, GUID, PASSWORD, NAME)
-            values (:1, :2, :3, :4, :5)
-            """, recordType, shortName, guid, password, name
-        )
-        
-        # Check for members
-        for memberRecordType, memberShortName in record.members:
-            self._db_execute(
-                """
-                insert into GROUPS (SHORT_NAME, MEMBER_RECORD_TYPE, MEMBER_SHORT_NAME)
-                values (:1, :2, :3)
-                """, shortName, memberRecordType, memberShortName
-            )
-                
-        # CUAddress
-        for cuaddr in record.calendarUserAddresses:
-            self._db_execute(
-                """
-                insert into ADDRESSES (ADDRESS, SHORT_NAME)
-                values (:1, :2)
-                """, cuaddr, shortName
-            )
-       
-    def _delete_from_db(self, shortName):
-        """
-        Deletes the specified entry from all dbs.
-        @param name: the name of the resource to delete.
-        @param shortName: the short name of the resource to delete.
-        """
-        self._db_execute("delete from ACCOUNTS  where SHORT_NAME        = :1", shortName)
-        self._db_execute("delete from GROUPS    where SHORT_NAME        = :1", shortName)
-        self._db_execute("delete from GROUPS    where MEMBER_SHORT_NAME = :1", shortName)
-        self._db_execute("delete from ADDRESSES where SHORT_NAME        = :1", shortName)
-    
-    def _db_version(self):
-        """
-        @return: the schema version assigned to this index.
-        """
-        return SQLDirectoryManager.dbFormatVersion
-        
-    def _db_type(self):
-        """
-        @return: the collection type assigned to this index.
-        """
-        return SQLDirectoryManager.dbType
-        
-    def _db_init_data_tables(self, q):
-        """
-        Initialise the underlying database tables.
-        @param q:           a database cursor to use.
-        """
-        #
-        # SERVICE table
-        #
-        q.execute("create table SERVICE (REALM text)")
-
-        #
-        # ACCOUNTS table
-        #
-        q.execute(
-            """
-            create table ACCOUNTS (
-                RECORD_TYPE  text,
-                SHORT_NAME   text,
-                GUID         text,
-                PASSWORD     text,
-                NAME         text
-            )
-            """
-        )
-
-        #
-        # GROUPS table
-        #
-        q.execute(
-            """
-            create table GROUPS (
-                SHORT_NAME          text,
-                MEMBER_RECORD_TYPE  text,
-                MEMBER_SHORT_NAME   text
-            )
-            """
-        )
-
-        #
-        # ADDRESSES table
-        #
-        q.execute(
-            """
-            create table ADDRESSES (
-                ADDRESS     text unique,
-                SHORT_NAME  text
-            )
-            """
-        )
-
-class SQLDirectoryService(DirectoryService):
-    """
-    XML based implementation of L{IDirectoryService}.
-    """
-    baseGUID = "8256E464-35E0-4DBB-A99C-F0E30C231675"
-    realmName = None
-
-    def __repr__(self):
-        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.manager.dbpath)
-
-    def __init__(self, dbParentPath, xmlFile=None):
-        super(SQLDirectoryService, self).__init__()
-
-        if type(dbParentPath) is str:
-            dbParentPath = FilePath(dbParentPath)
-            
-        self.manager = SQLDirectoryManager(dbParentPath.path)
-        if xmlFile:
-            self.manager.loadFromXML(xmlFile)
-        self.realmName = self.manager.getRealm()
-
-    def recordTypes(self):
-        recordTypes = (
-            DirectoryService.recordType_users,
-            DirectoryService.recordType_groups,
-            DirectoryService.recordType_locations,
-            DirectoryService.recordType_resources,
-        )
-        return recordTypes
-
-    def listRecords(self, recordType):
-        for result in self.manager.listRecords(recordType):
-            yield SQLDirectoryRecord(
-                service               = self,
-                recordType            = recordType,
-                shortName             = result[0],
-                guid                  = result[1],
-                password              = result[2],
-                name                  = result[3],
-                members               = result[4],
-                groups                = result[5],
-                calendarUserAddresses = result[6],
-                autoSchedule          = result[7],
-            )
-
-    def recordWithShortName(self, recordType, shortName):
-        result = self.manager.getRecord(recordType, shortName)
-        if result:
-            return SQLDirectoryRecord(
-                service               = self,
-                recordType            = recordType,
-                shortName             = result[0],
-                guid                  = result[1],
-                password              = result[2],
-                name                  = result[3],
-                members               = result[4],
-                groups                = result[5],
-                calendarUserAddresses = result[6],
-                autoSchedule          = result[7],
-            )
-
-        return None
-
-class SQLDirectoryRecord(DirectoryRecord):
-    """
-    XML based implementation implementation of L{IDirectoryRecord}.
-    """
-    def __init__(self, service, recordType, shortName, guid, password, name, members, groups, calendarUserAddresses, autoSchedule):
-        super(SQLDirectoryRecord, self).__init__(
-            service               = service,
-            recordType            = recordType,
-            guid                  = guid,
-            shortName             = shortName,
-            fullName              = name,
-            calendarUserAddresses = calendarUserAddresses,
-            autoSchedule          = autoSchedule,
-        )
-
-        self.password = password
-        self._members = members
-        self._groups  = groups
-
-    def members(self):
-        for recordType, shortName in self._members:
-            yield self.service.recordWithShortName(recordType, shortName)
-
-    def groups(self):
-        for shortName in self._groups:
-            yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
-
-    def verifyCredentials(self, credentials):
-        if isinstance(credentials, UsernamePassword):
-            return credentials.password == self.password
-
-        return super(SQLDirectoryRecord, self).verifyCredentials(credentials)
-
-if __name__ == '__main__':
-    mgr = SQLDirectoryManager("./")
-    mgr.loadFromXML("test/accounts.xml")

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/sudo.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/sudo.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/sudo.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -132,12 +132,13 @@
             guid=None,
             shortName=shortName,
             fullName=shortName,
-            calendarUserAddresses=set(),
-            autoSchedule=False,
-            enabledForCalendaring=False)
+            emailAddresses=set(),
+        )
 
         self.password = entry['password']
 
+        self.enabled = True     # Explicitly enabled
+
     def verifyCredentials(self, credentials):
         if IUsernamePassword.providedBy(credentials):
             return credentials.checkPassword(self.password)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/accounts.xml
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/accounts.xml	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/accounts.xml	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
 limitations under the License.
  -->
 
-<!DOCTYPE accounts SYSTEM "../../../conf/accounts.dtd">
+<!DOCTYPE accounts SYSTEM "../../../conf/auth/accounts.dtd">
 
 <accounts realm="Test">
   <user>
@@ -30,36 +30,45 @@
     <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
     <password>zehcnasw</password>
     <name>Wilfredo Sanchez</name>
-    <cuaddr>mailto:wsanchez at example.com</cuaddr>
+    <email-address>wsanchez at example.com</email-address>
   </user>
   <user>
     <uid>cdaboo</uid>
     <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
     <password>oobadc</password>
     <name>Cyrus Daboo</name>
-    <cuaddr>mailto:cdaboo at example.com</cuaddr>
+    <email-address>cdaboo at example.com</email-address>
   </user>
   <user>
     <uid>lecroy</uid>
     <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
     <password>yorcel</password>
     <name>Chris Lecroy</name>
-    <cuaddr>mailto:lecroy at example.com</cuaddr>
+    <email-address>lecroy at example.com</email-address>
   </user>
   <user>
     <uid>dreid</uid>
     <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
     <password>dierd</password>
     <name>David Reid</name>
-    <cuaddr>mailto:dreid at example.com</cuaddr>
+    <email-address>dreid at example.com</email-address>
   </user>
+  <user>
+    <uid>nocalendar</uid>
+    <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+    <password>radnelacon</password>
+    <name>No Calendar</name>
+    <email-address>nocalendar at example.com</email-address>
+  </user>
   <user repeat="2">
     <uid>user%02d</uid>
+    <guid>user%02d</guid>
     <password>%02duser</password>
     <name>User %02d</name>
   </user>
   <group>
     <uid>managers</uid>
+    <guid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
     <password>managers</password>
     <name>Managers</name>
     <members>
@@ -68,6 +77,7 @@
   </group>
   <group>
     <uid>admin</uid>
+    <guid>admin</guid>
     <password>admin</password>
     <name>Administrators</name>
     <members>
@@ -76,6 +86,7 @@
   </group>
   <group>
     <uid>grunts</uid>
+    <guid>grunts</guid>
     <password>grunts</password>
     <name>We do all the work</name>
     <members>
@@ -86,6 +97,7 @@
   </group>
   <group>
     <uid>right_coast</uid>
+    <guid>right_coast</guid>
     <password>right_coast</password>
     <name>East Coast</name>
     <members>
@@ -94,6 +106,7 @@
   </group>
   <group>
     <uid>left_coast</uid>
+    <guid>left_coast</guid>
     <password>left_coast</password>
     <name>West Coast</name>
     <members>
@@ -104,6 +117,7 @@
   </group>
   <group>
     <uid>both_coasts</uid>
+    <guid>both_coasts</guid>
     <password>both_coasts</password>
     <name>Both Coasts</name>
     <members>
@@ -113,6 +127,7 @@
   </group>
   <group>
     <uid>recursive1_coasts</uid>
+    <guid>recursive1_coasts</guid>
     <password>recursive1_coasts</password>
     <name>Recursive1 Coasts</name>
     <members>
@@ -122,6 +137,7 @@
   </group>
   <group>
     <uid>recursive2_coasts</uid>
+    <guid>recursive2_coasts</guid>
     <password>recursive2_coasts</password>
     <name>Recursive2 Coasts</name>
     <members>
@@ -131,74 +147,61 @@
   </group>
   <group>
     <uid>non_calendar_group</uid>
+    <guid>non_calendar_group</guid>
     <password>non_calendar_group</password>
     <name>Non-calendar group</name>
     <members>
       <member>cdaboo</member>
       <member>lecroy</member>
     </members>
-    <disable-calendar/>
   </group>
   <location>
     <uid>mercury</uid>
+    <guid>mercury</guid>
     <password>mercury</password>
     <name>Mecury Seven</name>
-    <cuaddr>mailto:mercury at example.com</cuaddr>
-    <proxies>
-      <member type="groups">left_coast</member>
-    </proxies>
+    <email-address>mercury at example.com</email-address>
   </location>
   <location>
     <uid>gemini</uid>
+    <guid>gemini</guid>
     <password>gemini</password>
     <name>Gemini Twelve</name>
-    <cuaddr>mailto:gemini at example.com</cuaddr>
-    <auto-schedule/>
-    <proxies>
-      <member>wsanchez</member>
-    </proxies>
+    <email-address>gemini at example.com</email-address>
   </location>
   <location>
     <uid>apollo</uid>
+    <guid>apollo</guid>
     <password>apollo</password>
     <name>Apollo Eleven</name>
-    <cuaddr>mailto:apollo at example.com</cuaddr>
-    <proxies>
-      <member type="groups">both_coasts</member>
-    </proxies>
+    <email-address>apollo at example.com</email-address>
   </location>
   <location>
     <uid>orion</uid>
+    <guid>orion</guid>
     <password>orion</password>
     <name>Orion</name>
-    <cuaddr>mailto:orion at example.com</cuaddr>
-    <proxies>
-      <member type="groups">recursive1_coasts</member>
-    </proxies>
+    <email-address>orion at example.com</email-address>
   </location>
   <resource>
     <uid>transporter</uid>
+    <guid>transporter</guid>
     <password>transporter</password>
     <name>Mass Transporter</name>
-    <cuaddr>mailto:transporter at example.com</cuaddr>
+    <email-address>transporter at example.com</email-address>
   </resource>
   <resource>
     <uid>ftlcpu</uid>
+    <guid>ftlcpu</guid>
     <password>ftlcpu</password>
     <name>Faster-Than-Light Microprocessor</name>
-    <cuaddr>mailto:ftlcpu at example.com</cuaddr>
+    <email-address>ftlcpu at example.com</email-address>
   </resource>
   <resource>
     <uid>non_calendar_proxy</uid>
     <guid>non_calendar_proxy</guid>
     <password>non_calendar_proxy</password>
     <name>Non-calendar proxy</name>
-    <cuaddr>mailto:non_calendar_proxy at example.com</cuaddr>
-    <proxies>
-      <member type="groups">non_calendar_group</member>
-    </proxies>
-    <read-only-proxies>
-      <member type="groups">recursive2_coasts</member>
-    </read-only-proxies>
+    <email-address>non_calendar_proxy at example.com</email-address>
   </resource>
 </accounts>

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments-test-default.xml (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/test/augments-test-default.xml)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments-test-default.xml	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments-test-default.xml	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2009 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE accounts SYSTEM "../../../conf/auth/augments.dtd">
+
+<augments>
+  <record>
+    <guid>Default</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <hosted-at>00001</hosted-at>
+  </record>
+  <record>
+    <guid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
+    <enable>false</enable>
+  </record>
+  <record>
+    <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
+    <enable>true</enable>
+    <enable-calendar>false</enable-calendar>
+  </record>
+  <record>
+    <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+    <enable>true</enable>
+    <hosted-at>00001</hosted-at>
+  </record>
+  <record>
+    <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+    <enable>true</enable>
+    <hosted-at>00002</hosted-at>
+  </record>
+  <record>
+    <guid>6A73326A-F781-47E7-A9F8-AF47364D4152</guid>
+    <enable>true</enable>
+    <hosted-at>00002</hosted-at>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+</augments>

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments-test.xml (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/test/augments-test.xml)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments-test.xml	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments-test.xml	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2009 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE accounts SYSTEM "../../../conf/auth/augments.dtd">
+
+<augments>
+  <record>
+    <guid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
+    <enable>false</enable>
+  </record>
+  <record>
+    <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
+    <enable>true</enable>
+    <enable-calendar>false</enable-calendar>
+  </record>
+  <record>
+    <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+    <enable>true</enable>
+    <hosted-at>00001</hosted-at>
+  </record>
+  <record>
+    <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+    <enable>true</enable>
+    <hosted-at>00002</hosted-at>
+  </record>
+  <record>
+    <guid>6A73326A-F781-47E7-A9F8-AF47364D4152</guid>
+    <enable>true</enable>
+    <hosted-at>00002</hosted-at>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+</augments>

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments.xml (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/test/augments.xml)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments.xml	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/augments.xml	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE augments SYSTEM "../../../conf/augments.dtd">
+
+<augments realm="Test">
+  <record>
+    <guid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+    <enable>true</enable>
+    <enable-calendar>false</enable-calendar>
+  </record>
+  <record repeat="2">
+    <guid>user%02d</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>admin</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>grunts</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>right_coast</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>left_coast</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>both_coasts</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>recursive1_coasts</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>recursive2_coasts</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>non_calendar_group</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>mercury</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>gemini</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <guid>apollo</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>orion</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>transporter</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>ftlcpu</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>non_calendar_proxy</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+</augments>

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/basic
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/basic	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/basic	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,4 +0,0 @@
-wsanchez:Cytm0Bwm7CPJs
-cdaboo:I.Ef5FJl5GVh2
-dreid:LVhqAv4qSrYPs
-lecroy:/7/5VDrkrLxY.

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/digest
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/digest	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/digest	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,4 +0,0 @@
-wsanchez:Test:decbe233ab3d997cacc2fc058b19db8c
-cdaboo:Test:61164bf3d607d072fe8a7ac420b24aac
-dreid:Test:8ee67801004b2752f72b84e7064889a6
-lecroy:Test:60d4feb424430953be045738041e51be

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/groups
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/groups	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/groups	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,4 +0,0 @@
-managers: lecroy
-grunts: wsanchez, cdaboo, dreid
-right_coast: cdaboo
-left_coast: wsanchez, dreid, lecroy

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/proxies.xml (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/test/proxies.xml)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/proxies.xml	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/proxies.xml	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE proxies SYSTEM "proxies.dtd">
+
+<proxies>
+  <record>
+    <guid>mercury</guid>
+    <proxies>
+      <member>left_coast</member>
+    </proxies>
+  </record>
+  <record>
+    <guid>gemini</guid>
+    <proxies>
+      <member>6423F94A-6B76-4A3A-815B-D52CFD77935D</member>
+    </proxies>
+  </record>
+  <record>
+    <guid>apollo</guid>
+    <proxies>
+      <member>both_coasts</member>
+    </proxies>
+  </record>
+  <record>
+    <guid>orion</guid>
+    <proxies>
+      <member>recursive1_coasts</member>
+    </proxies>
+  </record>
+  <record>
+    <guid>non_calendar_proxy</guid>
+    <proxies>
+      <member>non_calendar_group</member>
+    </proxies>
+    <read-only-proxies>
+      <member>recursive2_coasts</member>
+    </read-only-proxies>
+  </record>
+</proxies>

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_aggregate.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_aggregate.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_aggregate.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,20 +14,18 @@
 # limitations under the License.
 ##
 
-from twistedcaldav.directory.apache import BasicDirectoryService
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 
-from twistedcaldav.directory.test.test_apache import digestRealm, basicUserFile, groupFile
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
 
 import twistedcaldav.directory.test.util
+from twistedcaldav.directory import augment
 
 apache_prefix = "apache:"
 xml_prefix = "xml:"
 
 testServices = (
-    (apache_prefix, twistedcaldav.directory.test.test_apache.Apache  ),
     (xml_prefix   , twistedcaldav.directory.test.test_xmlfile.XMLFile),
 )
 
@@ -65,10 +63,9 @@
         """
         Returns an IDirectoryService.
         """
-        apacheService = BasicDirectoryService(digestRealm, basicUserFile, groupFile)
-        apacheService.recordTypePrefix = apache_prefix
-
-        xmlService = XMLDirectoryService(xmlFile)
+        xmlService = XMLDirectoryService(xmlFile=xmlFile)
         xmlService.recordTypePrefix = xml_prefix
 
-        return AggregateDirectoryService((apacheService, xmlService))
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+
+        return AggregateDirectoryService((xmlService,))

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_apache.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_apache.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_apache.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,120 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import os
-
-from twisted.python.filepath import FilePath
-
-import twistedcaldav.directory.test.util
-from twistedcaldav.directory.apache import BasicDirectoryService, DigestDirectoryService
-from twistedcaldav.directory.directory import DirectoryService
-
-digestRealm = "Test"
-
-basicUserFile  = FilePath(os.path.join(os.path.dirname(__file__), "basic"))
-digestUserFile = FilePath(os.path.join(os.path.dirname(__file__), "digest"))
-groupFile      = FilePath(os.path.join(os.path.dirname(__file__), "groups"))
-
-# FIXME: Add tests for GUID hooey, once we figure out what that means here
-
-class Apache (object):
-    recordTypes = set((
-        DirectoryService.recordType_users,
-        DirectoryService.recordType_groups
-    ))
-
-    users = {
-        "wsanchez": { "password": "foo",  "guid": None, "addresses": () },
-        "cdaboo"  : { "password": "bar",  "guid": None, "addresses": () },
-        "dreid"   : { "password": "baz",  "guid": None, "addresses": () },
-        "lecroy"  : { "password": "quux", "guid": None, "addresses": () },
-    }
-
-    groups = {
-        "managers"   : { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "lecroy"),)                                         },
-        "grunts"     : { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "wsanchez"),
-                                                                    (DirectoryService.recordType_users, "cdaboo"),
-                                                                    (DirectoryService.recordType_users, "dreid")) },
-        "right_coast": { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "cdaboo"),)                                         },
-        "left_coast" : { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "wsanchez"),
-                                                                    (DirectoryService.recordType_users, "dreid"),
-                                                                    (DirectoryService.recordType_users, "lecroy")) },
-    }
-
-    locations = {
-    }
-
-    resources = {
-    }
-
-    def service(self):
-        return self.serviceClass(digestRealm, self.userFile(), self.groupFile())
-
-    userFileName = None
-
-    def userFile(self):
-        if not hasattr(self, "_userFile"):
-            if self.userFileName is None:
-                raise NotImplementedError("Test subclass needs to specify userFileName.")
-            self._userFile = FilePath(self.mktemp())
-            basicUserFile.copyTo(self._userFile)
-        return self._userFile
-
-    def groupFile(self):
-        if not hasattr(self, "_groupFile"):
-            self._groupFile = FilePath(self.mktemp())
-            groupFile.copyTo(self._groupFile)
-        return self._groupFile
-
-    def test_changedGroupFile(self):
-        self.groupFile().open("w").write("grunts: wsanchez\n")
-        self.assertEquals(self.recordNames(DirectoryService.recordType_groups), set(("grunts",)))
-
-    def test_recordTypes_user(self):
-        """
-        IDirectoryService.recordTypes(userFile)
-        """
-        self.assertEquals(set(self.serviceClass(digestRealm, self.userFile()).recordTypes()), set((DirectoryService.recordType_users,)))
-
-    userEntry = None
-
-    def test_changedUserFile(self):
-        if self.userEntry is None:
-            raise NotImplementedError("Test subclass needs to specify userEntry.")
-        self.userFile().open("w").write(self.userEntry[1])
-        self.assertEquals(self.recordNames(DirectoryService.recordType_users), set((self.userEntry[0],)))
-
-class Basic (Apache, twistedcaldav.directory.test.util.BasicTestCase):
-    """
-    Test Apache-Compatible UserFile/GroupFile directory implementation.
-    """
-    serviceClass = BasicDirectoryService
-
-    userFileName = basicUserFile
-    userEntry = ("wsanchez", "wsanchez:Cytm0Bwm7CPJs\n")
-
-class Digest (Apache, twistedcaldav.directory.test.util.DigestTestCase):
-    """
-    Test Apache-Compatible DigestFile/GroupFile directory implementation.
-    """
-    serviceClass = DigestDirectoryService
-
-    userFileName = digestUserFile
-    userEntry = ("wsanchez", "wsanchez:Test:decbe233ab3d997cacc2fc058b19db8c\n")
-
-    def test_verifyCredentials_digest(self):
-        raise NotImplementedError() # Use super's implementation
-    test_verifyCredentials_digest.todo = "unimplemented"

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_augment.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/test/test_augment.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_augment.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_augment.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,178 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.test.util import TestCase
+from twistedcaldav.directory.augment import AugmentXMLDB, AugmentSqliteDB,\
+    AugmentPostgreSQLDB
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.directory.xmlaugmentsparser import XMLAugmentsParser
+import cStringIO
+import os
+
+xmlFile = os.path.join(os.path.dirname(__file__), "augments-test.xml")
+xmlFileDefault = os.path.join(os.path.dirname(__file__), "augments-test-default.xml")
+
+testRecords = (
+    {"guid":"D11F03A0-97EA-48AF-9A6C-FAC7F3975766", "enabled":True,  "hostedAt":"", "enabledForCalendaring":False, "autoSchedule":False},
+    {"guid":"6423F94A-6B76-4A3A-815B-D52CFD77935D", "enabled":True,  "hostedAt":"", "enabledForCalendaring":True, "autoSchedule":False},
+    {"guid":"5A985493-EE2C-4665-94CF-4DFEA3A89500", "enabled":False, "hostedAt":"", "enabledForCalendaring":False, "autoSchedule":False},
+    {"guid":"8B4288F6-CC82-491D-8EF9-642EF4F3E7D0", "enabled":True,  "hostedAt":"", "enabledForCalendaring":False, "autoSchedule":False},
+    {"guid":"5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1", "enabled":True,  "hostedAt":"00001", "enabledForCalendaring":False, "autoSchedule":False},
+    {"guid":"543D28BA-F74F-4D5F-9243-B3E3A61171E5", "enabled":True,  "hostedAt":"00002", "enabledForCalendaring":False, "autoSchedule":False},
+    {"guid":"6A73326A-F781-47E7-A9F8-AF47364D4152", "enabled":True,  "hostedAt":"00002", "enabledForCalendaring":True, "autoSchedule":True},
+)
+
+testRecordDefault = {"guid":"A4318887-F2C7-4A70-9056-B88CC8DB26F1", "enabled":True,  "hostedAt":"00001", "enabledForCalendaring":True, "autoSchedule":False}
+
+class AugmentTests(TestCase):
+
+    @inlineCallbacks
+    def _checkRecord(self, db, items):
+        
+        record = (yield db.getAugmentRecord(items["guid"]))
+        self.assertTrue(record is not None)
+        
+        for k,v in items.iteritems():
+            self.assertEqual(getattr(record, k), v)
+
+    @inlineCallbacks
+    def _checkNoRecord(self, db, guid):
+        
+        record = (yield db.getAugmentRecord(guid))
+        self.assertTrue(record is None)
+
+class AugmentXMLTests(AugmentTests):
+
+    @inlineCallbacks
+    def test_read(self):
+        
+        db = AugmentXMLDB((xmlFile,))
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkNoRecord(db, "D11F03A0-97EA-48AF-9A6C-FAC7F3975767")
+
+    @inlineCallbacks
+    def test_read_default(self):
+        
+        db = AugmentXMLDB((xmlFileDefault,))
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkRecord(db, testRecordDefault)
+
+    def test_parseErrors(self):
+        
+        db = {}
+        self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO(""), db)
+        self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+<accounts>
+    <foo/>
+</accounts>
+"""), db)
+        self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+<augments>
+    <foo/>
+</augments>
+"""), db)
+        self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+<augments>
+  <record>
+    <enable>true</enable>
+  </record>
+</augments>
+"""), db)
+        self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+  <record>
+    <guid>admin</guid>
+    <enable>true</enable>
+    <foo/>
+  </record>
+"""), db)
+
+class AugmentSqliteTests(AugmentTests):
+
+    @inlineCallbacks
+    def test_read(self):
+        
+        db = AugmentSqliteDB(self.mktemp())
+
+        dbxml = AugmentXMLDB((xmlFile,))
+        for record in dbxml.db.values():
+            yield db.addAugmentRecord(record)
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkNoRecord(db, "D11F03A0-97EA-48AF-9A6C-FAC7F3975767")
+
+    @inlineCallbacks
+    def test_read_default(self):
+        
+        db = AugmentSqliteDB(self.mktemp())
+
+        dbxml = AugmentXMLDB((xmlFileDefault,))
+        for record in dbxml.db.values():
+            yield db.addAugmentRecord(record)
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkRecord(db, testRecordDefault)
+
+class AugmentPostgreSQLTests(AugmentTests):
+
+    @inlineCallbacks
+    def test_read(self):
+        
+        db = AugmentPostgreSQLDB("localhost", "augments")
+        yield db.clean()
+
+        dbxml = AugmentXMLDB((xmlFile,))
+        for record in dbxml.db.values():
+            yield db.addAugmentRecord(record)
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkNoRecord(db, "D11F03A0-97EA-48AF-9A6C-FAC7F3975767")
+
+    @inlineCallbacks
+    def test_read_default(self):
+        
+        db = AugmentPostgreSQLDB("localhost", "augments")
+        yield db.clean()
+
+        dbxml = AugmentXMLDB((xmlFileDefault,))
+        for record in dbxml.db.values():
+            yield db.addAugmentRecord(record)
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkRecord(db, testRecordDefault)
+
+try:
+    import pgdb
+except ImportError:
+    AugmentPostgreSQLTests.skip = True
+else:
+    try:
+        db = pgdb.connect(host="localhost", database="augments")
+    except:
+        AugmentPostgreSQLTests.skip = True

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_calendar.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_calendar.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_calendar.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -19,8 +19,9 @@
 from twisted.web2.dav import davxml
 from twisted.web2.test.test_server import SimpleRequest
 
+from twistedcaldav.directory import augment
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.static import CalendarHomeProvisioningFile
 
@@ -34,11 +35,12 @@
         super(ProvisionedCalendars, self).setUp()
         
         # Setup the initial directory
-        self.xmlfile = self.mktemp()
-        fd = open(self.xmlfile, "w")
+        self.xmlFile = self.mktemp()
+        fd = open(self.xmlFile, "w")
         fd.write(open(xmlFile.path, "r").read())
         fd.close()
-        self.directoryService = XMLDirectoryService(self.xmlfile)
+        self.directoryService = XMLDirectoryService(xmlFile=self.xmlFile)
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
         
         # Set up a principals hierarchy for each service we're testing with
         name = "principals"
@@ -69,3 +71,33 @@
         request = SimpleRequest(self.site, "GET", "/calendars/users/12345/")
         d = request.locateResource(request.uri)
         d.addCallback(_response)
+
+    def test_ExistentCalendarHome(self):
+
+        def _response(resource):
+            if resource is None:
+                self.fail("Incorrect response to GET on existent calendar home.")
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        d = request.locateResource(request.uri)
+        d.addCallback(_response)
+
+    def test_ExistentCalendar(self):
+
+        def _response(resource):
+            if resource is None:
+                self.fail("Incorrect response to GET on existent calendar.")
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/calendar/")
+        d = request.locateResource(request.uri)
+        d.addCallback(_response)
+
+    def test_ExistentInbox(self):
+
+        def _response(resource):
+            if resource is None:
+                self.fail("Incorrect response to GET on existent inbox.")
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/inbox/")
+        d = request.locateResource(request.uri)
+        d.addCallback(_response)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_digest.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_digest.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_digest.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -2,12 +2,12 @@
 
 from twisted.cred import error
 from twisted.internet import address
-from twistedcaldav.test.util import TestCase
 from twisted.web2.auth import digest
 from twisted.web2.auth.wrapper import UnauthorizedResponse
 from twisted.web2.test.test_server import SimpleRequest
 from twisted.web2.dav.fileop import rmdir
 from twistedcaldav.directory.digest import QopDigestCredentialFactory
+from twistedcaldav.test.util import TestCase
 import os
 import md5
 

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_guidchange.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_guidchange.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_guidchange.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -37,11 +37,11 @@
         super(ProvisionedPrincipals, self).setUp()
         
         # Setup the initial directory
-        self.xmlfile = self.mktemp()
-        fd = open(self.xmlfile, "w")
+        self.xmlFile = self.mktemp()
+        fd = open(self.xmlFile, "w")
         fd.write(open(xmlFile.path, "r").read())
         fd.close()
-        self.directoryService = XMLDirectoryService(self.xmlfile)
+        self.directoryService = XMLDirectoryService(xmlFile=self.xmlFile)
         
         # Set up a principals hierarchy for each service we're testing with
         name = "principals"
@@ -78,7 +78,7 @@
         
         def privs1(result):
             # Change GUID in record
-            fd = open(self.xmlfile, "w")
+            fd = open(self.xmlFile, "w")
             fd.write(open(xmlFile.path, "r").read().replace(oldUID, newUID))
             fd.close()
             fd = None

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectory.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectory.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -21,8 +21,10 @@
 else:
     import twisted.web2.auth.digest
     import twistedcaldav.directory.test.util
+    from twistedcaldav.directory import augment
     from twistedcaldav.directory.directory import DirectoryService
     from twistedcaldav.directory.appleopendirectory import OpenDirectoryRecord
+    import dsattributes
 
     # Wonky hack to prevent unclean reactor shutdowns
     class DummyReactor(object):
@@ -51,6 +53,7 @@
         def setUp(self):
             super(OpenDirectory, self).setUp()
             self._service = OpenDirectoryService(node="/Search", dosetup=False)
+            augment.AugmentService = augment.AugmentXMLDB(xmlFiles=())
 
         def tearDown(self):
             for call in self._service._delayedCalls:
@@ -67,12 +70,8 @@
                 nodeName              = "/LDAPv2/127.0.0.1",
                 shortName             = "user",
                 fullName              = "Some user",
-                calendarUserAddresses = set(("mailtoguid at example.com",)),
-                autoSchedule          = False,
-                enabledForCalendaring = True,
+                emailAddresses        = set(("someuser at example.com",)),
                 memberGUIDs           = [],
-                proxyGUIDs            = (),
-                readOnlyProxyGUIDs    = (),
             )
 
             digestFields = {}

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryrecords.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryrecords.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryrecords.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,601 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twistedcaldav.test.util import TestCase
-from twisted.cred.credentials import UsernamePassword
-import opendirectory
-
-
-
-try:
-    from twistedcaldav.directory.appleopendirectory import OpenDirectoryService as RealOpenDirectoryService
-    import dsattributes
-except ImportError:
-    pass
-else:
-    from twistedcaldav.directory.directory import DirectoryService
-    from twistedcaldav.directory.util import uuidFromName
-    from twistedcaldav.test.util import TestCase
-
-    class OpenDirectoryService (RealOpenDirectoryService):
-        def _queryDirectory(self, recordType):
-            try:
-                return self.fakerecords[recordType]
-            except KeyError:
-                return []
-    
-    class ReloadCache(TestCase):
-        def setUp(self):
-            super(ReloadCache, self).setUp()
-            self._service = OpenDirectoryService(node="/Search", dosetup=False)
-            self._service.servicetags.add("FE588D50-0514-4DF9-BCB5-8ECA5F3DA274:030572AE-ABEC-4E0F-83C9-FCA304769E5F:calendar")
-            self._service.fakerecords = { }
-            
-        def tearDown(self):
-            for call in self._service._delayedCalls:
-                call.cancel()
-
-        def _verifyRecords(self, recordType, expected):
-            expected = set(expected)
-            found = set(self._service._records[recordType]["records"].keys())
-            
-            missing = expected.difference(found)
-            extras = found.difference(expected)
-
-            self.assertTrue(len(missing) == 0, msg="Directory records not found: %s" % (missing,))
-            self.assertTrue(len(extras) == 0, msg="Directory records not expected: %s" % (extras,))
-                
-        def _verifyRecordsCheckEnabled(self, recordType, expected, enabled):
-            expected = set(expected)
-            found = set([item for item in self._service._records[recordType]["records"].iterkeys()
-                         if self._service._records[recordType]["records"][item].enabledForCalendaring == enabled])
-            
-            missing = expected.difference(found)
-            extras = found.difference(expected)
-
-            self.assertTrue(len(missing) == 0, msg="Directory records not found: %s" % (missing,))
-            self.assertTrue(len(extras) == 0, msg="Directory records not expected: %s" % (extras,))
-                
-        def _verifyDisabledRecords(self, recordType, expectedNames, expectedGUIDs):
-            def check(disabledType, expected):
-                expected = set(expected)
-                found = self._service._records[recordType][disabledType]
-            
-                missing = expected.difference(found)
-                extras = found.difference(expected)
-
-                self.assertTrue(len(missing) == 0, msg="Disabled directory records not found: %s" % (missing,))
-                self.assertTrue(len(extras) == 0, msg="Disabled directory records not expected: %s" % (extras,))
-
-            check("disabled names", expectedNames)
-            check("disabled guids", (guid.lower() for guid in expectedGUIDs))
-
-        def test_normal(self):
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                ],
-                DirectoryService.recordType_groups: [
-                    fakeODRecord("Group 01"),
-                    fakeODRecord("Group 02"),
-                ],
-                DirectoryService.recordType_resources: [
-                    fakeODRecord("Resource 01"),
-                    fakeODRecord("Resource 02"),
-                ],
-                DirectoryService.recordType_locations: [
-                    fakeODRecord("Location 01"),
-                    fakeODRecord("Location 02"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users)
-            self._service.reloadCache(DirectoryService.recordType_groups)
-            self._service.reloadCache(DirectoryService.recordType_resources)
-            self._service.reloadCache(DirectoryService.recordType_locations)
-
-            self._verifyRecords(DirectoryService.recordType_users, ("user01", "user02"))
-            self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-            self._verifyRecords(DirectoryService.recordType_groups, ("group01", "group02"))
-            self._verifyDisabledRecords(DirectoryService.recordType_groups, (), ())
-
-            self._verifyRecords(DirectoryService.recordType_resources, ("resource01", "resource02"))
-            self._verifyDisabledRecords(DirectoryService.recordType_resources, (), ())
-
-            self._verifyRecords(DirectoryService.recordType_locations, ("location01", "location02"))
-            self._verifyDisabledRecords(DirectoryService.recordType_locations, (), ())
-
-        def test_normal_disabledusers(self):
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03", addLocator=False),
-                    fakeODRecord("User 04", addLocator=False),
-                ],
-                DirectoryService.recordType_groups: [
-                    fakeODRecord("Group 01"),
-                    fakeODRecord("Group 02"),
-                    fakeODRecord("Group 03", addLocator=False),
-                    fakeODRecord("Group 04", addLocator=False),
-                ],
-                DirectoryService.recordType_resources: [
-                    fakeODRecord("Resource 01"),
-                    fakeODRecord("Resource 02"),
-                    fakeODRecord("Resource 03", addLocator=False),
-                    fakeODRecord("Resource 04", addLocator=False),
-                ],
-                DirectoryService.recordType_locations: [
-                    fakeODRecord("Location 01"),
-                    fakeODRecord("Location 02"),
-                    fakeODRecord("Location 03", addLocator=False),
-                    fakeODRecord("Location 04", addLocator=False),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users)
-            self._service.reloadCache(DirectoryService.recordType_groups)
-            self._service.reloadCache(DirectoryService.recordType_resources)
-            self._service.reloadCache(DirectoryService.recordType_locations)
-
-            self._verifyRecordsCheckEnabled(DirectoryService.recordType_users, ("user01", "user02"), True)
-            self._verifyRecordsCheckEnabled(DirectoryService.recordType_users, ("user03", "user04"), False)
-
-            self._verifyRecordsCheckEnabled(DirectoryService.recordType_groups, ("group01", "group02"), True)
-            self._verifyRecordsCheckEnabled(DirectoryService.recordType_groups, ("group03", "group04"), False)
-
-            self._verifyRecordsCheckEnabled(DirectoryService.recordType_resources, ("resource01", "resource02"), True)
-            self._verifyRecordsCheckEnabled(DirectoryService.recordType_resources, (), False)
-
-            self._verifyRecordsCheckEnabled(DirectoryService.recordType_locations, ("location01", "location02"), True)
-            self._verifyRecordsCheckEnabled(DirectoryService.recordType_locations, (), False)
-
-        def test_normalCacheMiss(self):
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users)
-
-            self._verifyRecords(DirectoryService.recordType_users, ("user01",))
-            self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03", guid="D10F3EE0-5014-41D3-8488-3819D3EF3B2A"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
-
-            self._verifyRecords(DirectoryService.recordType_users, ("user01", "user02", "user03"))
-            self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-        def test_duplicateRecords(self):
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 02"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users)
-
-            self._verifyRecords(DirectoryService.recordType_users, ("user01", "user02"))
-            self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-            self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-
-        def test_duplicateName(self):
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", guid="A25775BB-1281-4606-98C6-2893B2D5CCD7"),
-                    fakeODRecord("User 02", guid="30CA2BB9-C935-4A5D-80E2-79266BCB0255"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users)
-
-            self._verifyRecords(DirectoryService.recordType_users, ("user01",))
-            self._verifyDisabledRecords(
-                DirectoryService.recordType_users,
-                ("user02",),
-                ("A25775BB-1281-4606-98C6-2893B2D5CCD7", "30CA2BB9-C935-4A5D-80E2-79266BCB0255"),
-            )
-
-        def test_duplicateGUID(self):
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8"),
-                    fakeODRecord("User 03", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users)
-
-            self._verifyRecords(DirectoryService.recordType_users, ("user01",))
-            self._verifyDisabledRecords(
-                DirectoryService.recordType_users,
-                ("user02", "user03"),
-                ("113D7F74-F84A-4F17-8C96-CE8F10D68EF8",),
-            )
-
-        def test_duplicateCombo(self):
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8"),
-                    fakeODRecord("User 02", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8", shortName="user03"),
-                    fakeODRecord("User 02", guid="136E369F-DB40-4135-878D-B75D38242D39"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users)
-
-            self._verifyRecords(DirectoryService.recordType_users, ("user01",))
-            self._verifyDisabledRecords(
-                DirectoryService.recordType_users,
-                ("user02", "user03"),
-                ("113D7F74-F84A-4F17-8C96-CE8F10D68EF8", "136E369F-DB40-4135-878D-B75D38242D39"),
-            )
-
-        def test_duplicateGUIDCacheMiss(self):
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", guid="EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E"),
-                    fakeODRecord("User 03", guid="D10F3EE0-5014-41D3-8488-3819D3EF3B2A"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users)
-
-            self._verifyRecords(DirectoryService.recordType_users, ("user01", "user02", "user03"))
-            self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", guid="EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E"),
-                    fakeODRecord("User 02", guid="EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E", shortName="user04"),
-                    fakeODRecord("User 03", guid="62368DDF-0C62-4C97-9A58-DE9FD46131A0"),
-                    fakeODRecord("User 03", guid="62368DDF-0C62-4C97-9A58-DE9FD46131A0", shortName="user05"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
-
-            self._verifyRecords(DirectoryService.recordType_users, ("user01",))
-            self._verifyDisabledRecords(
-                DirectoryService.recordType_users,
-                ("user02", "user03", "user04", "user05"),
-                ("EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E", "62368DDF-0C62-4C97-9A58-DE9FD46131A0"),
-            )
-
-        def test_groupmembers(self):
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                ],
-                DirectoryService.recordType_groups: [
-                    fakeODRecord("Group 01", members=[
-                        guidForShortName("user01"),
-                        guidForShortName("user02"),
-                    ]),
-                    fakeODRecord("Group 02", members=[
-                        guidForShortName("resource01"),
-                        guidForShortName("user02"),
-                    ]),
-                ],
-                DirectoryService.recordType_resources: [
-                    fakeODRecord("Resource 01"),
-                    fakeODRecord("Resource 02"),
-                ],
-                DirectoryService.recordType_locations: [
-                    fakeODRecord("Location 01"),
-                    fakeODRecord("Location 02"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users)
-            self._service.reloadCache(DirectoryService.recordType_groups)
-            self._service.reloadCache(DirectoryService.recordType_resources)
-            self._service.reloadCache(DirectoryService.recordType_locations)
-
-            group1 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group01")
-            self.assertTrue(group1 is not None)
-
-            group2 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group02")
-            self.assertTrue(group2 is not None)
-
-            user1 = self._service.recordWithShortName(DirectoryService.recordType_users, "user01")
-            self.assertTrue(user1 is not None)
-            self.assertEqual(set((group1,)), user1.groups()) 
-            
-            user2 = self._service.recordWithShortName(DirectoryService.recordType_users, "user02")
-            self.assertTrue(user2 is not None)
-            self.assertEqual(set((group1, group2)), user2.groups()) 
-            
-            self._service.fakerecords[DirectoryService.recordType_groups] = [
-                fakeODRecord("Group 01", members=[
-                    guidForShortName("user01"),
-                ]),
-                fakeODRecord("Group 02", members=[
-                    guidForShortName("resource01"),
-                    guidForShortName("user02"),
-                ]),
-            ]
-            self._service.refresh(loop=False, master=True)
-            self._service.reloadCache(DirectoryService.recordType_groups, forceUpdate=True)
-
-            group1 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group01")
-            self.assertTrue(group1 is not None)
-
-            group2 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group02")
-            self.assertTrue(group2 is not None)
-
-            user1 = self._service.recordWithShortName(DirectoryService.recordType_users, "user01")
-            self.assertTrue(user1 is not None)
-            self.assertEqual(set((group1,)), user1.groups()) 
-            
-            user2 = self._service.recordWithShortName(DirectoryService.recordType_users, "user02")
-            self.assertTrue(user2 is not None)
-            self.assertEqual(set((group2,)), user2.groups()) 
-            
-            self._service.fakerecords[DirectoryService.recordType_groups] = [
-                fakeODRecord("Group 01", members=[
-                    guidForShortName("user01"),
-                ]),
-                fakeODRecord("Group 02", members=[
-                    guidForShortName("resource01"),
-                    guidForShortName("user02"),
-                ]),
-                fakeODRecord("Group 03", members=[
-                    guidForShortName("user01"),
-                    guidForShortName("user02"),
-                ]),
-            ]
-            self._service.refresh(loop=False, master=True)
-            self._service.reloadCache(DirectoryService.recordType_groups, forceUpdate=True)
-
-            group1 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group01")
-            self.assertTrue(group1 is not None)
-
-            group2 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group02")
-            self.assertTrue(group2 is not None)
-
-            group3 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group03")
-            self.assertTrue(group2 is not None)
-
-            user1 = self._service.recordWithShortName(DirectoryService.recordType_users, "user01")
-            self.assertTrue(user1 is not None)
-            self.assertEqual(set((group1, group3)), user1.groups()) 
-            
-            user2 = self._service.recordWithShortName(DirectoryService.recordType_users, "user02")
-            self.assertTrue(user2 is not None)
-            self.assertEqual(set((group2, group3)), user2.groups()) 
-
-        def test_calendaruseraddress(self):
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                ],
-                DirectoryService.recordType_groups: [],
-                DirectoryService.recordType_resources: [],
-                DirectoryService.recordType_locations: [],
-            }
-            self._service.refresh(loop=False, master=True)
-
-            self._service.reloadCache(DirectoryService.recordType_users)
-
-            user1 = self._service.recordWithCalendarUserAddress("mailto:user01 at example.com")
-            self.assertTrue(user1 is not None)
-
-            user3 = self._service.recordWithCalendarUserAddress("mailto:user03 at example.com")
-            self.assertTrue(user3 is None)
-
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03"),
-                ],
-                DirectoryService.recordType_groups: [],
-                DirectoryService.recordType_resources: [],
-                DirectoryService.recordType_locations: [],
-            }
-            self._service.refresh(loop=False, master=True)
-            self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
-
-            user1 = self._service.recordWithCalendarUserAddress("mailto:user01 at example.com")
-            self.assertTrue(user1 is not None)
-
-            user3 = self._service.recordWithCalendarUserAddress("mailto:user03 at example.com")
-            self.assertTrue(user3 is not None)
-
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03"),
-                ],
-                DirectoryService.recordType_groups: [],
-                DirectoryService.recordType_resources: [],
-                DirectoryService.recordType_locations: [],
-            }
-            self._service.refresh(loop=False, master=True)
-            self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
-
-            user1 = self._service.recordWithCalendarUserAddress("mailto:user01 at example.com")
-            self.assertTrue(user1 is None)
-
-            user3 = self._service.recordWithCalendarUserAddress("mailto:user03 at example.com")
-            self.assertTrue(user3 is not None)
-
-
-        def test_substantialDecline(self):
-            """
-            Test the "substantial decline" protection logic in the case where an
-            od query returns less than half the results of the previous
-            successful one.
-            """
-
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03"),
-                    fakeODRecord("User 04"),
-                    fakeODRecord("User 05"),
-                    fakeODRecord("User 06"),
-                ],
-                DirectoryService.recordType_groups: [],
-                DirectoryService.recordType_resources: [],
-                DirectoryService.recordType_locations: [],
-            }
-            self._service.refresh(loop=False, master=True)
-            self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
-            user1 = self._service.recordWithCalendarUserAddress("mailto:user01 at example.com")
-            self.assertTrue(user1 is not None)
-
-            # Pretend OD suddenly returned less than half:
-            self._service.fakerecords[DirectoryService.recordType_users] = [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-            ]
-            self._service.refresh(loop=False, master=True)
-            self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
-            user3 = self._service.recordWithCalendarUserAddress("mailto:user03 at example.com")
-            self.assertTrue(user3 is not None)
-
-    class AuthCacheTests(TestCase):
-
-        def _authenticateUserBasic(self, ignore, node, name, password):
-            self._odAccessed = True
-            return self._passwords.get(name, "") == password
-
-        def setUp(self):
-            super(AuthCacheTests, self).setUp()
-            self._service = OpenDirectoryService(node="/Search", dosetup=False)
-            self._service.servicetags.add("FE588D50-0514-4DF9-BCB5-8ECA5F3DA274:030572AE-ABEC-4E0F-83C9-FCA304769E5F:calendar")
-            self._passwords = { }
-
-            # Monkeypatch the real opendirectory.authenticateUserBasic
-            self._prev = opendirectory.authenticateUserBasic
-            opendirectory.authenticateUserBasic = self._authenticateUserBasic
-
-        def tearDown(self):
-            for call in self._service._delayedCalls:
-                call.cancel()
-            opendirectory.authenticateUserBasic = self._prev
-
-        def test_caching(self):
-
-            self._service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                ],
-            }
-            self._service.refresh(loop=False, master=True)
-            self._service.reloadCache(DirectoryService.recordType_users)
-
-            user1 = self._service.recordWithCalendarUserAddress("mailto:user01 at example.com")
-            self._passwords["user01"] = "user01"
-            cred = UsernamePassword("user01", "user01")
-
-            # Verify we go to OD when password is neither local nor in memcache
-            self._odAccessed = False
-            self.assertTrue(user1.verifyCredentials(cred))
-            self.assertTrue(self._odAccessed)
-
-            # Verify we don't go to OD when password is local
-            self._odAccessed = False
-            self.assertTrue(user1.verifyCredentials(cred))
-            self.assertFalse(self._odAccessed)
-
-            # Verify we don't go to OD when password not local but *is* in
-            # memcache
-            del user1.password
-            self._odAccessed = False
-            self.assertTrue(user1.verifyCredentials(cred))
-            self.assertFalse(self._odAccessed)
-
-            # Verify a password change will send us to OD
-            self._passwords["user01"] = "new user01"
-            cred = UsernamePassword("user01", "new user01")
-            self._odAccessed = False
-            self.assertTrue(user1.verifyCredentials(cred))
-            self.assertTrue(self._odAccessed)
-
-            # Verify the new password was memcached correctly
-            del user1.password
-            self._odAccessed = False
-            self.assertTrue(user1.verifyCredentials(cred))
-            self.assertFalse(self._odAccessed)
-
-
-def fakeODRecord(fullName, shortName=None, guid=None, email=None, addLocator=True, members=None):
-    if shortName is None:
-        shortName = shortNameForFullName(fullName)
-
-    if guid is None:
-        guid = guidForShortName(shortName)
-    else:
-        guid = guid.lower()
-
-    if email is None:
-        email = "%s at example.com" % (shortName,)
-
-    attrs = {
-        dsattributes.kDS1AttrDistinguishedName: fullName,
-        dsattributes.kDS1AttrGeneratedUID: guid,
-        dsattributes.kDSNAttrEMailAddress: email,
-        dsattributes.kDSNAttrMetaNodeLocation: "/LDAPv3/127.0.0.1",
-    }
-    
-    if members:
-        attrs[dsattributes.kDSNAttrGroupMembers] = members
-
-    if addLocator:
-        attrs[dsattributes.kDSNAttrServicesLocator] = "FE588D50-0514-4DF9-BCB5-8ECA5F3DA274:030572AE-ABEC-4E0F-83C9-FCA304769E5F:calendar"
-
-    return [ shortName, attrs ]
-
-def shortNameForFullName(fullName):
-    return fullName.lower().replace(" ", "")
-
-def guidForShortName(shortName):
-    return uuidFromName(OpenDirectoryService.baseGUID, shortName)

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryschema.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryschema.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryschema.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,1298 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-try:
-    from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
-    from twistedcaldav.directory.appleopendirectory import OpenDirectoryInitError
-    import dsattributes
-except ImportError:
-    pass
-else:
-    from twistedcaldav.directory.directory import DirectoryService
-    import twisted.trial.unittest
-
-    class PlistParse (twisted.trial.unittest.TestCase):
-        """
-        Test Open Directory service schema.
-        """
-
-        plist_nomacosxserver_key = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-    <dict>
-        <key>ReplicaName</key>
-        <string>Master</string>
-
-        <key>com.apple.od.role</key>
-        <string>master</string>
-    </dict>
-</plist>
-"""
-
-        plist_nocalendarservice = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-    <dict>
-        <key>ReplicaName</key>
-        <string>Master</string>
-
-        <key>com.apple.od.role</key>
-        <string>master</string>
-
-        <key>com.apple.macosxserver.virtualhosts</key>
-        <dict>
-            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
-            <dict>
-                <key>hostname</key>
-                <string>foo.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>80</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>443</integer>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>wiki</string>
-                    <string>webCalendar</string>
-                    <string>webMailingList</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>webCalendar</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
-                    </dict>
-                    <key>wiki</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
-                    </dict>
-                    <key>webMailingList</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
-                    </dict>
-                </dict>
-            </dict>
-
-        </dict>
-    </dict>
-</plist>
-"""
-
-        plist_noserviceinfo = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-    <dict>
-        <key>ReplicaName</key>
-        <string>Master</string>
-
-        <key>com.apple.od.role</key>
-        <string>master</string>
-
-        <key>com.apple.macosxserver.virtualhosts</key>
-        <dict>
-            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
-            <dict>
-                <key>hostname</key>
-                <string>foo.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>80</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>443</integer>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>wiki</string>
-                    <string>webCalendar</string>
-                    <string>webMailingList</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>webCalendar</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
-                    </dict>
-                    <key>wiki</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
-                    </dict>
-                    <key>webMailingList</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
-                    </dict>
-                </dict>
-            </dict>
-
-            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
-            <dict>
-                <key>hostname</key>
-                <string>calendar.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8008</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8443</integer>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>calendar</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>webCalendar</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
-                    </dict>
-                </dict>
-            </dict>
-
-        </dict>
-    </dict>
-</plist>
-"""
-
-        plist_disabledservice = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-    <dict>
-        <key>ReplicaName</key>
-        <string>Master</string>
-
-        <key>com.apple.od.role</key>
-        <string>master</string>
-
-        <key>com.apple.macosxserver.virtualhosts</key>
-        <dict>
-            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
-            <dict>
-                <key>hostname</key>
-                <string>foo.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>80</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <string>443</string>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>wiki</string>
-                    <string>webCalendar</string>
-                    <string>webMailingList</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>webCalendar</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
-                    </dict>
-                    <key>wiki</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
-                    </dict>
-                    <key>webMailingList</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
-                    </dict>
-                </dict>
-            </dict>
-
-            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
-            <dict>
-                <key>hostname</key>
-                <string>calendar.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8008</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8443</integer>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>calendar</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>calendar</key>
-                    <dict>
-                        <key>enabled</key>
-                        <false/>
-                        <key>templates</key>
-                        <dict>
-                            <key>principalPath</key>
-                            <string>/principals/%(type)s/%(name)s</string>
-                            <key>calendarUserAddresses</key>
-                            <array>
-                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
-                                <string>mailto:%(email)s</string>
-                                <string>urn:uuid:%(guid)s</string>
-                            </array>
-                        </dict>
-                    </dict>
-                </dict>
-            </dict>
-
-        </dict>
-    </dict>
-</plist>
-"""
-
-        plist_nohostname = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-    <dict>
-        <key>ReplicaName</key>
-        <string>Master</string>
-
-        <key>com.apple.od.role</key>
-        <string>master</string>
-
-        <key>com.apple.macosxserver.virtualhosts</key>
-        <dict>
-            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
-            <dict>
-                <key>hostname</key>
-                <string>foo.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>80</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <string>443</string>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>wiki</string>
-                    <string>webCalendar</string>
-                    <string>webMailingList</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>webCalendar</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
-                    </dict>
-                    <key>wiki</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
-                    </dict>
-                    <key>webMailingList</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
-                    </dict>
-                </dict>
-            </dict>
-
-            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
-            <dict>
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8008</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8443</integer>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>calendar</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>calendar</key>
-                    <dict>
-                        <key>templates</key>
-                        <dict>
-                            <key>principalPath</key>
-                            <string>/principals/%(type)s/%(name)s</string>
-                            <key>calendarUserAddresses</key>
-                            <array>
-                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
-                                <string>mailto:%(email)s</string>
-                                <string>urn:uuid:%(guid)s</string>
-                            </array>
-                        </dict>
-                    </dict>
-                </dict>
-            </dict>
-
-        </dict>
-    </dict>
-</plist>
-"""
-
-        plist_nohostdetails = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-    <dict>
-        <key>ReplicaName</key>
-        <string>Master</string>
-
-        <key>com.apple.od.role</key>
-        <string>master</string>
-
-        <key>com.apple.macosxserver.virtualhosts</key>
-        <dict>
-            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
-            <dict>
-                <key>hostname</key>
-                <string>foo.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>80</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <string>443</string>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>wiki</string>
-                    <string>webCalendar</string>
-                    <string>webMailingList</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>webCalendar</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
-                    </dict>
-                    <key>wiki</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
-                    </dict>
-                    <key>webMailingList</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
-                    </dict>
-                </dict>
-            </dict>
-
-            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
-            <dict>
-                <key>hostname</key>
-                <string>calendar.apple.com</string>
-
-                <key>serviceType</key>
-                <array>
-                    <string>calendar</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>calendar</key>
-                    <dict>
-                        <key>templates</key>
-                        <dict>
-                            <key>principalPath</key>
-                            <string>/principals/%(type)s/%(name)s</string>
-                            <key>calendarUserAddresses</key>
-                            <array>
-                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
-                                <string>mailto:%(email)s</string>
-                                <string>urn:uuid:%(guid)s</string>
-                            </array>
-                        </dict>
-                    </dict>
-                </dict>
-            </dict>
-
-        </dict>
-    </dict>
-</plist>
-"""
-
-        plist_good = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-    <dict>
-        <key>ReplicaName</key>
-        <string>Master</string>
-
-        <key>com.apple.od.role</key>
-        <string>master</string>
-
-        <key>com.apple.macosxserver.virtualhosts</key>
-        <dict>
-            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
-            <dict>
-                <key>hostname</key>
-                <string>foo.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>80</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <string>443</string>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>wiki</string>
-                    <string>webCalendar</string>
-                    <string>webMailingList</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>webCalendar</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
-                    </dict>
-                    <key>wiki</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
-                    </dict>
-                    <key>webMailingList</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
-                    </dict>
-                </dict>
-            </dict>
-
-            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
-            <dict>
-                <key>hostname</key>
-                <string>calendar.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8008</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8443</integer>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>calendar</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>calendar</key>
-                    <dict>
-                        <key>templates</key>
-                        <dict>
-                            <key>principalPath</key>
-                            <string>/principals/%(type)s/%(name)s</string>
-                            <key>calendarUserAddresses</key>
-                            <array>
-                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
-                                <string>mailto:%(email)s</string>
-                                <string>urn:uuid:%(guid)s</string>
-                            </array>
-                        </dict>
-                    </dict>
-                </dict>
-            </dict>
-
-        </dict>
-    </dict>
-</plist>
-"""
-
-        plist_good_other = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-    <dict>
-        <key>ReplicaName</key>
-        <string>Master</string>
-
-        <key>com.apple.od.role</key>
-        <string>master</string>
-
-        <key>com.apple.macosxserver.virtualhosts</key>
-        <dict>
-            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
-            <dict>
-                <key>hostname</key>
-                <string>foo.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>80</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <string>443</string>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>wiki</string>
-                    <string>webCalendar</string>
-                    <string>webMailingList</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>webCalendar</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
-                    </dict>
-                    <key>wiki</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
-                    </dict>
-                    <key>webMailingList</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
-                    </dict>
-                </dict>
-            </dict>
-
-            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
-            <dict>
-                <key>hostname</key>
-                <string>privatecalendar.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8008</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8443</integer>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>calendar</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>calendar</key>
-                    <dict>
-                        <key>templates</key>
-                        <dict>
-                            <key>principalPath</key>
-                            <string>/principals/%(type)s/%(name)s</string>
-                            <key>calendarUserAddresses</key>
-                            <array>
-                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
-                                <string>mailto:%(email)s</string>
-                                <string>urn:uuid:%(guid)s</string>
-                            </array>
-                        </dict>
-                    </dict>
-                </dict>
-            </dict>
-
-        </dict>
-    </dict>
-</plist>
-"""
-
-        plist_duplicate = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-    <dict>
-        <key>ReplicaName</key>
-        <string>Master</string>
-
-        <key>com.apple.od.role</key>
-        <string>master</string>
-
-        <key>com.apple.macosxserver.virtualhosts</key>
-        <dict>
-            <key>F4088107-51FD-4DE5-904D-C20AD9C6C893</key>
-            <dict>
-                <key>hostname</key>
-                <string>foo.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>80</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <string>443</string>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>wiki</string>
-                    <string>webCalendar</string>
-                    <string>webMailingList</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>webCalendar</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
-                    </dict>
-                    <key>wiki</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
-                    </dict>
-                    <key>webMailingList</key>
-                    <dict>
-                        <key>enabled</key>
-                        <true/>
-                        <key>urlMask</key>
-                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
-                    </dict>
-                </dict>
-            </dict>
-
-            <key>1C8C34AC-3D9E-403C-8A33-FBC303F3840E</key>
-            <dict>
-                <key>hostname</key>
-                <string>calendar.apple.com</string>
-
-                <key>hostDetails</key>
-                <dict>
-                    <key>access</key>
-                    <dict>
-                        <key>somethingorother</key>
-                        <string>somethingelse</string>
-                    </dict>
-                    <key>http</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8008</integer>
-                    </dict>
-                    <key>https</key>
-                    <dict>
-                        <key>port</key>
-                        <integer>8443</integer>
-                    </dict>
-                </dict>
-
-                <key>serviceType</key>
-                <array>
-                    <string>calendar</string>
-                </array>
-
-                <key>serviceInfo</key>
-                <dict>
-                    <key>calendar</key>
-                    <dict>
-                        <key>templates</key>
-                        <dict>
-                            <key>principalPath</key>
-                            <string>/principals/%(type)s/%(name)s</string>
-                            <key>calendarUserAddresses</key>
-                            <array>
-                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
-                                <string>mailto:%(email)s</string>
-                                <string>urn:uuid:%(guid)s</string>
-                            </array>
-                        </dict>
-                    </dict>
-                </dict>
-            </dict>
-
-        </dict>
-    </dict>
-</plist>
-"""
-
-        def test_plist_errors(self):
-            def _doParse(plist, title):
-                service = OpenDirectoryService(node="/Search", dosetup=False)
-                if service._parseServiceInfo("calendar.apple.com", "recordit", {
-                'dsAttrTypeNative:apple-serviceinfo'  : plist,
-                dsattributes.kDS1AttrGeneratedUID:      "GUIDIFY",
-                dsattributes.kDSNAttrMetaNodeLocation:  "/LDAPv3/127.0.0.1"}) and service.servicetags:
-                    self.fail(msg="Plist parse should have failed: %s" % (title,))
-
-            plists = (
-                (PlistParse.plist_nomacosxserver_key, "nomacosxserver_key"),
-                (PlistParse.plist_nocalendarservice,  "nocalendarservice"),
-                (PlistParse.plist_noserviceinfo,      "noserviceinfo"),
-                (PlistParse.plist_disabledservice,    "disabledservice"),
-                (PlistParse.plist_nohostname,         "nohostname"),
-                (PlistParse.plist_nohostdetails,      "nohostdetails"),
-            )
-            for plist, title in plists:
-                _doParse(plist, title)
-
-        def test_goodplist(self):
-            service = OpenDirectoryService(node="/Search", dosetup=False)
-            if not service._parseServiceInfo("calendar.apple.com", "recordit", {
-                'dsAttrTypeNative:apple-serviceinfo'  : PlistParse.plist_good,
-                dsattributes.kDS1AttrGeneratedUID:      "GUIDIFY",
-                dsattributes.kDSNAttrMetaNodeLocation:  "/LDAPv3/127.0.0.1"}):
-                self.fail(msg="Plist parse should not have failed")
-            else:
-                # Verify that we extracted the proper items
-                self.assertEqual(service.servicetags.pop(), "GUIDIFY:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")
-
-        def test_expandcuaddrs(self):
-            def doTest(recordName, record, result, title):
-                service = OpenDirectoryService(node="/Search", dosetup=False)
-                if not service._parseServiceInfo("calendar.apple.com", recordName, {
-                'dsAttrTypeNative:apple-serviceinfo'  : PlistParse.plist_good,
-                dsattributes.kDS1AttrGeneratedUID:      "GUIDIFY",
-                dsattributes.kDSNAttrMetaNodeLocation:  "/LDAPv3/127.0.0.1"}):
-                    self.fail(msg="Plist parse should not have failed: %s" % (recordName,))
-                else:
-                    expanded = service._calendarUserAddresses(DirectoryService.recordType_users, recordName, record)
-
-                    # Verify that we extracted the proper items
-                    self.assertEqual(expanded, result, msg=title % (expanded, result,))
-
-            data = (
-                (
-                 "user01",
-                 {
-                    dsattributes.kDS1AttrGeneratedUID: "GUID-USER-01",
-                    dsattributes.kDSNAttrEMailAddress: "user01 at example.com",
-                 },
-                 set((
-                    "mailto:user01 at example.com",
-                 )),
-                 "User with one email address, %s != %s",
-                ),
-                (
-                 "user02",
-                 {
-                    dsattributes.kDS1AttrGeneratedUID: "GUID-USER-02",
-                    dsattributes.kDSNAttrEMailAddress: ["user02 at example.com", "user02 at calendar.example.com"],
-                 },
-                 set((
-                    "mailto:user02 at example.com",
-                    "mailto:user02 at calendar.example.com",
-                 )),
-                 "User with multiple email addresses, %s != %s",
-                ),
-                (
-                 "user03",
-                 {
-                    dsattributes.kDS1AttrGeneratedUID: "GUID-USER-03",
-                 },
-                 set(()),
-                 "User with no email addresses, %s != %s",
-                ),
-            )
-
-            for recordName, record, result, title in data:
-                doTest(recordName, record, result, title)
-
-    class ODRecordsParse (twisted.trial.unittest.TestCase):
-
-        record_localod_good = ("computer1.apple.com", {
-            dsattributes.kDS1AttrGeneratedUID     : "GUID1",
-            dsattributes.kDSNAttrRecordName       : "computer1.apple.com",
-            'dsAttrTypeNative:apple-serviceinfo'  : PlistParse.plist_good,
-            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
-        })
-        record_localod_good_other = ("computer2.apple.com", {
-            dsattributes.kDS1AttrGeneratedUID     : "GUID1",
-            dsattributes.kDSNAttrRecordName       : "computer2.apple.com",
-            'dsAttrTypeNative:apple-serviceinfo'  : PlistParse.plist_good_other,
-            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
-        })
-        record_localod_duplicate = ("computer1", {
-            dsattributes.kDS1AttrGeneratedUID     : "GUID1_bad",
-            dsattributes.kDSNAttrRecordName       : "computer1",
-            'dsAttrTypeNative:apple-serviceinfo'  : PlistParse.plist_duplicate,
-            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
-        })
-        record_remoteod_good = ("computer3.apple.com", {
-            dsattributes.kDS1AttrGeneratedUID     : "GUID2",
-            dsattributes.kDSNAttrRecordName       : "computer3.apple.com",
-            'dsAttrTypeNative:apple-serviceinfo'  : PlistParse.plist_good,
-            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/directory.apple.com",
-        })
-        record_remoteod_duplicate = ("computer3", {
-            dsattributes.kDS1AttrGeneratedUID     : "GUID2",
-            dsattributes.kDSNAttrRecordName       : "computer3",
-            'dsAttrTypeNative:apple-serviceinfo'  : PlistParse.plist_duplicate,
-            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/directory.apple.com",
-        })
-        record_default_good = ("computer4.apple.com", {
-            dsattributes.kDS1AttrGeneratedUID     : "GUID3",
-            dsattributes.kDSNAttrRecordName       : "computer4.apple.com",
-            'dsAttrTypeNative:apple-serviceinfo'  : PlistParse.plist_good,
-            dsattributes.kDSNAttrMetaNodeLocation : "/Local/Default",
-        })
-        record_default_duplicate = ("computer4", {
-            dsattributes.kDS1AttrGeneratedUID     : "GUID3",
-            dsattributes.kDSNAttrRecordName       : "computer4",
-            'dsAttrTypeNative:apple-serviceinfo'  : PlistParse.plist_duplicate,
-            dsattributes.kDSNAttrMetaNodeLocation : "/Local/Default",
-        })
-
-        def test_odrecords_error(self):
-            def _doParseRecords(recordlist, title):
-                service = OpenDirectoryService(node="/Search", dosetup=False)
-                try:
-                    service._parseComputersRecords(recordlist, "calendar.apple.com")
-                    self.fail(msg="Record parse should have failed: %s" % (title,))
-                except OpenDirectoryInitError:
-                    pass
-
-            records = (
-                ((), "no records found"),
-                ((
-                      (ODRecordsParse.record_localod_good_other[0], ODRecordsParse.record_localod_good_other[1]),
-                 ), "non-matching record found"),
-            )
-
-            for recordlist, title in records:
-                _doParseRecords(recordlist, title)
-
-        def test_odrecords_good(self):
-            def _doParseRecords(recordlist, title):
-                service = OpenDirectoryService(node="/Search", dosetup=False)
-                try:
-                    service._parseComputersRecords(recordlist, "calendar.apple.com")
-                except OpenDirectoryInitError, ex:
-                    self.fail(msg="Record parse should not have failed: \"%s\" with error: %s" % (title, ex))
-
-            records = (
-                ((
-                      (ODRecordsParse.record_localod_good[0],       ODRecordsParse.record_localod_good[1]),
-                 ), "single good plist"),
-                ((
-                      (ODRecordsParse.record_localod_good[0],       ODRecordsParse.record_localod_good[1]),
-                      (ODRecordsParse.record_localod_good_other[0], ODRecordsParse.record_localod_good_other[1]),
-                 ), "multiple plists"),
-            )
-
-            for recordlist, title in records:
-                _doParseRecords(recordlist, title)
-
-        def test_odrecords_multiple(self):
-            def _doParseRecords(recordlist, title, tags):
-                service = OpenDirectoryService(node="/Search", dosetup=False)
-                service._parseComputersRecords(recordlist, "calendar.apple.com")
-
-                self.assertEquals(service.servicetags, set(tags),
-                                  "Got wrong service tags: %s and %s" % (service.servicetags, set(tags),))
-
-            records = (
-                (((ODRecordsParse.record_remoteod_good[0],  ODRecordsParse.record_remoteod_good[1]),
-                  (ODRecordsParse.record_localod_good[0],   ODRecordsParse.record_localod_good[1]),
-                  (ODRecordsParse.record_default_good[0],   ODRecordsParse.record_default_good[1])),
-                 "Three records",
-                 ("GUID2:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
-                  "GUID1:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
-                  "GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")),
-                (((ODRecordsParse.record_localod_good[0],   ODRecordsParse.record_localod_good[1]),
-                  (ODRecordsParse.record_default_good[0],   ODRecordsParse.record_default_good[1])),
-                 "Two records",
-                 ("GUID1:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
-                  "GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")),
-                (((ODRecordsParse.record_default_good[0],   ODRecordsParse.record_default_good[1]),),
-                 "One record",
-                 ("GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",)),
-            )
-
-            for recordlist, title, tags in records:
-                _doParseRecords(recordlist, title, tags)
-
-        def test_odrecords_duplicates(self):
-            def _doParseRecords(recordlist, title, items, tags):
-                service = OpenDirectoryService(node="/Search", dosetup=False)
-                service._parseComputersRecords(recordlist, "calendar.apple.com")
-                self.assertEquals(service.servicetags, set(tags))
-
-            records = (
-                (((ODRecordsParse.record_remoteod_good[0],       ODRecordsParse.record_remoteod_good[1]),
-                  (ODRecordsParse.record_remoteod_duplicate[0],  ODRecordsParse.record_remoteod_duplicate[1]),
-                  (ODRecordsParse.record_localod_good[0],        ODRecordsParse.record_localod_good[1]),
-                  (ODRecordsParse.record_default_good[0],        ODRecordsParse.record_default_good[1])),
-                 "Remote Record Duplicated", ("computer3.apple.com", "computer3",),
-                 ("GUID2:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
-                  "GUID2:1C8C34AC-3D9E-403C-8A33-FBC303F3840E:calendar",
-                  "GUID1:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
-                  "GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")),
-                (((ODRecordsParse.record_localod_good[0],        ODRecordsParse.record_localod_good[1]),
-                  (ODRecordsParse.record_localod_duplicate[0],   ODRecordsParse.record_localod_duplicate[1]),
-                  (ODRecordsParse.record_default_good[0],        ODRecordsParse.record_default_good[1])),
-                 "Local OD Duplicated", ("computer1.apple.com", "computer1",),
-                 ("GUID1:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
-                  "GUID1_bad:1C8C34AC-3D9E-403C-8A33-FBC303F3840E:calendar",
-                  "GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")),
-                (((ODRecordsParse.record_default_good[0],        ODRecordsParse.record_default_good[1]),
-                  (ODRecordsParse.record_default_duplicate[0],   ODRecordsParse.record_default_duplicate[1])),
-                 "Local Node Duplicated", ("computer4.apple.com", "computer4",),
-                 ("GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
-                  "GUID3:1C8C34AC-3D9E-403C-8A33-FBC303F3840E:calendar")),
-            )
-
-            for recordlist, title, items, tags in records:
-                _doParseRecords(recordlist, title, items, tags)
-
-    class ODResourceInfoParse (twisted.trial.unittest.TestCase):
-
-        plist_good_false = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <dict>
-        <key>AutoAcceptsInvitation</key>
-        <false/>
-        <key>Label</key>
-        <string>Location</string>
-        <key>CalendaringDelegate</key>
-        <string>1234-GUID-5678</string>
-        <key>ReadOnlyCalendaringDelegate</key>
-        <string>1234-GUID-5679</string>
-    </dict>
-</dict>
-</plist>
-"""
-
-        plist_good_true = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <dict>
-        <key>AutoAcceptsInvitation</key>
-        <true/>
-        <key>Label</key>
-        <string>Location</string>
-        <key>CalendaringDelegate</key>
-        <string></string>
-        <key>ReadOnlyCalendaringDelegate</key>
-        <string></string>
-    </dict>
-</dict>
-</plist>
-"""
-
-        plist_good_missing = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <dict>
-        <key>Label</key>
-        <string>Location</string>
-    </dict>
-</dict>
-</plist>
-"""
-
-        plist_bad = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <string>bogus</string>
-</dict>
-</plist>
-"""
-
-        plist_wrong = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.YellowPagesFramework</key>
-    <dict>
-        <key>AutoAcceptsInvitation</key>
-        <true/>
-        <key>Label</key>
-        <string>Location</string>
-        <key>CalendaringDelegate</key>
-        <string>1234-GUID-5678</string>
-        <key>ReadOnlyCalendaringDelegate</key>
-        <string>1234-GUID-5679</string>
-    </dict>
-</dict>
-</plist>
-"""
-
-        plist_invalid = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <string>bogus</string>
-    <string>another bogon</string>
-</dict>
-</plist>
-"""
-
-        plist_invalid_xml = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <string>R&D</string>
-</dict>
-</plist>
-"""
-
-        test_bool = (
-            (plist_good_false, False, "1234-GUID-5678", "1234-GUID-5679", None),
-            (plist_good_true, True, "", "", None),
-            (plist_good_missing, False, None, None, None),
-            (plist_wrong, False, None, None, None),
-            (plist_bad, False, None, None, ValueError),
-            (plist_invalid, False, None, None, ValueError),
-            (plist_invalid_xml, False, None, None, ValueError),
-        )
-
-        def test_plists(self):
-            service = OpenDirectoryService(node="/Search", dosetup=False)
-            
-            for item in ODResourceInfoParse.test_bool:
-                if item[4] is None:
-                    item1, item2, item3 = service._parseResourceInfo(item[0], "guid", "locations", "name")
-                    self.assertEqual(item1, item[1])
-                    self.assertEqual(item2, item[2])
-                    self.assertEqual(item3, item[3])
-                else:
-                    self.assertRaises(item[4], service._parseResourceInfo, item[0], "guid", "locations", "name")

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_principal.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_principal.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_principal.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,19 +16,19 @@
 
 import os
 
+from twisted.cred.credentials import UsernamePassword
 from twisted.internet.defer import inlineCallbacks
 from twisted.web2.dav import davxml
 from twisted.web2.dav.fileop import rmdir
 from twisted.web2.dav.resource import AccessDeniedError
 from twisted.web2.test.test_server import SimpleRequest
-from twisted.web2.dav.test.util import serialize
 
 from twistedcaldav.static import CalendarHomeProvisioningFile
-from twistedcaldav.directory.apache import BasicDirectoryService, DigestDirectoryService
+from twistedcaldav.config import config
+from twistedcaldav.directory import augment, calendaruserproxy
 from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.test.test_apache import basicUserFile, digestUserFile, groupFile, digestRealm
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
 from twistedcaldav.directory.principal import DirectoryPrincipalTypeProvisioningResource
 from twistedcaldav.directory.principal import DirectoryPrincipalResource
@@ -38,11 +38,6 @@
 
 import twistedcaldav.test.util
 
-directoryServices = (
-    BasicDirectoryService(digestRealm, basicUserFile, groupFile),
-    DigestDirectoryService(digestRealm, digestUserFile, groupFile),
-    XMLDirectoryService(xmlFile),
-)
 
 class ProvisionedPrincipals (twistedcaldav.test.util.TestCase):
     """
@@ -51,9 +46,13 @@
     def setUp(self):
         super(ProvisionedPrincipals, self).setUp()
 
+        self.directoryServices = (
+            XMLDirectoryService(xmlFile=xmlFile),
+        )
+
         # Set up a principals hierarchy for each service we're testing with
         self.principalRootResources = {}
-        for directory in directoryServices:
+        for directory in self.directoryServices:
             name = directory.__class__.__name__
             url = "/" + name + "/"
 
@@ -63,6 +62,9 @@
 
             self.principalRootResources[directory.__class__.__name__] = provisioningResource
 
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+        calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB(self.mktemp())
+
     def test_hierarchy(self):
         """
         DirectoryPrincipalProvisioningResource.listChildren(),
@@ -77,7 +79,7 @@
 
         DirectoryPrincipalResource.principalURL(),
         """
-        for directory in directoryServices:
+        for directory in self.directoryServices:
             #print "\n -> %s" % (directory.__class__.__name__,)
             provisioningResource = self.principalRootResources[directory.__class__.__name__]
 
@@ -139,7 +141,7 @@
         """
         DirectoryPrincipalProvisioningResource.principalForUser()
         """
-        for directory in directoryServices:
+        for directory in self.directoryServices:
             provisioningResource = self.principalRootResources[directory.__class__.__name__]
 
             for user in directory.listRecords(DirectoryService.recordType_users):
@@ -162,7 +164,7 @@
         """
         for provisioningResource, recordType, recordResource, record in self._allRecords():
             principal = provisioningResource.principalForRecord(record)
-            self.failIf(principal is None)
+            self.failIf(principal is None, msg=str(record))
             self.assertEquals(record, principal.record)
 
     def test_principalForCalendarUserAddress(self):
@@ -184,6 +186,13 @@
                 else:
                     self.failIf(principal is not None)
 
+        # Explicitly check the disabled record
+        provisioningResource = self.principalRootResources['XMLDirectoryService']
+        self.failIf(provisioningResource.principalForCalendarUserAddress("mailto:nocalendar at example.com") is not None)
+        self.failIf(provisioningResource.principalForCalendarUserAddress("urn:uuid:543D28BA-F74F-4D5F-9243-B3E3A61171E5") is not None)
+        self.failIf(provisioningResource.principalForCalendarUserAddress("/principals/users/nocalendar/") is not None)
+        self.failIf(provisioningResource.principalForCalendarUserAddress("/principals/__uids__/543D28BA-F74F-4D5F-9243-B3E3A61171E5/") is not None)
+
     def test_autoSchedule(self):
         """
         DirectoryPrincipalProvisioningResource.principalForCalendarUserAddress()
@@ -251,24 +260,6 @@
             memberships = yield recordResource.groupMemberships()
             self.failUnless(set(record.groups()).issubset(set(r.record for r in memberships if hasattr(r, "record"))))
 
-    def test_proxies(self):
-        """
-        DirectoryPrincipalResource.proxies()
-        """
-        for provisioningResource, recordType, recordResource, record in self._allRecords():
-            if record.enabledForCalendaring:
-                self.failUnless(set(record.proxies()).issubset(set(r.record for r in recordResource.proxies())))
-                self.assertEqual(record.hasEditableProxyMembership(), recordResource.hasEditableProxyMembership())
-
-    def test_read_only_proxies(self):
-        """
-        DirectoryPrincipalResource.proxies()
-        """
-        for provisioningResource, recordType, recordResource, record in self._allRecords():
-            if record.enabledForCalendaring:
-                self.failUnless(set(record.readOnlyProxies()).issubset(set(r.record for r in recordResource.readOnlyProxies())))
-                self.assertEqual(record.hasEditableProxyMembership(), recordResource.hasEditableProxyMembership())
-
     def test_principalUID(self):
         """
         DirectoryPrincipalResource.principalUID()
@@ -305,7 +296,7 @@
         # Need to create a calendar home provisioner for each service.
         calendarRootResources = {}
 
-        for directory in directoryServices:
+        for directory in self.directoryServices:
             url = "/homes_" + directory.__class__.__name__ + "/"
             path = os.path.join(self.docroot, url[1:])
 
@@ -352,37 +343,29 @@
         """
         Default access controls for principals.
         """
-        def work():
-            for provisioningResource, recordType, recordResource, record in self._allRecords():
-                for args in _authReadOnlyPrivileges(recordResource, recordResource.principalURL()):
-                    yield args
+        for provisioningResource, recordType, recordResource, record in self._allRecords():
+            for args in _authReadOnlyPrivileges(self, recordResource, recordResource.principalURL()):
+                yield self._checkPrivileges(*args)
 
-        for args in work():
-            yield self._checkPrivileges(*args)
-
     @inlineCallbacks
     def test_defaultAccessControlList_provisioners(self):
         """
         Default access controls for principal provisioning resources.
         """
-        def work():
-            for directory in directoryServices:
-                #print "\n -> %s" % (directory.__class__.__name__,)
-                provisioningResource = self.principalRootResources[directory.__class__.__name__]
+        for directory in self.directoryServices:
+            #print "\n -> %s" % (directory.__class__.__name__,)
+            provisioningResource = self.principalRootResources[directory.__class__.__name__]
 
-                for args in _authReadOnlyPrivileges(provisioningResource, provisioningResource.principalCollectionURL()):
-                    yield args
+            for args in _authReadOnlyPrivileges(self, provisioningResource, provisioningResource.principalCollectionURL()):
+                yield self._checkPrivileges(*args)
 
-                for recordType in provisioningResource.listChildren():
-                    #print "   -> %s" % (recordType,)
-                    typeResource = provisioningResource.getChild(recordType)
+            for recordType in provisioningResource.listChildren():
+                #print "   -> %s" % (recordType,)
+                typeResource = provisioningResource.getChild(recordType)
 
-                    for args in _authReadOnlyPrivileges(typeResource, typeResource.principalCollectionURL()):
-                        yield args
+                for args in _authReadOnlyPrivileges(self, typeResource, typeResource.principalCollectionURL()):
+                    yield self._checkPrivileges(*args)
 
-        for args in work():
-            yield self._checkPrivileges(*args)
-
     def _allRecords(self):
         """
         @return: an iterable of tuples
@@ -393,7 +376,7 @@
             C{record} is the directory service record
             for each record in each directory in C{directoryServices}.
         """
-        for directory in directoryServices:
+        for directory in self.directoryServices:
             provisioningResource = self.principalRootResources[directory.__class__.__name__]
             for recordType in directory.recordTypes():
                 for record in directory.listRecords(recordType):
@@ -425,13 +408,17 @@
         d.addCallback(gotResource)
         return d
 
-def _authReadOnlyPrivileges(resource, url):
-    for principal, privilege, allowed in (
-        ( davxml.All()             , davxml.Read()  , False ),
-        ( davxml.All()             , davxml.Write() , False ),
-        ( davxml.Unauthenticated() , davxml.Read()  , False ),
-        ( davxml.Unauthenticated() , davxml.Write() , False ),
-        ( davxml.Authenticated()   , davxml.Read()  , True  ),
-        ( davxml.Authenticated()   , davxml.Write() , False ),
-    ):
+def _authReadOnlyPrivileges(self, resource, url):
+    items = []
+    for provisioningResource, recordType, recordResource, record in self._allRecords():
+        if recordResource == resource:
+            items.append(( davxml.HRef().fromString(recordResource.principalURL()), davxml.Read()  , True ))
+            items.append(( davxml.HRef().fromString(recordResource.principalURL()), davxml.Write() , True ))
+        else:
+            items.append(( davxml.HRef().fromString(recordResource.principalURL()), davxml.Read()  , True ))
+            items.append(( davxml.HRef().fromString(recordResource.principalURL()), davxml.Write() , False ))
+    items.append(( davxml.Unauthenticated() , davxml.Read()  , False ))
+    items.append(( davxml.Unauthenticated() , davxml.Write() , False ))
+            
+    for principal, privilege, allowed in items:
         yield resource, url, principal, privilege, allowed

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipaldb.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipaldb.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipaldb.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -15,18 +15,19 @@
 ##
 from twistedcaldav.config import config
 
-import os
-
 from twisted.internet.defer import inlineCallbacks
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
+from twistedcaldav.directory.calendaruserproxy import ProxySqliteDB,\
+    ProxyPostgreSQLDB
 import twistedcaldav.test.util
+from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
+from twistedcaldav.directory import calendaruserproxy
 
-class ProxyPrincipalDB (twistedcaldav.test.util.TestCase):
+class ProxyPrincipalDBSqlite (twistedcaldav.test.util.TestCase):
     """
     Directory service provisioned principals.
     """
     
-    class old_CalendarUserProxyDatabase(CalendarUserProxyDatabase):
+    class old_ProxyDB(ProxySqliteDB):
         
         def _db_version(self):
             """
@@ -34,7 +35,7 @@
             """
             return "3"
             
-        def _db_init_data_tables(self, q):
+        def _db_init_data_tables(self):
             """
             Initialise the underlying database tables.
             @param q:           a database cursor to use.
@@ -43,7 +44,7 @@
             #
             # GROUPS table
             #
-            q.execute(
+            return self.execute(
                 """
                 create table GROUPS (
                     GROUPNAME   text,
@@ -52,7 +53,7 @@
                 """
             )
 
-    class new_CalendarUserProxyDatabase(CalendarUserProxyDatabase):
+    class new_ProxyDB(ProxySqliteDB):
         
         def _db_version(self):
             """
@@ -60,7 +61,7 @@
             """
             return "11"
             
-    class newer_CalendarUserProxyDatabase(CalendarUserProxyDatabase):
+    class newer_ProxyDB(ProxySqliteDB):
         
         def _db_version(self):
             """
@@ -73,8 +74,7 @@
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = CalendarUserProxyDatabase(db_path)
+        db = ProxySqliteDB(db_path)
         yield db.setGroupMembers("A", ("B", "C", "D",))
         
         membersA = yield db.getMembers("A")
@@ -83,29 +83,28 @@
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
 
+    @inlineCallbacks
     def test_DBIndexed(self):
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = CalendarUserProxyDatabase(db_path)
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
+        db = ProxySqliteDB(db_path)
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
 
+    @inlineCallbacks
     def test_OldDB(self):
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = self.old_CalendarUserProxyDatabase(db_path)
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set())
+        db = self.old_ProxyDB(db_path)
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set())
 
     @inlineCallbacks
     def test_DBUpgrade(self):
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = self.old_CalendarUserProxyDatabase(db_path)
+        db = self.old_ProxyDB(db_path)
         yield db.setGroupMembers("A", ("B", "C", "D",))
 
         membersA = yield db.getMembers("A")
@@ -113,19 +112,19 @@
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set())
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set())
+        db.close()
         db = None
         
-        db = CalendarUserProxyDatabase(db_path)
+        db = ProxySqliteDB(db_path)
 
         membersA = yield db.getMembers("A")
         membershipsB = yield db.getMemberships("B")
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+        db.close()
         db = None
 
     @inlineCallbacks
@@ -133,8 +132,7 @@
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = self.old_CalendarUserProxyDatabase(db_path)
+        db = self.old_ProxyDB(db_path)
         yield db.setGroupMembers("A", ("B", "C", "D",))
 
         membersA = yield db.getMembers("A")
@@ -142,19 +140,19 @@
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set())
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set())
+        db.close()
         db = None
         
-        db = self.new_CalendarUserProxyDatabase(db_path)
+        db = self.new_ProxyDB(db_path)
 
         membersA = yield db.getMembers("A")
         membershipsB = yield db.getMemberships("B")
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+        db.close()
         db = None
 
     @inlineCallbacks
@@ -162,8 +160,7 @@
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = self.new_CalendarUserProxyDatabase(db_path)
+        db = self.new_ProxyDB(db_path)
         yield db.setGroupMembers("A", ("B", "C", "D",))
 
         membersA = yield db.getMembers("A")
@@ -171,31 +168,30 @@
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+        db.close()
         db = None
         
-        db = self.newer_CalendarUserProxyDatabase(db_path)
+        db = self.newer_ProxyDB(db_path)
 
         membersA = yield db.getMembers("A")
         membershipsB = yield db.getMemberships("B")
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+        db.close()
         db = None
 
     @inlineCallbacks
     def test_cachingDBInsert(self):
     
         for processType in ("Single", "Combined",):
-            config.processType = processType
+            config.ProcessType = processType
 
             # Get the DB
             db_path = self.mktemp()
-            os.mkdir(db_path)
-            db = CalendarUserProxyDatabase(db_path)
+            db = ProxySqliteDB(db_path)
             
             # Do one insert and check the result
             yield db.setGroupMembers("A", ("B", "C", "D",))
@@ -231,12 +227,11 @@
     def test_cachingDBRemove(self):
     
         for processType in ("Single", "Combined",):
-            config.processType = processType
+            config.ProcessType = processType
 
             # Get the DB
             db_path = self.mktemp()
-            os.mkdir(db_path)
-            db = CalendarUserProxyDatabase(db_path)
+            db = ProxySqliteDB(db_path)
             
             # Do one insert and check the result
             yield db.setGroupMembers("A", ("B", "C", "D",))
@@ -270,15 +265,88 @@
             self.assertEqual(membershipsD, set())
 
     @inlineCallbacks
+    def test_cachingDBRemoveSpecial(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            db_path = self.mktemp()
+            db = ProxySqliteDB(db_path)
+            
+            # Do one insert and check the result
+            yield db.setGroupMembers("A", ("B", "C", "D",))
+            yield db.setGroupMembers("X", ("B", "C",))
+    
+            membershipsB = yield db.getMemberships("B")
+            membershipsC = yield db.getMemberships("C")
+            membershipsD = yield db.getMemberships("D")
+            
+            # Remove and check the result
+            yield db.removeGroup("A")
+    
+            membersA = yield db.getMembers("A")
+            membersX = yield db.getMembers("X")
+            membershipsB = yield db.getMemberships("B")
+            membershipsC = yield db.getMemberships("C")
+            membershipsD = yield db.getMemberships("D")
+    
+            self.assertEqual(membersA, set())
+            self.assertEqual(membersX, set(("B", "C",)))
+            self.assertEqual(membershipsB, set("X",))
+            self.assertEqual(membershipsC, set("X",))
+            self.assertEqual(membershipsD, set())
+
+    @inlineCallbacks
+    def test_cachingDBRemovePrincipal(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            db_path = self.mktemp()
+            db = ProxySqliteDB(db_path)
+            
+            # Do one insert and check the result
+            yield db.setGroupMembers("A", ("B", "C", "D",))
+            yield db.setGroupMembers("X", ("B", "C",))
+    
+            membersA = yield db.getMembers("A")
+            membersX = yield db.getMembers("X")
+            membershipsB = yield db.getMemberships("B")
+            membershipsC = yield db.getMemberships("C")
+            membershipsD = yield db.getMemberships("D")
+    
+            self.assertEqual(membersA, set(("B", "C", "D",)))
+            self.assertEqual(membersX, set(("B", "C",)))
+            self.assertEqual(membershipsB, set(("A", "X",)))
+            self.assertEqual(membershipsC, set(("A", "X",)))
+            self.assertEqual(membershipsD, set(("A",)))
+            
+            # Remove and check the result
+            yield db.removePrincipal("B")
+    
+            membersA = yield db.getMembers("A")
+            membersX = yield db.getMembers("X")
+            membershipsB = yield db.getMemberships("B")
+            membershipsC = yield db.getMemberships("C")
+            membershipsD = yield db.getMemberships("D")
+    
+            self.assertEqual(membersA, set(("C", "D",)))
+            self.assertEqual(membersX, set(("C",)))
+            self.assertEqual(membershipsB, set())
+            self.assertEqual(membershipsC, set(("A", "X",)))
+            self.assertEqual(membershipsD, set(("A",),))
+
+    @inlineCallbacks
     def test_cachingDBInsertUncached(self):
     
         for processType in ("Single", "Combined",):
-            config.processType = processType
+            config.ProcessType = processType
 
             # Get the DB
             db_path = self.mktemp()
-            os.mkdir(db_path)
-            db = CalendarUserProxyDatabase(db_path)
+            db = ProxySqliteDB(db_path)
             
             # Do one insert and check the result for the one we will remove
             yield db.setGroupMembers("AA", ("BB", "CC", "DD",))
@@ -299,3 +367,234 @@
             self.assertEqual(membershipsDD, set())
             self.assertEqual(membershipsEE, set(("AA",)))
 
+class ProxyPrincipalDBPostgreSQL (twistedcaldav.test.util.TestCase):
+    """
+    Directory service provisioned principals.
+    """
+    
+    @inlineCallbacks
+    def setUp(self):
+
+        super(ProxyPrincipalDBPostgreSQL, self).setUp()
+        self.db = ProxyPostgreSQLDB(host="localhost", database="proxies")
+        yield self.db.clean()
+
+    @inlineCallbacks
+    def tearDown(self):
+        yield self.db.close()
+        self.db = None
+
+    @inlineCallbacks
+    def test_normalDB(self):
+    
+        # Get the DB
+        yield self.db.clean()
+        
+        calendaruserproxy.ProxyDBService = self.db
+        loader = XMLCalendarUserProxyLoader("/Volumes/Data/Users/cyrusdaboo/Documents/Development/Apple/eclipse/CalendarServer-3/conf/auth/proxies-test.xml")
+        yield loader.updateProxyDB()
+
+        yield self.db.setGroupMembers("A", ("B", "C", "D",))
+        
+        membersA = yield self.db.getMembers("A")
+        membershipsB = yield self.db.getMemberships("B")
+        
+        self.assertEqual(membersA, set(("B", "C", "D",)))
+        self.assertEqual(membershipsB, set(("A",)))
+
+    @inlineCallbacks
+    def test_DBIndexed(self):
+    
+        # Get the DB
+        yield self.db.clean()
+        self.assertTrue((yield self.db.queryOne("select hasindexes from pg_tables where tablename = 'groups'")))
+
+    @inlineCallbacks
+    def test_cachingDBInsert(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            yield self.db.clean()
+            
+            # Do one insert and check the result
+            yield self.db.setGroupMembers("A", ("B", "C", "D",))
+    
+            membersA = yield self.db.getMembers("A")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+            membershipsE = yield self.db.getMemberships("E")
+    
+            self.assertEqual(membersA, set(("B", "C", "D",)))
+            self.assertEqual(membershipsB, set(("A",)))
+            self.assertEqual(membershipsC, set(("A",)))
+            self.assertEqual(membershipsD, set(("A",)))
+            self.assertEqual(membershipsE, set(()))
+            
+            # Change and check the result
+            yield self.db.setGroupMembers("A", ("B", "C", "E",))
+    
+            membersA = yield self.db.getMembers("A")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+            membershipsE = yield self.db.getMemberships("E")
+    
+            self.assertEqual(membersA, set(("B", "C", "E",)))
+            self.assertEqual(membershipsB, set(("A",)))
+            self.assertEqual(membershipsC, set(("A",)))
+            self.assertEqual(membershipsD, set())
+            self.assertEqual(membershipsE, set(("A",)))
+
+    @inlineCallbacks
+    def test_cachingDBRemove(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            yield self.db.clean()
+            
+            # Do one insert and check the result
+            yield self.db.setGroupMembers("A", ("B", "C", "D",))
+            yield self.db.setGroupMembers("X", ("B", "C",))
+    
+            membersA = yield self.db.getMembers("A")
+            membersX = yield self.db.getMembers("X")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+    
+            self.assertEqual(membersA, set(("B", "C", "D",)))
+            self.assertEqual(membersX, set(("B", "C",)))
+            self.assertEqual(membershipsB, set(("A", "X",)))
+            self.assertEqual(membershipsC, set(("A", "X",)))
+            self.assertEqual(membershipsD, set(("A",)))
+            
+            # Remove and check the result
+            yield self.db.removeGroup("A")
+    
+            membersA = yield self.db.getMembers("A")
+            membersX = yield self.db.getMembers("X")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+    
+            self.assertEqual(membersA, set())
+            self.assertEqual(membersX, set(("B", "C",)))
+            self.assertEqual(membershipsB, set("X",))
+            self.assertEqual(membershipsC, set("X",))
+            self.assertEqual(membershipsD, set())
+
+    @inlineCallbacks
+    def test_cachingDBRemoveSpecial(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            yield self.db.clean()
+            
+            # Do one insert and check the result
+            yield self.db.setGroupMembers("A", ("B", "C", "D",))
+            yield self.db.setGroupMembers("X", ("B", "C",))
+    
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+            
+            # Remove and check the result
+            yield self.db.removeGroup("A")
+    
+            membersA = yield self.db.getMembers("A")
+            membersX = yield self.db.getMembers("X")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+    
+            self.assertEqual(membersA, set())
+            self.assertEqual(membersX, set(("B", "C",)))
+            self.assertEqual(membershipsB, set("X",))
+            self.assertEqual(membershipsC, set("X",))
+            self.assertEqual(membershipsD, set())
+
+    @inlineCallbacks
+    def test_cachingDBRemovePrincipal(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            yield self.db.clean()
+            
+            # Do one insert and check the result
+            yield self.db.setGroupMembers("A", ("B", "C", "D",))
+            yield self.db.setGroupMembers("X", ("B", "C",))
+    
+            membersA = yield self.db.getMembers("A")
+            membersX = yield self.db.getMembers("X")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+    
+            self.assertEqual(membersA, set(("B", "C", "D",)))
+            self.assertEqual(membersX, set(("B", "C",)))
+            self.assertEqual(membershipsB, set(("A", "X",)))
+            self.assertEqual(membershipsC, set(("A", "X",)))
+            self.assertEqual(membershipsD, set(("A",)))
+            
+            # Remove and check the result
+            yield self.db.removePrincipal("B")
+    
+            membersA = yield self.db.getMembers("A")
+            membersX = yield self.db.getMembers("X")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+    
+            self.assertEqual(membersA, set(("C", "D",)))
+            self.assertEqual(membersX, set(("C",)))
+            self.assertEqual(membershipsB, set())
+            self.assertEqual(membershipsC, set(("A", "X",)))
+            self.assertEqual(membershipsD, set(("A",),))
+
+    @inlineCallbacks
+    def test_cachingDBInsertUncached(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            yield self.db.clean()
+            
+            # Do one insert and check the result for the one we will remove
+            yield self.db.setGroupMembers("AA", ("BB", "CC", "DD",))
+            yield self.db.getMemberships("DD")
+    
+            # Change and check the result
+            yield self.db.setGroupMembers("AA", ("BB", "CC", "EE",))
+    
+            membersAA = yield self.db.getMembers("AA")
+            membershipsBB = yield self.db.getMemberships("BB")
+            membershipsCC = yield self.db.getMemberships("CC")
+            membershipsDD = yield self.db.getMemberships("DD")
+            membershipsEE = yield self.db.getMemberships("EE")
+    
+            self.assertEqual(membersAA, set(("BB", "CC", "EE",)))
+            self.assertEqual(membershipsBB, set(("AA",)))
+            self.assertEqual(membershipsCC, set(("AA",)))
+            self.assertEqual(membershipsDD, set())
+            self.assertEqual(membershipsEE, set(("AA",)))
+
+
+try:
+    import pgdb
+except ImportError:
+    ProxyPrincipalDBPostgreSQL.skip = True
+else:
+    try:
+        db = pgdb.connect(host="localhost", database="proxies")
+    except:
+        ProxyPrincipalDBPostgreSQL.skip = True

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,39 +14,54 @@
 # limitations under the License.
 ##
 
-from twisted.internet.defer import DeferredList, inlineCallbacks, succeed
+from twisted.internet.defer import DeferredList, inlineCallbacks, returnValue,\
+    succeed
 from twisted.web2.dav import davxml
 
 from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile,\
+    proxiesFile
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
 from twistedcaldav.directory.principal import DirectoryPrincipalResource
+from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
+from twistedcaldav.directory.xmlfile import XMLDirectoryService
 
 import twistedcaldav.test.util
+from twistedcaldav.config import config
+from twistedcaldav.directory import augment, calendaruserproxy
+from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
+import os
 
-directoryService = XMLDirectoryService(xmlFile)
-
 class ProxyPrincipals (twistedcaldav.test.util.TestCase):
     """
     Directory service provisioned principals.
     """
+    
+    @inlineCallbacks
     def setUp(self):
         super(ProxyPrincipals, self).setUp()
 
+        self.directoryService = XMLDirectoryService(xmlFile=xmlFile)
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+        calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB(self.mktemp())
+
         # Set up a principals hierarchy for each service we're testing with
         self.principalRootResources = {}
-        name = directoryService.__class__.__name__
+        name = self.directoryService.__class__.__name__
         url = "/" + name + "/"
 
-        provisioningResource = DirectoryPrincipalProvisioningResource(url, directoryService)
+        provisioningResource = DirectoryPrincipalProvisioningResource(url, self.directoryService)
 
         self.site.resource.putChild(name, provisioningResource)
 
-        self.principalRootResources[directoryService.__class__.__name__] = provisioningResource
+        self.principalRootResources[self.directoryService.__class__.__name__] = provisioningResource
 
+        config.DataRoot = self.mktemp()
+        os.mkdir(config.DataRoot)
+        yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
+
     def _getPrincipalByShortName(self, type, name):
-        provisioningResource = self.principalRootResources[directoryService.__class__.__name__]
+        provisioningResource = self.principalRootResources[self.directoryService.__class__.__name__]
         return provisioningResource.principalForShortName(type, name)
 
     def _groupMembersTest(self, recordType, recordName, subPrincipalName, expectedMembers):
@@ -76,6 +91,43 @@
         return d
     
     @inlineCallbacks
+    def _addProxy(self, principal, subPrincipalName, proxyPrincipal):
+
+        if isinstance(principal, tuple):
+            principal = self._getPrincipalByShortName(principal[0], principal[1])
+        principal = principal.getChild(subPrincipalName)
+        members = (yield principal.groupMembers())
+
+        if isinstance(proxyPrincipal, tuple):
+            proxyPrincipal = self._getPrincipalByShortName(proxyPrincipal[0], proxyPrincipal[1])
+        members.add(proxyPrincipal)
+        
+        yield principal.setGroupMemberSetPrincipals(members)
+
+    @inlineCallbacks
+    def _removeProxy(self, recordType, recordName, subPrincipalName, proxyRecordType, proxyRecordName):
+
+        principal = self._getPrincipalByShortName(recordType, recordName)
+        principal = principal.getChild(subPrincipalName)
+        members = (yield principal.groupMembers())
+
+        proxyPrincipal = self._getPrincipalByShortName(proxyRecordType, proxyRecordName)
+        for p in members:
+            if p.principalUID() == proxyPrincipal.principalUID():
+                members.remove(p)
+                break
+        
+        yield principal.setGroupMemberSetPrincipals(members)
+
+    @inlineCallbacks
+    def _clearProxy(self, principal, subPrincipalName):
+
+        if isinstance(principal, tuple):
+            principal = self._getPrincipalByShortName(principal[0], principal[1])
+        principal = principal.getChild(subPrincipalName)
+        yield principal.setGroupMemberSetPrincipals(set())
+
+    @inlineCallbacks
     def _proxyForTest(self, recordType, recordName, expectedProxies, read_write):
         principal = self._getPrincipalByShortName(recordType, recordName)
         proxies = (yield principal.proxyFor(read_write))
@@ -215,7 +267,7 @@
                 return succeed(self.members)
 
 
-        user = self._getPrincipalByShortName(directoryService.recordType_users,
+        user = self._getPrincipalByShortName(self.directoryService.recordType_users,
                                            "cdaboo")
 
         proxyGroup = user.getChild("calendar-proxy-write")
@@ -243,8 +295,9 @@
             changedCount = 0
             def changed(self):
                 self.changedCount += 1
+                return succeed(True)
 
-        user = self._getPrincipalByShortName(directoryService.recordType_users, "cdaboo")
+        user = self._getPrincipalByShortName(self.directoryService.recordType_users, "cdaboo")
 
         proxyGroup = user.getChild("calendar-proxy-write")
 
@@ -253,7 +306,7 @@
         oldCacheNotifier = DirectoryPrincipalResource.cacheNotifierFactory
 
         try:
-            DirectoryPrincipalResource.cacheNotifierFactory = (lambda _1, _2: notifier)
+            DirectoryPrincipalResource.cacheNotifierFactory = (lambda _1, _2, **kwargs: notifier)
 
             self.assertEquals(notifier.changedCount, 0)
 
@@ -283,3 +336,187 @@
             False
         )
 
+    @inlineCallbacks
+    def test_UserProxy(self):
+        
+        for proxyType in ("calendar-proxy-read", "calendar-proxy-write"):
+
+            yield self._addProxy(
+                (DirectoryService.recordType_users, "wsanchez",),
+                proxyType,
+                (DirectoryService.recordType_users, "cdaboo",),
+            )
+    
+            yield self._groupMembersTest(
+                DirectoryService.recordType_users, "wsanchez",
+                proxyType,
+                ("Cyrus Daboo",),
+            )
+            
+            yield self._addProxy(
+                (DirectoryService.recordType_users, "wsanchez",),
+                proxyType,
+                (DirectoryService.recordType_users, "lecroy",),
+            )
+    
+            yield self._groupMembersTest(
+                DirectoryService.recordType_users, "wsanchez",
+                proxyType,
+                ("Cyrus Daboo", "Chris Lecroy",),
+            )
+    
+            yield self._removeProxy(
+                DirectoryService.recordType_users, "wsanchez",
+                proxyType,
+                DirectoryService.recordType_users, "cdaboo",
+            )
+    
+            yield self._groupMembersTest(
+                DirectoryService.recordType_users, "wsanchez",
+                proxyType,
+                ("Chris Lecroy",),
+            )
+
+    @inlineCallbacks
+    def test_InvalidUserProxy(self):
+
+
+        # Set up the in-memory (non-null) memcacher:
+        config.ProcessType = "Single"
+        calendaruserproxy.ProxyDBService._memcacher._memcacheProtocol = None
+        principal = self._getPrincipalByShortName(
+            DirectoryService.recordType_users, "wsanchez")
+        db = principal._calendar_user_proxy_index()
+
+        # Set the clock to the epoch:
+        theTime = 0
+        db._memcacher.theTime = theTime
+
+
+        for doMembershipFirst in (True, False):
+            for proxyType in ("calendar-proxy-read", "calendar-proxy-write"):
+
+                principal = self._getPrincipalByShortName(DirectoryService.recordType_users, "wsanchez")
+                proxyGroup = principal.getChild(proxyType)
+
+                testPrincipal = self._getPrincipalByShortName(DirectoryService.recordType_users, "cdaboo")
+
+                fakePrincipal = self._getPrincipalByShortName(DirectoryService.recordType_users, "dreid")
+                fakeProxyGroup = fakePrincipal.getChild(proxyType)
+
+                yield self._addProxy(
+                    principal,
+                    proxyType,
+                    testPrincipal,
+                )
+
+                members = yield proxyGroup._index().getMembers(proxyGroup.uid)
+                self.assertEquals(len(members), 1)
+
+                yield self._addProxy(
+                    fakePrincipal,
+                    proxyType,
+                    testPrincipal,
+                )
+                members = yield fakeProxyGroup._index().getMembers(fakeProxyGroup.uid)
+                self.assertEquals(len(members), 1)
+
+                uids = [p.principalUID() for p in (yield testPrincipal.groupMemberships())]
+                self.assertTrue("5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1#%s" % (proxyType,) in uids)
+
+                memberships = yield testPrincipal._calendar_user_proxy_index().getMemberships(testPrincipal.principalUID())
+                self.assertEquals(len(memberships), 2)
+
+                yield self._addProxy(
+                    principal,
+                    proxyType,
+                    fakePrincipal,
+                )
+                members = yield proxyGroup._index().getMembers(proxyGroup.uid)
+                self.assertEquals(len(members), 2)
+
+                # Remove the dreid user from the directory service
+                del self.directoryService._accounts()[DirectoryService.recordType_users]["dreid"]
+
+
+                cacheTimeout = config.DirectoryService.params.get("cacheTimeout", 30) * 60 * 2
+
+                @inlineCallbacks
+                def _membershipTest():
+
+                    uids = [p.principalUID() for p in (yield testPrincipal.groupMemberships())]
+                    self.assertTrue("5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1#%s" % (proxyType,) not in uids)
+
+                    memberships = yield testPrincipal._calendar_user_proxy_index().getMemberships(testPrincipal.principalUID())
+                    self.assertEquals(len(memberships), 1)
+
+                @inlineCallbacks
+                def _membersTest(theTime):
+                    yield self._groupMembersTest(
+                        DirectoryService.recordType_users, "wsanchez",
+                        proxyType,
+                        ("Cyrus Daboo",),
+                    )
+
+                    # Trigger the proxy DB clean up, which won't actually
+                    # remove anything because we haven't exceeded the timeout
+                    yield proxyGroup.groupMembers()
+
+                    # Advance 10 seconds
+                    theTime += 10
+                    db._memcacher.theTime = theTime
+
+                    # When we first examine the members, we have not exceeded
+                    # the clean-up timeout, so we'll still have 2:
+                    members = yield proxyGroup._index().getMembers(proxyGroup.uid)
+                    self.assertEquals(len(members), 2)
+
+                    # Restore removed user
+                    parser = XMLAccountsParser(self.directoryService.xmlFile)
+                    self.directoryService._parsedAccounts = parser.items
+                    self.directoryService.recordWithShortName(
+                        DirectoryService.recordType_users, "dreid")
+
+                    # Trigger the proxy DB clean up, which will actually
+                    # remove the deletion timer because the principal has been
+                    # restored
+                    yield proxyGroup.groupMembers()
+
+                    # Verify the deletion timer has been removed
+                    result = yield db._memcacher.checkDeletionTimer("5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1")
+                    self.assertEquals(result, None)
+
+                    # Remove the dreid user from the directory service
+                    del self.directoryService._accounts()[DirectoryService.recordType_users]["dreid"]
+
+                    # Trigger the proxy DB clean up, which won't actually
+                    # remove anything because we haven't exceeded the timeout
+                    yield proxyGroup.groupMembers()
+
+                    # Advance beyond the timeout
+                    theTime += cacheTimeout
+                    db._memcacher.theTime = theTime
+
+                    # Trigger the proxy DB clean up
+                    yield proxyGroup.groupMembers()
+
+                    # The missing principal has now been cleaned out of the
+                    # proxy DB
+                    members = yield proxyGroup._index().getMembers(proxyGroup.uid)
+                    self.assertEquals(len(members), 1)
+                    returnValue(theTime)
+
+
+                if doMembershipFirst:
+                    yield _membershipTest()
+                    theTime = yield _membersTest(theTime)
+                else:
+                    theTime = yield _membersTest(theTime)
+                    yield _membershipTest()
+
+                # Restore removed user
+                parser = XMLAccountsParser(self.directoryService.xmlFile)
+                self.directoryService._parsedAccounts = parser.items
+
+                yield self._clearProxy(principal, proxyType)
+                yield self._clearProxy(fakePrincipal, proxyType)

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_sqldb.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_sqldb.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_sqldb.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,51 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import os
-
-from twisted.python.filepath import FilePath
-
-import twistedcaldav.directory.test.util
-import twistedcaldav.directory.test.test_xmlfile
-from twistedcaldav.directory.sqldb import SQLDirectoryService
-
-xmlFile = FilePath(os.path.join(os.path.dirname(__file__), "accounts.xml"))
-
-# FIXME: Add tests for GUID hooey, once we figure out what that means here
-
-class SQLDB (
-    twistedcaldav.directory.test.test_xmlfile.XMLFileBase,
-    twistedcaldav.directory.test.util.BasicTestCase,
-    twistedcaldav.directory.test.util.DigestTestCase
-):
-    """
-    Test SQL directory implementation.
-    """
-    def service(self):
-        return SQLDirectoryService(os.getcwd(), self.xmlFile())
-
-    def test_verifyCredentials_digest(self):
-        super(SQLDB, self).test_verifyCredentials_digest()
-    test_verifyCredentials_digest.todo = ""
-
-    def test_verifyRealmFromDB(self):
-        # Make sure the database has been initialized with the XML file
-        self.service()
-
-        # Then get an instance without using the XML file
-        service = SQLDirectoryService(os.getcwd(), None)
-
-        self.assertEquals(service.realmName, "Test")

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_util.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_util.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_util.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -15,12 +15,11 @@
 ##
 
 from twistedcaldav.directory.util import uuidFromName
+from twistedcaldav.test.util import TestCase
 
-import twisted.trial.unittest
-
 uuid_namespace_dns = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
 
-class UUID (twisted.trial.unittest.TestCase):
+class UUID (TestCase):
     def test_uuidFromName(self):
         self.assertEquals(
             uuidFromName(uuid_namespace_dns, "python.org"),

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_xmlfile.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_xmlfile.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_xmlfile.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -18,11 +18,14 @@
 
 from twisted.python.filepath import FilePath
 
+from twistedcaldav.directory import augment
 from twistedcaldav.directory.directory import DirectoryService
 import twistedcaldav.directory.test.util
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 
 xmlFile = FilePath(os.path.join(os.path.dirname(__file__), "accounts.xml"))
+augmentsFile = FilePath(os.path.join(os.path.dirname(__file__), "augments.xml"))
+proxiesFile = FilePath(os.path.join(os.path.dirname(__file__), "proxies.xml"))
 
 # FIXME: Add tests for GUID hooey, once we figure out what that means here
 
@@ -35,13 +38,14 @@
     ))
 
     users = {
-        "admin"   : { "password": "nimda",    "guid": "D11F03A0-97EA-48AF-9A6C-FAC7F3975766", "addresses": () },
-        "wsanchez": { "password": "zehcnasw", "guid": "6423F94A-6B76-4A3A-815B-D52CFD77935D", "addresses": ("mailto:wsanchez at example.com",) },
-        "cdaboo"  : { "password": "oobadc",   "guid": "5A985493-EE2C-4665-94CF-4DFEA3A89500", "addresses": ("mailto:cdaboo at example.com",)   },
-        "lecroy"  : { "password": "yorcel",   "guid": "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0", "addresses": ("mailto:lecroy at example.com",)   },
-        "dreid"   : { "password": "dierd",    "guid": "5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1", "addresses": ("mailto:dreid at example.com",)    },
-        "user01"  : { "password": "01user",   "guid": None                                  , "addresses": () },
-        "user02"  : { "password": "02user",   "guid": None                                  , "addresses": () },
+        "admin"      : { "password": "nimda",      "guid": "D11F03A0-97EA-48AF-9A6C-FAC7F3975766", "addresses": () },
+        "wsanchez"   : { "password": "zehcnasw",   "guid": "6423F94A-6B76-4A3A-815B-D52CFD77935D", "addresses": ("mailto:wsanchez at example.com",) },
+        "cdaboo"     : { "password": "oobadc",     "guid": "5A985493-EE2C-4665-94CF-4DFEA3A89500", "addresses": ("mailto:cdaboo at example.com",)   },
+        "lecroy"     : { "password": "yorcel",     "guid": "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0", "addresses": ("mailto:lecroy at example.com",)   },
+        "dreid"      : { "password": "dierd",      "guid": "5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1", "addresses": ("mailto:dreid at example.com",)    },
+        "nocalendar" : { "password": "radnelacon", "guid": "543D28BA-F74F-4D5F-9243-B3E3A61171E5", "addresses": () },
+        "user01"     : { "password": "01user",     "guid": None                                  , "addresses": () },
+        "user02"     : { "password": "02user",     "guid": None                                  , "addresses": () },
     }
 
     groups = {
@@ -83,6 +87,12 @@
             xmlFile.copyTo(self._xmlFile)
         return self._xmlFile
 
+    def augmentsFile(self):
+        if not hasattr(self, "_augmentsFile"):
+            self._augmentsFile = FilePath(self.mktemp())
+            augmentsFile.copyTo(self._augmentsFile)
+        return self._augmentsFile
+
 class XMLFile (
     XMLFileBase,
     twistedcaldav.directory.test.util.BasicTestCase,
@@ -92,7 +102,9 @@
     Test XML file based directory implementation.
     """
     def service(self):
-        return XMLDirectoryService(self.xmlFile())
+        directory = XMLDirectoryService(self.xmlFile())
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,))
+        return directory
 
     def test_changedXML(self):
         service = self.service()
@@ -103,6 +115,7 @@
 <accounts realm="Test Realm">
   <user>
     <uid>admin</uid>
+    <guid>admin</guid>
     <password>nimda</password>
     <name>Super User</name>
   </user>
@@ -129,13 +142,28 @@
 <accounts realm="Test Realm">
   <location>
     <uid>my office</uid>
+    <guid>myoffice</guid>
     <password>nimda</password>
     <name>Super User</name>
-    <auto-schedule/>
   </location>
 </accounts>
 """
         )
+        self.augmentsFile().open("w").write(
+"""<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE accounts SYSTEM "accounts.dtd">
+<augments>
+  <record>
+    <guid>myoffice</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+</augments>
+"""
+        )
+        augment.AugmentService.refresh()
+
         for recordType, expectedRecords in (
             ( DirectoryService.recordType_users     , ()             ),
             ( DirectoryService.recordType_groups    , ()             ),
@@ -148,28 +176,7 @@
             )
         self.assertTrue(service.recordWithShortName(DirectoryService.recordType_locations, "my office").autoSchedule)
 
-    def test_badAutoSchedule(self):
-        service = self.service()
 
-        self.xmlFile().open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-<accounts realm="Test Realm">
-  <user>
-    <uid>my office</uid>
-    <password>nimda</password>
-    <name>Super User</name>
-    <auto-schedule/>
-  </user>
-</accounts>
-"""
-        )
-        
-        def _findRecords():
-            set(r.shortName for r in service.listRecords(DirectoryService.recordType_users))
-
-        self.assertRaises(ValueError, _findRecords)
-        
     def test_okDisableCalendar(self):
         service = self.service()
 
@@ -186,11 +193,11 @@
     <uid>disabled</uid>
     <password>disabled</password>
     <name>Disabled</name>
-    <disable-calendar/>
   </group>
 </accounts>
 """
         )
+        
         for recordType, expectedRecords in (
             ( DirectoryService.recordType_users     , ()                       ),
             ( DirectoryService.recordType_groups    , ("enabled", "disabled")  ),
@@ -201,88 +208,5 @@
                 set(r.shortName for r in service.listRecords(recordType)),
                 set(expectedRecords)
             )
-        self.assertTrue(service.recordWithShortName(DirectoryService.recordType_groups, "enabled").enabledForCalendaring)
+        self.assertFalse(service.recordWithShortName(DirectoryService.recordType_groups, "enabled").enabledForCalendaring)
         self.assertFalse(service.recordWithShortName(DirectoryService.recordType_groups, "disabled").enabledForCalendaring)
-
-    def test_badDisableCalendar(self):
-        service = self.service()
-
-        self.xmlFile().open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-<accounts realm="Test Realm">
-  <location>
-    <uid>my office</uid>
-    <password>nimda</password>
-    <name>Super User</name>
-    <disable-calendar/>
-  </location>
-</accounts>
-"""
-        )
-        
-        def _findRecords():
-            set(r.shortName for r in service.listRecords(DirectoryService.recordType_users))
-
-        self.assertRaises(ValueError, _findRecords)
-
-    def test_okProxies(self):
-        service = self.service()
-
-        self.xmlFile().open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-<accounts realm="Test Realm">
-  <user>
-    <uid>test</uid>
-    <password>nimda</password>
-    <name>Test</name>
-  </user>
-  <location>
-    <uid>my office</uid>
-    <password>nimda</password>
-    <name>Super User</name>
-    <auto-schedule/>
-    <proxies>
-        <member>test</member>
-    </proxies>
-  </location>
-</accounts>
-"""
-        )
-        for recordType, expectedRecords in (
-            ( DirectoryService.recordType_users     , ("test",)      ),
-            ( DirectoryService.recordType_groups    , ()             ),
-            ( DirectoryService.recordType_locations , ("my office",) ),
-            ( DirectoryService.recordType_resources , ()             ),
-        ):
-            self.assertEquals(
-                set(r.shortName for r in service.listRecords(recordType)),
-                set(expectedRecords)
-            )
-        self.assertEqual(set([("users", "test",)],), service.recordWithShortName(DirectoryService.recordType_locations, "my office")._proxies)
-        self.assertEqual(set([("locations", "my office",)],), service.recordWithShortName(DirectoryService.recordType_users, "test")._proxyFor)
-
-    def test_badProxies(self):
-        service = self.service()
-
-        self.xmlFile().open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-<accounts realm="Test Realm">
-  <user>
-    <uid>my office</uid>
-    <password>nimda</password>
-    <name>Super User</name>
-    <proxies>
-        <member>12345-GUID-67890</member>
-    </proxies>
-  </user>
-</accounts>
-"""
-        )
-        
-        def _findRecords():
-            set(r.shortName for r in service.listRecords(DirectoryService.recordType_users))
-
-        self.assertRaises(ValueError, _findRecords)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/util.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/util.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/util.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -14,17 +14,17 @@
 # limitations under the License.
 ##
 
-import twisted.trial.unittest
 from twisted.trial.unittest import SkipTest
 from twisted.cred.credentials import UsernamePassword
 from twisted.web2.auth.digest import DigestedCredentials, calcResponse, calcHA1
 
 from twistedcaldav.directory.directory import DirectoryService
 from twistedcaldav.directory.directory import UnknownRecordTypeError
+from twistedcaldav.test.util import TestCase
 
 # FIXME: Add tests for GUID hooey, once we figure out what that means here
 
-class DirectoryTestCase (twisted.trial.unittest.TestCase):
+class DirectoryTestCase (TestCase):
     """
     Tests a directory implementation.
     """

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlaccountsparser.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlaccountsparser.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlaccountsparser.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@
     "XMLAccountsParser",
 ]
 
-from uuid import UUID
 import xml.dom.minidom
 
 from twisted.python.filepath import FilePath
@@ -43,18 +42,17 @@
 ELEMENT_GUID              = "guid"
 ELEMENT_PASSWORD          = "password"
 ELEMENT_NAME              = "name"
+ELEMENT_EMAIL_ADDRESS     = "email-address"
 ELEMENT_MEMBERS           = "members"
 ELEMENT_MEMBER            = "member"
-ELEMENT_CUADDR            = "cuaddr"
-ELEMENT_AUTOSCHEDULE      = "auto-schedule"
-ELEMENT_DISABLECALENDAR   = "disable-calendar"
-ELEMENT_PROXIES           = "proxies"
-ELEMENT_READ_ONLY_PROXIES = "read-only-proxies"
 
 ATTRIBUTE_REALM           = "realm"
 ATTRIBUTE_REPEAT          = "repeat"
 ATTRIBUTE_RECORDTYPE      = "type"
 
+VALUE_TRUE                = "true"
+VALUE_FALSE               = "false"
+
 RECORD_TYPES = {
     ELEMENT_USER     : DirectoryService.recordType_users,
     ELEMENT_GROUP    : DirectoryService.recordType_groups,
@@ -69,7 +67,8 @@
     def __repr__(self):
         return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
 
-    def __init__(self, xmlFile):
+    def __init__(self, xmlFile, externalUpdate=True):
+
         if type(xmlFile) is str:
             xmlFile = FilePath(xmlFile)
 
@@ -88,7 +87,7 @@
         # Verify that top-level element is correct
         accounts_node = doc._get_documentElement()
         if accounts_node._get_localName() != ELEMENT_ACCOUNTS:
-            self.log("Ignoring file %r because it is not a repository builder file" % (self.xmlFile,))
+            log.error("Ignoring file %r because it is not a repository builder file" % (self.xmlFile,))
             return
         self._parseXML(accounts_node)
         
@@ -103,23 +102,10 @@
         def updateMembership(group):
             # Update group membership
             for recordType, shortName in group.members:
-                item = self.items[recordType].get(shortName, None)
+                item = self.items[recordType].get(shortName)
                 if item is not None:
                     item.groups.add(group.shortName)
 
-        def updateProxyFor(proxier):
-            # Update proxy membership
-            for recordType, shortName in proxier.proxies:
-                item = self.items[recordType].get(shortName, None)
-                if item is not None:
-                    item.proxyFor.add((proxier.recordType, proxier.shortName))
-
-            # Update read-only proxy membership
-            for recordType, shortName in proxier.readOnlyProxies:
-                item = self.items[recordType].get(shortName, None)
-                if item is not None:
-                    item.readOnlyProxyFor.add((proxier.recordType, proxier.shortName))
-
         for child in node._get_childNodes():
             child_name = child._get_localName()
             if child_name is None:
@@ -148,7 +134,6 @@
         for records in self.items.itervalues():
             for principal in records.itervalues():
                 updateMembership(principal)
-                updateProxyFor(principal)
                 
 class XMLAccountRecord (object):
     """
@@ -163,15 +148,9 @@
         self.guid = None
         self.password = None
         self.name = None
+        self.emailAddresses = set()
         self.members = set()
         self.groups = set()
-        self.calendarUserAddresses = set()
-        self.autoSchedule = False
-        self.enabledForCalendaring = True
-        self.proxies = set()
-        self.proxyFor = set()
-        self.readOnlyProxies = set()
-        self.readOnlyProxyFor = set()
 
     def repeat(self, ctr):
         """
@@ -195,24 +174,20 @@
             name = self.name % ctr
         else:
             name = self.name
-        calendarUserAddresses = set()
-        for cuaddr in self.calendarUserAddresses:
-            if cuaddr.find("%") != -1:
-                calendarUserAddresses.add(cuaddr % ctr)
+        emailAddresses = set()
+        for emailAddr in self.emailAddresses:
+            if emailAddr.find("%") != -1:
+                emailAddresses.add(emailAddr % ctr)
             else:
-                calendarUserAddresses.add(cuaddr)
+                emailAddresses.add(emailAddr)
         
         result = XMLAccountRecord(self.recordType)
         result.shortName = shortName
         result.guid = guid
         result.password = password
         result.name = name
+        result.emailAddresses = emailAddresses
         result.members = self.members
-        result.calendarUserAddresses = calendarUserAddresses
-        result.autoSchedule = self.autoSchedule
-        result.enabledForCalendaring = self.enabledForCalendaring
-        result.proxies = self.proxies
-        result.readOnlyProxies = self.readOnlyProxies
         return result
 
     def parseXML(self, node):
@@ -225,43 +200,20 @@
                     self.shortName = child.firstChild.data.encode("utf-8")
             elif child_name == ELEMENT_GUID:
                 if child.firstChild is not None:
-                    guid = child.firstChild.data.encode("utf-8")
-                    try:
-                        UUID(guid)
-                    except:
-                        log.error("Invalid GUID in accounts XML: %r" % (guid,))
-                    self.guid = guid
+                    self.guid = child.firstChild.data.encode("utf-8")
+                    if len(self.guid) < 4:
+                        self.guid += "?" * (4 - len(self.guid))
             elif child_name == ELEMENT_PASSWORD:
                 if child.firstChild is not None:
                     self.password = child.firstChild.data.encode("utf-8")
             elif child_name == ELEMENT_NAME:
                 if child.firstChild is not None:
                     self.name = child.firstChild.data.encode("utf-8")
+            elif child_name == ELEMENT_EMAIL_ADDRESS:
+                if child.firstChild is not None:
+                    self.emailAddresses.add(child.firstChild.data.encode("utf-8").lower())
             elif child_name == ELEMENT_MEMBERS:
                 self._parseMembers(child, self.members)
-            elif child_name == ELEMENT_CUADDR:
-                if child.firstChild is not None:
-                    self.calendarUserAddresses.add(child.firstChild.data.encode("utf-8").lower())
-            elif child_name == ELEMENT_AUTOSCHEDULE:
-                # Only Resources & Locations
-                if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
-                    raise ValueError("<auto-schedule> element only allowed for Resources and Locations: %s" % (child_name,))
-                self.autoSchedule = True
-            elif child_name == ELEMENT_DISABLECALENDAR:
-                # Only Users or Groups
-                if self.recordType not in (DirectoryService.recordType_users, DirectoryService.recordType_groups):
-                    raise ValueError("<disable-calendar> element only allowed for Users or Groups: %s" % (child_name,))
-                self.enabledForCalendaring = False
-            elif child_name == ELEMENT_PROXIES:
-                # Only Resources & Locations
-                if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
-                    raise ValueError("<auto-schedule> element only allowed for Resources and Locations: %s" % (child_name,))
-                self._parseMembers(child, self.proxies)
-            elif child_name == ELEMENT_READ_ONLY_PROXIES:
-                # Only Resources & Locations
-                if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
-                    raise ValueError("<auto-schedule> element only allowed for Resources and Locations: %s" % (child_name,))
-                self._parseMembers(child, self.readOnlyProxies)
             else:
                 raise RuntimeError("Unknown account attribute: %s" % (child_name,))
 

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlaugmentsparser.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/xmlaugmentsparser.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlaugmentsparser.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlaugmentsparser.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,137 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from xml.etree.ElementTree import ElementTree
+from xml.parsers.expat import ExpatError
+import types
+
+"""
+XML based augment configuration file handling.
+"""
+
+__all__ = [
+    "XMLAugmentsParser",
+]
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+ELEMENT_AUGMENTS          = "augments"
+ELEMENT_RECORD            = "record"
+
+ELEMENT_GUID              = "guid"
+ELEMENT_ENABLE            = "enable"
+ELEMENT_HOSTEDAT          = "hosted-at"
+ELEMENT_ENABLECALENDAR    = "enable-calendar"
+ELEMENT_AUTOSCHEDULE      = "auto-schedule"
+
+ATTRIBUTE_REPEAT          = "repeat"
+
+VALUE_TRUE                = "true"
+VALUE_FALSE               = "false"
+
+ELEMENT_AUGMENTRECORD_MAP = {
+    ELEMENT_GUID:           "guid",
+    ELEMENT_ENABLE:         "enabled",
+    ELEMENT_HOSTEDAT:       "hostedAt",
+    ELEMENT_ENABLECALENDAR: "enabledForCalendaring",
+    ELEMENT_AUTOSCHEDULE:   "autoSchedule",
+}
+
+class XMLAugmentsParser(object):
+    """
+    XML augments configuration file parser.
+    """
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+    def __init__(self, xmlFile, items):
+
+        self.items = items
+        self.xmlFile = xmlFile
+
+        # Read in XML
+        try:
+            tree = ElementTree(file=self.xmlFile)
+        except ExpatError, e:
+            log.error("Unable to parse file '%s' because: %s" % (self.xmlFile, e,), raiseException=RuntimeError)
+
+        # Verify that top-level element is correct
+        augments_node = tree.getroot()
+        if augments_node.tag != ELEMENT_AUGMENTS:
+            log.error("Ignoring file '%s' because it is not a augments file" % (self.xmlFile,), raiseException=RuntimeError)
+
+        self._parseXML(augments_node)
+
+    def _parseXML(self, rootnode):
+        """
+        Parse the XML root node from the augments configuration document.
+        @param rootnode: the L{Element} to parse.
+        """
+        for child in rootnode.getchildren():
+            
+            if child.tag != ELEMENT_RECORD:
+                log.error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, self.xmlFile,), raiseException=RuntimeError)
+
+            repeat = int(child.get(ATTRIBUTE_REPEAT, "1"))
+
+            fields = {}
+            for node in child.getchildren():
+                
+                if node.tag in (
+                    ELEMENT_GUID,
+                    ELEMENT_HOSTEDAT,
+                ):
+                    fields[node.tag] = node.text
+                elif node.tag in (
+                    ELEMENT_ENABLE,
+                    ELEMENT_ENABLECALENDAR,
+                    ELEMENT_AUTOSCHEDULE,
+                ):
+                    fields[node.tag] = node.text == VALUE_TRUE
+                else:
+                    log.error("Invalid element '%s' in augment file: '%s'" % (node.tag, self.xmlFile,), raiseException=RuntimeError)
+                    
+            # Must have at least a guid
+            if ELEMENT_GUID not in fields:
+                log.error("Invalid record '%s' without a guid in augment file: '%s'" % (child, self.xmlFile,), raiseException=RuntimeError)
+                
+            if repeat > 1:
+                for i in xrange(1, repeat+1):
+                    self.buildRecord(fields, i)
+            else:
+                self.buildRecord(fields)
+    
+    def buildRecord(self, fields, count=None):
+        
+        from twistedcaldav.directory.augment import AugmentRecord
+
+        def expandCount(value, count):
+            
+            if type(value) in types.StringTypes:
+                return value % (count,) if count and "%" in value else value
+            elif type(value) == set:
+                return set([item % (count,) if count and "%" in item else item for item in value])
+            else:
+                return value
+        
+        actualFields = {}
+        for k,v in fields.iteritems():
+            actualFields[ELEMENT_AUGMENTRECORD_MAP[k]] = expandCount(v, count)
+
+        record = AugmentRecord(**actualFields)
+        self.items[record.guid] = record

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlfile.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/xmlfile.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
 from twisted.web2.auth.digest import DigestedCredentials
 from twisted.python.filepath import FilePath
 
+from twistedcaldav.directory import augment
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
 from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
 
@@ -61,23 +62,39 @@
 
     def listRecords(self, recordType):
         for entryShortName, xmlPrincipal in self._entriesForRecordType(recordType):
-            yield XMLDirectoryRecord(
+            record = XMLDirectoryRecord(
                 service       = self,
                 recordType    = recordType,
                 shortName     = entryShortName,
                 xmlPrincipal  = xmlPrincipal,
             )
 
+            # Look up augment information
+            # TODO: this needs to be deferred but for now we hard code the deferred result because
+            # we know it is completing immediately.
+            d = augment.AugmentService.getAugmentRecord(record.guid)
+            d.addCallback(lambda x:record.addAugmentInformation(x))
+
+            yield record
+
     def recordWithShortName(self, recordType, shortName):
         for entryShortName, xmlprincipal in self._entriesForRecordType(recordType):
             if entryShortName == shortName:
-                return XMLDirectoryRecord(
+                record = XMLDirectoryRecord(
                     service       = self,
                     recordType    = recordType,
                     shortName     = entryShortName,
                     xmlPrincipal  = xmlprincipal,
                 )
 
+                # Look up augment information
+                # TODO: this needs to be deferred but for now we hard code the deferred result because
+                # we know it is completing immediately.
+                d = augment.AugmentService.getAugmentRecord(record.guid)
+                d.addCallback(lambda x:record.addAugmentInformation(x))
+
+                return record
+
         return None
 
     def _entriesForRecordType(self, recordType):
@@ -108,18 +125,12 @@
             guid                  = xmlPrincipal.guid,
             shortName             = shortName,
             fullName              = xmlPrincipal.name,
-            calendarUserAddresses = xmlPrincipal.calendarUserAddresses,
-            autoSchedule          = xmlPrincipal.autoSchedule,
-            enabledForCalendaring = xmlPrincipal.enabledForCalendaring,
+            emailAddresses        = xmlPrincipal.emailAddresses,
         )
 
         self.password          = xmlPrincipal.password
         self._members          = xmlPrincipal.members
         self._groups           = xmlPrincipal.groups
-        self._proxies          = xmlPrincipal.proxies
-        self._proxyFor         = xmlPrincipal.proxyFor
-        self._readOnlyProxies  = xmlPrincipal.readOnlyProxies
-        self._readOnlyProxyFor = xmlPrincipal.readOnlyProxyFor
 
     def members(self):
         for recordType, shortName in self._members:
@@ -129,26 +140,11 @@
         for shortName in self._groups:
             yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
 
-    def proxies(self):
-        for recordType, shortName in self._proxies:
-            yield self.service.recordWithShortName(recordType, shortName)
-
-    def proxyFor(self, read_write=True):
-        for recordType, shortName in self._proxyFor:
-            yield self.service.recordWithShortName(recordType, shortName)
-
-    def readOnlyProxies(self):
-        for recordType, shortName in self._readOnlyProxies:
-            yield self.service.recordWithShortName(recordType, shortName)
-
-    def readOnlyProxyFor(self, read_write=True):
-        for recordType, shortName in self._readOnlyProxyFor:
-            yield self.service.recordWithShortName(recordType, shortName)
-
     def verifyCredentials(self, credentials):
-        if isinstance(credentials, UsernamePassword):
-            return credentials.password == self.password
-        if isinstance(credentials, DigestedCredentials):
-            return credentials.checkPassword(self.password)
+        if self.enabled:
+            if isinstance(credentials, UsernamePassword):
+                return credentials.password == self.password
+            if isinstance(credentials, DigestedCredentials):
+                return credentials.checkPassword(self.password)
 
         return super(XMLDirectoryRecord, self).verifyCredentials(credentials)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/extensions.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/extensions.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -42,6 +42,10 @@
 from twisted.web2.static import MetaDataMixin
 from twisted.web2.dav import davxml
 from twisted.web2.dav.davxml import dav_namespace
+from twisted.web2.dav.element.base import WebDAVTextElement, WebDAVUnknownElement,\
+    twisted_dav_namespace
+from twisted.web2.dav.davxml import Error
+from twisted.web2.dav.http import ErrorResponse as SuperErrorResponse
 from twisted.web2.dav.http import StatusResponse
 from twisted.web2.dav.static import DAVFile as SuperDAVFile
 from twisted.web2.dav.resource import DAVResource as SuperDAVResource
@@ -855,3 +859,51 @@
                 for name in self.propertyStore.list()
             )
         return self._data
+
+class ErrorDescription(WebDAVTextElement):
+    """
+    The human-readable description of a failed precondition
+    """
+    namespace = twisted_dav_namespace
+    name = "error-description"
+    protected = True
+
+
+class ErrorResponse(SuperErrorResponse):
+    """
+    A L{Response} object which contains a status code and a L{davxml.Error}
+    element.
+    Renders itself as a DAV:error XML document.
+    """
+    error = None
+
+    def __init__(self, code, error, description=None):
+        """
+        @param code: a response code.
+        @param error: an L{davxml.WebDAVElement} identifying the error, or a
+            tuple C{(namespace, name)} with which to create an empty element
+            denoting the error.  (The latter is useful in the case of
+            preconditions ans postconditions, not all of which have defined
+            XML element classes.)
+        @param description: an optional string that, if present, will get
+            wrapped in a (twisted_dav_namespace, error-description) element.
+        """
+        if type(error) is tuple:
+            xml_namespace, xml_name = error
+            error = WebDAVUnknownElement()
+            error.namespace = xml_namespace
+            error.name = xml_name
+
+        if description:
+            output = Error(error, ErrorDescription(description)).toxml()
+        else:
+            output = Error(error).toxml()
+
+        Response.__init__(self, code=code, stream=output)
+
+        self.headers.setHeader("content-type", MimeType("text", "xml"))
+
+        self.error = error
+
+    def __repr__(self):
+        return "<%s %s %s>" % (self.__class__.__name__, self.code, self.error.sname())

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/ical.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/ical.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -49,6 +49,7 @@
 from twistedcaldav.dateops import compareDateTime, normalizeToUTC, timeRangesOverlap
 from twistedcaldav.instance import InstanceList
 from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
 
 log = Logger()
 
@@ -1110,6 +1111,43 @@
 
         return None
 
+    def getAttendeesByInstance(self, makeUnique=False, onlyScheduleAgentServer=False):
+        """
+        Get the attendee values for each instance. Optionally remove duplicates.
+        
+        @param makeUnique: if C{True} remove duplicate ATTENDEEs in each component
+        @type makeUnique: C{bool}
+        @param onlyScheduleAgentServer: if C{True} only return ATETNDEEs with SCHEDULE-AGENT=SERVER set
+        @type onlyScheduleAgentServer: C{bool}
+        @return: a list of tuples of (organizer value, recurrence-id)
+        """
+        
+        # Extract appropriate sub-component if this is a VCALENDAR
+        if self.name() == "VCALENDAR":
+            result = ()
+            for component in self.subcomponents():
+                if component.name() != "VTIMEZONE":
+                    result += component.getAttendeesByInstance(makeUnique, onlyScheduleAgentServer)
+            return result
+        else:
+            result = ()
+            attendees = set()
+            rid = self.getRecurrenceIDUTC()
+            for attendee in tuple(self.properties("ATTENDEE")):
+                
+                if onlyScheduleAgentServer:
+                    if "SCHEDULE-AGENT" in attendee.params():
+                        if attendee.paramValue("SCHEDULE-AGENT") != "SERVER":
+                            continue
+
+                cuaddr = attendee.value()
+                if makeUnique and cuaddr in attendees:
+                    self.removeProperty(attendee)
+                else:
+                    result += ((cuaddr, rid),)
+                    attendees.add(cuaddr)
+            return result
+
     def getAttendeeProperty(self, match):
         """
         Get the attendees matching a value. Works on either a VCALENDAR or on a component.
@@ -1118,18 +1156,10 @@
         @return: the string value of the Organizer property, or None
         """
         
-        # FIXME: we should really have a URL class and have it manage comparisons
-        # in a sensible fashion.
-        def _normalizeCUAddress(addr):
-            if addr.startswith("/") or addr.startswith("http:") or addr.startswith("https:"):
-                return addr.rstrip("/")
-            else:
-                return addr
-
         # Need to normalize http/https cu addresses
         test = set()
         for item in match:
-            test.add(_normalizeCUAddress(item))
+            test.add(normalizeCUAddr(item))
         
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
@@ -1139,7 +1169,7 @@
         else:
             # Find the primary subcomponent
             for p in self.properties("ATTENDEE"):
-                if _normalizeCUAddress(p.value()) in test:
+                if normalizeCUAddr(p.value()) in test:
                     return p
 
         return None

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/index.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/index.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/index.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -686,7 +686,7 @@
         assert resource.isCalendarCollection(), "non-calendar collection resource %s has no index." % (resource,)
         super(Index, self).__init__(resource)
 
-        if config.Memcached['ClientEnabled']:
+        if config.Memcached['Pools']['Default']['ClientEnabled']:
             self.reserver = MemcachedUIDReserver(self)
 
         else:

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/itip.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/itip.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/itip.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,953 +0,0 @@
-##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-iTIP (RFC2446) processing.
-"""
-
-#
-# This is currently used for handling auto-replies to schedule requests arriving
-# in an inbox. It is called in a delayed fashion via reactor.callLater.
-#
-# We assume that all the components/calendars we deal with have been determined
-# as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
-# types, etc.
-#
-# The logic for component matching needs a lot more work as it currently does not
-# know how to deal with overridden instances.
-#
-
-import datetime
-import md5
-import time
-
-from twisted.python.failure import Failure
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
-from twisted.web2.dav import davxml
-from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
-from twisted.web2.dav.util import joinURL
-from twisted.web2.dav.fileop import delete
-from twisted.web2.dav.resource import AccessDeniedError
-
-from twistedcaldav import caldavxml
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.log import Logger
-from twistedcaldav.ical import Property, iCalendarProductID
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
-from twistedcaldav.resource import isCalendarCollectionResource
-
-log = Logger()
-
-__version__ = "0.0"
-
-__all__ = [
-    "handleRequest",
-    "canAutoRespond",
-]
-
-class iTipException(Exception):
-    pass
-
-def handleRequest(request, principal, inbox, calendar, child):
-    """
-    Handle an iTIP response automatically.
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    @return: L{Deferred} that is a L{deferredGenerator}
-    """
-    
-    method = calendar.propertyValue("METHOD")
-    if method == "REQUEST":
-        f = processRequest
-    elif method == "ADD":
-        f = processAdd
-    elif method == "CANCEL":
-        f = processCancel
-
-    return f(request, principal, inbox, calendar, child)
-
- at inlineCallbacks
-def processRequest(request, principal, inbox, calendar, child):
-    """
-    Process a METHOD=REQUEST.
-
-    Steps:
-    
-      1. See if this updates existing ones in Inbox.
-          1. If so,
-              1. Remove existing ones in Inbox.
-              2. See if this updates existing ones in free-busy-set calendars.
-              3. Remove existing ones in those calendars.
-              4. See if this fits into a free slot:
-                  1. If not, send REPLY with failure status
-                  2. If so
-                      1. send REPLY with success
-                      2. add to f-b-s calendar
-          2. If not,
-              1. remove the one we got - its 'stale'
-          3. Delete the request from the Inbox.
-    
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    """
-    
-    log.info("Auto-processing iTIP REQUEST for: %s" % (str(principal),))
-    processed = "ignored"
-
-    # First determine whether this is a full or partial update. A full update is one containing the master
-    # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
-    # being changed.
-    
-    new_master = calendar.masterComponent()
-
-    # Next we want to try and find a match to any components on existing calendars listed as contributing
-    # to free-busy as we will need to update those with the new one.
-    calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-    
-    if new_master:
-        # So we have a full update. That means we need to delete any existing events completely and
-        # replace with the ones provided so long as the new one is newer.
-        
-        # If we have a match then we need to check whether we are updating etc
-        check_reply = False
-        if calmatch:
-            # See whether the new component is older than any existing ones and throw it away if so
-            newinfo = (None,) + getComponentSyncInfo(new_master)
-            cal = updatecal.iCalendar(calmatch)
-            old_master = cal.masterComponent()
-            if old_master:
-                info = getSyncInfo(calmatch, cal)
-            else:
-                info = None
-            if info is None or compareSyncInfo(info, newinfo) < 0:
-                # Existing resource is older and will be replaced
-                check_reply = True
-            else:
-                processed = "older"
-        else:
-            # We have a new request which we can reply to
-            check_reply = True
-            
-        if check_reply:
-            # Process the reply by determining PARTSTAT and sending the reply and booking the event.
-            doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-            
-            try:
-                if accepted:
-                    if calmatch:
-                        newchild = yield writeResource(request, calURL, updatecal, calmatch, calendar)
-                        log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
-                    else:
-                        newchild = yield writeResource(request, calURL, updatecal, None, calendar)
-                        log.info("Added new calendar component in %s." % (calURL,))
-                else:
-                    if calmatch:
-                        yield deleteResource(updatecal, calmatch)
-                        log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
-                        
-                # Send a reply if needed. 
-                if doreply:
-                    log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
-                    newchild = yield writeReply(request, principal, replycal, inbox)
-                    newInboxResource(child, newchild)
-                processed = "processed"
-            except:
-                # FIXME: bare except
-                log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-                raise iTipException()
-            
-    else:
-        # So we have a partial update. That means we have to do partial updates to instances in
-        # the existing calendar component.
-
-        # If we have a match then we need to check whether we are updating etc
-        check_reply = False
-        if calmatch:
-            # Check each component to see whether its new
-            cal = updatecal.iCalendar(calmatch)
-            old_master = cal.masterComponent()
-            processed = "older"
-            new_components = [component for component in calendar.subcomponents()]
-            for component in new_components:
-                if component.name() == "VTIMEZONE":
-                    continue
-                
-                newinfo = (None,) + getComponentSyncInfo(component)
-                old_component = findMatchingComponent(component, cal)
-                if old_component:
-                    info = (None,) + getComponentSyncInfo(old_component)
-                elif old_master:
-                    info = (None,) + getComponentSyncInfo(old_master)
-                else:
-                    info = None
-                    
-                if info is None or compareSyncInfo(info, newinfo) < 0:
-                    # Existing resource is older and will be replaced
-                    check_reply = True
-                    processed = "processed"
-                else:
-                    calendar.removeComponent(component)
-        else:
-            # We have a new request which we can reply to
-            check_reply = True
-
-        if check_reply:
-            # Process the reply by determining PARTSTAT and sending the reply and booking the event.
-            doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-            
-            try:
-                if calmatch:
-                    # Merge the new instances with the old ones
-                    mergeComponents(calendar, cal)
-                    newchild = yield writeResource(request, calURL, updatecal, calmatch, cal)
-                    log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
-                else:
-                    if accepted:
-                        newchild = yield writeResource(request, calURL, updatecal, None, calendar)
-                        log.info("Added new calendar component in %s." % (calURL,))
-                        
-                # Do reply if needed. 
-                if doreply:
-                    log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
-                    newchild = yield writeReply(request, principal, replycal, inbox)
-                    newInboxResource(child, newchild)
-                    
-                processed = "processed"
-            except:
-                # FIXME: bare except
-                log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-                raise iTipException()
-
-    # Remove the now processed incoming request.
-    try:
-        yield deleteResource(inbox, child.fp.basename())
-        log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
-                   child.fp.basename(),
-                   {
-                     "processed": "processed",
-                     "older"    : "ignored: older",
-                     "ignored"  : "ignored: no match"
-                   }[processed]
-                ))
-    except:
-        # FIXME: bare except
-        log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-        raise iTipException()
-
-def processAdd(request, principal, inbox, calendar, child):
-    """
-    Process a METHOD=ADD.
-
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    """
-    log.info("Auto-processing iTIP ADD for: %s" % (str(principal),))
-    raise NotImplementedError()
-
- at inlineCallbacks
-def processCancel(request, principal, inbox, calendar, child):
-    """
-    Process a METHOD=CANCEL.
-
-    Policy find all components that match UID, SEQ and R-ID and remove them.
-
-    Steps:
-    
-      1. See if this updates existing ones in Inbox.
-      2. Remove existing ones in Inbox.
-      3. See if this updates existing ones in free-busy-set calendars.
-      4. Remove existing ones in those calendars.
-      5. Remove the incoming request.
-
-    NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
-    instance may be cancelled. What we need to do for this is:
-    
-      1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
-         delete the old one.
-      2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
-         leave the new and existing ones in the Inbox.
-      3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
-         remove the overridden component from the f-b-s resource.
-      4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
-    
-    TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
-    
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    """
-    
-    log.info("Auto-processing iTIP CANCEL for: %s" % (str(principal),))
-    processed = "ignored"
-
-    # Get all component info for this iTIP message
-    newinfo = getSyncInfo(child.fp.basename(), calendar)
-    info = getAllInfo(inbox, calendar, child)
-
-    # First see if we have a recurrence id which will force extra work
-    has_rid = False
-    if newinfo[4] is not None:
-        has_rid = True
-    else:
-        for i in info:
-            if i[4] is not None:
-                has_rid = True
-                break
-            
-    if not has_rid:
-        # Compare the new one with each existing one.
-        delete_child = yield processOthersInInbox(info, newinfo, inbox, child)
-        if delete_child:
-            return
-
-        # Next we want to try and find a match to any components on existing calendars listed as contributing
-        # to free-busy as we will need to update those with the new one.
-        calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-        
-        # If we have a match then we need to check whether we are updating etc
-        if calmatch:
-            # See whether the current component is older than any existing ones and throw it away if so
-            cal = updatecal.iCalendar(calmatch)
-            info = getSyncInfo(calmatch, cal)
-            if compareSyncInfo(info, newinfo) < 0:
-                # Delete existing resource which has been cancelled
-                try:
-                    yield deleteResource(updatecal, calmatch)
-                    log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
-                except:
-                    # FIXME: bare except
-                    log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-                    raise iTipException()
-                processed = "processed"
-            else:
-                processed = "older"
-        else:
-            # Nothing to do except delete the inbox item as we have nothing to cancel.
-            processed = "ignored"
-    else:
-        # Try and find a match to any components on existing calendars listed as contributing
-        # to free-busy as we will need to update those with the new one.
-        calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-        
-        # If we have a match then we need to check whether we are updating etc
-        if calmatch:
-            # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
-            # So we need to iterate over each iTIP component.
-
-            # Get the existing calendar object
-            existing_calendar = updatecal.iCalendar(calmatch)
-            existing_master = existing_calendar.masterComponent()
-            exdates = []
-
-            for component in calendar.subcomponents():
-                if component.name() == "VTIMEZONE":
-                    continue
-            
-                # Find matching component in existing calendar
-                old_component = findMatchingComponent(component, existing_calendar)
-                
-                if old_component:
-                    # We are cancelling an overridden component, so we need to check the
-                    # SEQUENCE/DTSAMP with the master.
-                    if compareComponents(old_component, component) < 0:
-                        # Exclude the cancelled instance
-                        exdates.append(component.getRecurrenceIDUTC())
-                        
-                        # Remove the existing component.
-                        existing_calendar.removeComponent(old_component)
-                elif existing_master:
-                    # We are trying to CANCEL a non-overridden instance, so we need to
-                    # check SEQUENCE/DTSTAMP with the master.
-                    if compareComponents(existing_master, component) < 0:
-                        # Exclude the cancelled instance
-                        exdates.append(component.getRecurrenceIDUTC())
-
-            # If we have any EXDATEs lets add them to the existing calendar object and write
-            # it back.
-            if exdates:
-                if existing_master:
-                    existing_master.addProperty(Property("EXDATE", exdates))
-
-                # See if there are still components in the calendar - we might have deleted the last overridden instance
-                # in which case the calendar object is empty (except for VTIMEZONEs).
-                if existing_calendar.mainType() is None:
-                    # Delete the now empty calendar object
-                    yield deleteResource(updatecal, calmatch)
-                    log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
-                else:
-                    # Update the existing calendar object
-                    newchild = yield writeResource(request, calURL, updatecal, calmatch, existing_calendar)
-                    log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
-                processed = "processed"
-            else:
-                processed = "older"
-        else:
-            # Nothing to do except delete the inbox item as we have nothing to cancel.
-            processed = "ignored"
-
-    # Remove the now processed incoming request.
-    try:
-        yield deleteResource(inbox, child.fp.basename())
-        log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
-                  child.fp.basename(),
-                  {
-                    "processed": "processed",
-                    "older"    : "ignored: older",
-                    "ignored"  : "ignored: no match"
-                  }[processed]
-                ))
-    except:
-        # FIXME: bare except
-        log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-        raise iTipException()
-
- at inlineCallbacks
-def checkForReply(request, principal, calendar):
-    """
-    Check whether a reply to the given iTIP message is needed. We will not process a reply
-    if RSVP=FALSE. A reply will either be positive (accepted
-    invitation) or negative (denied invitation). In addition we will modify calendar to reflect
-    any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
-    
-    BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
-    At the moment we will treat a failure on one instances as a DECLINE of the entire set.
-
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @return: C{True} if a reply is needed, C{False} otherwise.
-    """
-    
-    # We need to figure out whether the specified component will clash with any others in the f-b-set calendars
-    accepted = True
-        
-    # First expand current one to get instances (only go 1 year into the future)
-    default_future_expansion_duration = datetime.timedelta(days=356*1)
-    expand_max = datetime.date.today() + default_future_expansion_duration
-    instances = calendar.expandTimeRanges(expand_max)
-    
-    # Extract UID from primary component as we want to ignore this one if we match it
-    # in any calendars.
-    comp = calendar.mainComponent(allow_multiple=True)
-    uid = comp.propertyValue("UID")
-
-    # Now compare each instance time-range with the index and see if there is an overlap
-    fbset = yield principal.calendarFreeBusyURIs(request)
-
-    for calURL in fbset:
-        testcal = yield request.locateResource(calURL)
-        
-        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-        fbinfo = ([], [], [])
-        
-        # Now do search for overlapping time-range
-        for instance in instances.instances.itervalues():
-            try:
-                tr = caldavxml.TimeRange(start="20000101", end="20000101")
-                tr.start = instance.start
-                tr.end = instance.end
-                yield report_common.generateFreeBusyInfo(request, testcal, fbinfo, tr, 0, uid)
-                
-                # If any fbinfo entries exist we have an overlap
-                if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
-                    accepted = False
-                    break
-            except NumberOfMatchesWithinLimits:
-                accepted = False
-                log.info("Exceeded number of matches whilst trying to find free-time.")
-                break
-            
-        if not accepted:
-            break
-     
-    # Extract the ATTENDEE property matching current recipient from the calendar data
-    cuas = principal.calendarUserAddresses()
-    attendeeProps = calendar.getAttendeeProperties(cuas)
-    if not attendeeProps:
-        returnValue((False, None, accepted))
-
-    # Look for specific parameters
-    rsvp = True
-    for attendeeProp in attendeeProps:
-        if "RSVP" in attendeeProp.params():
-            if attendeeProp.params()["RSVP"][0] == "FALSE":
-                rsvp = False
-    
-            # Now modify the original component
-            del attendeeProp.params()["RSVP"]
-
-    if accepted:
-        partstat = "ACCEPTED"
-    else:
-        partstat = "DECLINED"
-    for attendeeProp in attendeeProps:
-        if "PARTSTAT" in attendeeProp.params():
-            attendeeProp.params()["PARTSTAT"][0] = partstat
-        else:
-            attendeeProp.params()["PARTSTAT"] = [partstat]
-    
-    # Now create a new calendar object for the reply
-    
-    # First get useful props from the original
-    replycal = calendar.duplicate()
-    
-    # Change METHOD
-    replycal.getProperty("METHOD").setValue("REPLY")
-    
-    # Change PRODID to this server
-    replycal.getProperty("PRODID").setValue(iCalendarProductID)
-    
-    # Add REQUEST-STATUS
-    for component in replycal.subcomponents():
-        if accepted:
-            component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
-        else:
-            component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
-
-    # Remove all attendees other than ourselves
-    for component in replycal.subcomponents():
-        if component.name() == "VTIMEZONE":
-            continue
-        attendeeProp = component.getAttendeeProperty(cuas)
-        attendees = tuple(component.properties("ATTENDEE"))
-        for attendee in attendees:
-            if attendeeProp is None or (attendee.value() != attendeeProp.value()):
-                component.removeProperty(attendee)
-
-    returnValue((rsvp, replycal, accepted))
-
- at inlineCallbacks
-def writeReply(request, principal, replycal, ainbox):
-    """
-    Write an iTIP message reply into the specified Inbox.
-    
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param replycal: the L{Component} for the iTIP message reply.
-    @param ainbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    """
-    
-    # Get the Inbox of the ORGANIZER
-    organizer = replycal.getOrganizer()
-    assert organizer is not None
-    organizerPrincipal = ainbox.principalForCalendarUserAddress(organizer)
-    assert organizerPrincipal is not None
-    inboxURL = organizerPrincipal.scheduleInboxURL()
-    assert inboxURL
-    
-    # Determine whether current principal has CALDAV:schedule right on that Inbox
-    inbox = yield request.locateResource(inboxURL)
-
-    try:
-        yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL())))
-    except AccessDeniedError:
-        log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
-        returnValue(None)
-    
-    # Now deposit the new calendar into the inbox
-    result = yield writeResource(request, inboxURL, inbox, None, replycal)
-
-    if accountingEnabled("iTIP", organizerPrincipal):
-        emitAccounting(
-            "iTIP", organizerPrincipal,
-            "Originator: %s\nRecipients: %s\n\n%s"
-            % (principal.principalURL(), str(organizer), str(replycal))
-        )
-
-    returnValue(result)
-
- at inlineCallbacks
-def writeResource(request, collURL, collection, name, calendar):
-    """
-    Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
-    resource or by creating a new one.
-    
-    @param request: the L{IRequest} for the current request.
-    @param collURL: the C{str} containing the URL of the calendar collection.
-    @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
-    @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
-    @param calendar: the L{Component} calendar to write.
-    @return: C{tuple} of L{Deferred}, L{CalDAVFile}
-    """
-    
-    # Create a new name if one was not provided
-    if name is None:
-        name =  md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
-
-    # Get a resource for the new item
-    newchildURL = joinURL(collURL, name)
-    newchild = yield request.locateResource(newchildURL)
-    
-    # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
-    # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
-    # NB Only do this when writing to something other than an Inbox or Outbox
-    itipper = True
-    if collection.isCalendarCollection():
-        method = calendar.getProperty("METHOD")
-        if method:
-            calendar.removeProperty(method)
-        itipper = False
-    
-    # Now write it to the resource
-    try:
-        yield storeCalendarObjectResource(
-            request=request,
-            sourcecal = False,
-            destination = newchild,
-            destination_uri = newchildURL,
-            calendardata = str(calendar),
-            destinationparent = collection,
-            destinationcal = True,
-            isiTIP = itipper
-        )
-    except:
-        # FIXME: bare except
-        return
-    
-    returnValue(newchild)
-
-def newInboxResource(child, newchild):
-    """
-    Copy recipient and organizer properties from one iTIP resource, to another,
-    switching them as appropriate for a reply, and also set the state.
-    
-    @param child: the L{CalDAVFile} for the original iTIP message.
-    @param newchild: the L{CalDAVFile} for the iTIP message reply.
-    """
-    # Make previous Recipient the new Originator
-    if child.hasDeadProperty(caldavxml.Recipient):
-        recip = child.readDeadProperty(caldavxml.Recipient)
-        if recip.children:
-            # Store CALDAV:originator property
-            newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
-    
-    # Make previous Originator the new Recipient
-    if child.hasDeadProperty(caldavxml.Originator):
-        orig = child.readDeadProperty(caldavxml.Originator)
-        if orig.children:
-            # Store CALDAV:originator property
-            newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
-  
-def deleteResource(collection, name):
-    """
-    Delete the calendar resource in the specified calendar.
-    
-    @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
-    @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
-    @return: L{Deferred}
-    """
-    
-    delchild = collection.getChild(name)
-    index = collection.index()
-    index.deleteResource(delchild.fp.basename())
-    
-    def _deletedResourced(result):
-        # Change CTag on the parent calendar collection
-        return collection.updateCTag().addCallback(lambda _: result)
-
-    d = maybeDeferred(delete, "", delchild.fp, "0")
-    d.addCallback(_deletedResourced)
-    return d
-
-def canAutoRespond(calendar):
-    """
-    Check whether the METHOD of this iTIP calendar object is one we can process. Also,
-    we will only handle VEVENTs right now.
-
-    @param calendar: L{Component} for calendar to examine.
-    @return: C{True} if we can auto-respond, C{False} if not.
-    """
-
-    try:
-        method = calendar.propertyValue("METHOD")
-        if method not in ("REQUEST", "ADD", "CANCEL"):
-            return False
-        if calendar.mainType() not in ("VEVENT"):
-            return False
-    except ValueError:
-        return False
-    
-    return True
-
- at inlineCallbacks
-def processOthersInInbox(info, newinfo, inbox, child):
-    # Compare the new one with each existing one.
-    delete_child = False
-    for i in info:
-        # For any that are older, delete them.
-        if compareSyncInfo(i, newinfo) < 0:
-            try:
-                yield deleteResource(inbox, i[0])
-                log.info("Deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
-            except:
-                # FIXME: bare except
-                log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-                raise iTipException()
-        else:
-            # For any that are newer or the same, mark the new one to be deleted.
-            delete_child = True
-
-    # Delete the new one if so marked.
-    if delete_child:
-        try:
-            yield deleteResource(inbox, child.fp.basename())
-            log.info("Deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
-        except:
-            # FIXME: bare except
-            log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-            raise iTipException()
-    
-    returnValue(delete_child)
-
- at inlineCallbacks
-def findCalendarMatch(request, principal, calendar):
-    # Try and find a match to any components on existing calendars listed as contributing
-    # to free-busy as we will need to update those with the new one.
-    
-    # Find the current recipients calendar-free-busy-set
-    fbset = yield principal.calendarFreeBusyURIs(request)
-
-    # Find the first calendar in the list with a component matching the one we are processing
-    calmatch = None
-    updatecal = None
-    calURL = None
-    for calURL in fbset:
-        updatecal = yield request.locateResource(calURL)
-        if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
-            # We will ignore missing calendars. If the recipient has failed to
-            # properly manage the free busy set that should not prevent us from working.
-            continue
-        calmatch = matchComponentInCalendar(updatecal, calendar)
-        if calmatch:
-            log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
-            break
-    
-    if calmatch is None and len(fbset):
-        calURL = fbset[0]
-        updatecal = yield request.locateResource(calURL)
-
-    returnValue((calmatch, updatecal, calURL))
-
-def matchComponentInCalendar(collection, calendar):
-    """
-    See if the component in the provided iTIP calendar object matches any in the specified calendar
-    collection.
-    
-    @param collection: L{CalDAVFile} for the calendar collection to examine.
-    @param calendar: L{Component} for calendar to examine.
-    @return: C{list} of resource names found.
-    """
-
-    try:
-        # Extract UID from primary component (note we allow multiple components to be present
-        # because CANCEL requests can have multiple components).
-        comp = calendar.mainComponent(allow_multiple=True)
-        uid = comp.propertyValue("UID")
-        
-        # Now use calendar collection index to find all other resources with the same UID
-        index = collection.index()
-        result = index.resourceNamesForUID(uid)
-        
-        # There can be only one
-        if len(result) > 0: 
-            return result[0]
-        else:
-            return None
-    except ValueError:
-        return None
-
-def findMatchingComponent(component, calendar):
-    """
-    See if any overridden component in the provided iTIP calendar object matches the specified component.
-    
-    @param component: the component to try and match.
-    @type component: L{Component}
-    @param calendar: the calendar to find a match in.
-    @type calendar: L{Component}
-    @return: L{Component} for matching component,
-        or C{None} if not found.
-    """
-
-    # Extract RECURRENCE-ID value from component
-    rid = component.getRecurrenceIDUTC()
-    
-    # Return the one that matches in the calendar
-    return calendar.overriddenComponent(rid)
-
-def mergeComponents(newcal, oldcal):
-    """
-    Merge the overridden instance components in newcal into oldcal replacing any
-    matching components there.
-
-    @param newcal: the new overridden instances to use.
-    @type newcal: L{Component}
-    @param oldcal: the component to merge into.
-    @type oldcal: L{Component}
-    """
-    
-    # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
-    # use a TZID that is already specified in the old component set.
-
-    # We will update the SEQUENCE on the master to the highest value of the current one on the master
-    # or the ones in the components we are changing.
-
-    for component in newcal.subcomponents():
-        if component.name() == "VTIMEZONE":
-            continue
-        
-        rid = component.getRecurrenceIDUTC()
-        old_component = oldcal.overriddenComponent(rid)
-        if old_component:
-            oldcal.removeComponent(old_component)
-        oldcal.addComponent(component)
-
-def getAllInfo(collection, calendar, ignore):
-    """
-    Find each component in the calendar collection that has a matching UID with
-    the supplied component, and get useful synchronization details from it, ignoring
-    the one with the supplied resource name.
-
-    @param collection: the L{CalDAVFile} for the calendar collection.
-    @param calendar: the L{Component} for the component being compared with.
-    @param ignore: the C{str} containing the name of a resource to ignore,
-        or C{None} if none to ignore.
-    @return: C{list} of synchronization information for each resource found.
-    """
-    names = []
-    try:
-        # Extract UID from primary component (note we allow multiple components to be present
-        # because CANCEL requests can have multiple components).
-        comp = calendar.mainComponent(allow_multiple=True)
-        uid = comp.propertyValue("UID")
-        
-        # Now use calendar collection index to find all other resources with the same UID
-        index = collection.index()
-        names = index.resourceNamesForUID(uid)
-        
-        # Remove the one we want to ignore
-        if ignore is not None:
-            names = [name for name in names if name != ignore.fp.basename()]
-    except ValueError:
-        return []
-    
-    # Now get info for each name
-    result = []
-    for name in names:
-        cal = collection.iCalendar(name)
-        result.append(getSyncInfo(name, cal))
-
-    return result
-    
-def getSyncInfo(name, calendar):
-    """
-    Get property value details needed to synchronize iTIP components.
-    
-    @param calendar: L{Component} for calendar to check.
-    @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
-    """
-    try:
-        # Extract components from primary component (note we allow multiple components to be present
-        # because CANCEL requests can have multiple components).
-        comp = calendar.mainComponent(allow_multiple=True)
-        uid, seq, dtstamp, rid = getComponentSyncInfo(comp)
-        
-    except ValueError:
-        return (name, None, None, None, None)
-    
-    return (name, uid, seq, dtstamp, rid)
-
-def getComponentSyncInfo(component):
-    """
-    Get property value details needed to synchronize iTIP components.
-    
-    @param component: L{Component} to check.
-    @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
-    """
-    try:
-        # Extract items from component
-        uid = component.propertyValue("UID")
-        seq = component.propertyValue("SEQUENCE")
-        if seq:
-            seq = int(seq)
-        dtstamp = component.propertyValue("DTSTAMP")
-        rid = component.propertyValue("RECURRENCE-ID")
-        
-    except ValueError:
-        return (None, None, None, None)
-    
-    return (uid, seq, dtstamp, rid)
-
-def compareComponents(component1, component2):
-    """
-    Compare synchronization information for two components to see if they match according to iTIP.
-
-    @param component1: first component to check.
-    @type component1: L{Component}
-    @param component2: second component to check.
-    @type component2: L{Component}
-    
-    @return: 0, 1, -1 as per compareSyncInfo.
-    """
-    info1 = (None,) + getComponentSyncInfo(component1)
-    info2 = (None,) + getComponentSyncInfo(component2)
-    return compareSyncInfo(info1, info2)
-
-def compareSyncInfo(info1, info2):
-    """
-    Compare two synchronization information records.
-    
-    @param info1: a C{tuple} as returned by L{getSyncInfo}.
-    @param info2: a C{tuple} as returned by L{getSyncInfo}.
-    @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
-    """
-    # UIDs MUST match
-    assert info1[1] == info2[1]
-    
-    # Look for sequence
-    if (info1[2] is not None) and (info2[2] is not None):
-        if info1[2] > info2[2]:
-            return 1
-        if info1[2] < info2[2]:
-            return -1
-    elif (info1[2] is not None) and (info2[2] is None):
-        return 1
-    elif (info1[2] is None) and (info2[2] is not None):
-        return -1
-
-    # Look for DTSTAMP
-    if (info1[3] is not None) and (info2[3] is not None):
-        if info1[3] > info2[3]:
-            return 1
-        if info1[3] < info2[3]:
-            return -1
-    elif (info1[3] is not None) and (info2[3] is None):
-        return 1
-    elif (info1[3] is None) and (info2[3] is not None):
-        return -1
-
-    return 0

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/log.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/log.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/log.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -55,10 +55,14 @@
     "LoggingMixIn",
 ]
 
+from StringIO import StringIO
+from twisted.internet.defer import succeed
+from twisted.python import log
+from twisted.web2 import responsecode
+from twisted.web2.dav.util import allDataFromStream
+from twisted.web2.stream import MemoryStream
 import inspect
 
-from twisted.python import log
-
 logLevels = (
     "debug",
     "info",
@@ -193,6 +197,86 @@
         """
         return cmpLogLevels(self.level(), level) <= 0
 
+    # FIXME: This doesn't belong here
+    def logRequest(self, level, message, request, **kwargs):
+        """
+        Log an HTTP request.
+        """
+
+        assert level in logLevels
+
+        if self.willLogAtLevel(level):
+            iostr = StringIO()
+            iostr.write("%s\n" % (message,))
+            if hasattr(request, "clientproto"):
+                protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
+            else:
+                protocol = "HTTP/1.1"
+            iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
+            for name, valuelist in request.headers.getAllRawHeaders():
+                for value in valuelist:
+                    # Do not log authorization details
+                    if name not in ("Authorization",):
+                        iostr.write("%s: %s\n" % (name, value))
+                    else:
+                        iostr.write("%s: xxxxxxxxx\n" % (name,))
+            iostr.write("\n")
+            
+            # We need to play a trick with the request stream as we can only read it once. So we
+            # read it, store the value in a MemoryStream, and replace the request's stream with that,
+            # so the data can be read again.
+            def _gotData(data):
+                iostr.write(data)
+                
+                request.stream = MemoryStream(data if data is not None else "")
+                request.stream.doStartReading = None
+            
+                self.emit(level, iostr.getvalue(), **kwargs)
+
+            d = allDataFromStream(request.stream)
+            d.addCallback(_gotData)
+            return d
+        
+        else:
+            return succeed(None)
+    
+    # FIXME: This doesn't belong here
+    def logResponse(self, level, message, response, **kwargs):
+        """
+        Log an HTTP request.
+        """
+
+        assert level in logLevels
+
+        if self.willLogAtLevel(level):
+            iostr = StringIO()
+            iostr.write("%s\n" % (message,))
+            code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
+            iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
+            for name, valuelist in response.headers.getAllRawHeaders():
+                for value in valuelist:
+                    # Do not log authorization details
+                    if name not in ("WWW-Authenticate",):
+                        iostr.write("%s: %s\n" % (name, value))
+                    else:
+                        iostr.write("%s: xxxxxxxxx\n" % (name,))
+            iostr.write("\n")
+            
+            # We need to play a trick with the response stream to ensure we don't mess it up. So we
+            # read it, store the value in a MemoryStream, and replace the response's stream with that,
+            # so the data can be read again.
+            def _gotData(data):
+                iostr.write(data)
+                
+                response.stream = MemoryStream(data if data is not None else "")
+                response.stream.doStartReading = None
+            
+                self.emit(level, iostr.getvalue(), **kwargs)
+                
+            d = allDataFromStream(response.stream)
+            d.addCallback(_gotData)
+            return d
+
 class LoggingMixIn (object):
     """
     Mix-in class for logging methods.
@@ -224,8 +308,10 @@
     #
     # Attach methods to Logger
     #
-    def log_emit(self, message, level=level, **kwargs):
+    def log_emit(self, message, level=level, raiseException=None, **kwargs):
         self.emit(level, message, **kwargs)
+        if raiseException:
+            raise raiseException(message)
 
     log_emit.__doc__ = doc
 
@@ -234,8 +320,10 @@
     #
     # Attach methods to LoggingMixIn
     #
-    def log_emit(self, message, level=level, **kwargs):
+    def log_emit(self, message, level=level, raiseException=None, **kwargs):
         self.logger.emit(level, message, **kwargs)
+        if raiseException:
+            raise raiseException(message)
 
     log_emit.__doc__ = doc
 

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcachepool.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcachepool.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcachepool.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -15,6 +15,7 @@
 ##
 
 from twisted.python.failure import Failure
+from twisted.internet.address import IPv4Address
 from twisted.internet.defer import Deferred, fail
 from twisted.internet.error import ConnectionLost, ConnectionDone
 from twisted.internet.protocol import ClientFactory
@@ -338,22 +339,46 @@
     @ivar _cachePool: A saved cachePool.
     """
     _cachePool = None
+    _cachePoolHandle = "Default"
 
     def getCachePool(self):
         if self._cachePool is None:
-            return defaultCachePool()
+            return defaultCachePool(self._cachePoolHandle)
 
         return self._cachePool
 
 
 
-_memCachePool = None
+_memCachePools = {}         # Maps a name to a pool object
+_memCachePoolHandler = {}   # Maps a handler id to a named pool
 
-def installPool(serverAddress, maxClients=5, reactor=None):
-    global _memCachePool
-    _memCachePool = MemCachePool(serverAddress,
+def installPools(pools, maxClients=5, reactor=None):
+    
+    for name, pool in pools.items():
+        if pool["ClientEnabled"]:
+            _installPool(
+                name,
+                pool["HandleCacheTypes"],
+                IPv4Address(
+                    "TCP",
+                    pool["BindAddress"],
+                    pool["Port"],
+                ),
+                maxClients,
+                reactor,
+            )
+
+def _installPool(name, handleTypes, serverAddress, maxClients=5, reactor=None):
+
+    pool = MemCachePool(serverAddress,
                                  maxClients=maxClients,
                                  reactor=None)
+    _memCachePools[name] = pool
 
-def defaultCachePool():
-    return _memCachePool
+    for handle in handleTypes:
+        _memCachePoolHandler[handle] = pool
+
+def defaultCachePool(name):
+    if name not in _memCachePoolHandler:
+        name = "Default"
+    return _memCachePoolHandler[name]

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcacheprops.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcacheprops.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcacheprops.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -60,7 +60,9 @@
 
             log.info("Instantiating memcache connection for MemcachePropertyCollection")
 
-            MemcachePropertyCollection._memcacheClient = MemcacheClientFactory.getClient(["%s:%s" % (config.Memcached.BindAddress, config.Memcached.Port)],
+            MemcachePropertyCollection._memcacheClient = MemcacheClientFactory.getClient([
+                    "%s:%s" % (config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port)
+                ],
                 debug=0,
                 pickleProtocol=2,
             )

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcacher.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcacher.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/memcacher.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -82,6 +82,7 @@
         @type no_invalidation: C{bool}
         """
         self._memcacheProtocol = None
+        self._cachePoolHandle = namespace
         self._namespace = namespace
         self._pickle = pickle
         self._noInvalidation = no_invalidation
@@ -91,7 +92,7 @@
         if self._memcacheProtocol is not None:
             return self._memcacheProtocol
 
-        if config.Memcached['ClientEnabled']:
+        if config.Memcached['Pools']['Default']['ClientEnabled']:
             return self.getCachePool()
 
         elif config.ProcessType == "Single" or self._noInvalidation:

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/report_common.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/report_common.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -287,7 +287,7 @@
 fbtype_index_mapper = {'B': 0, 'T': 1, 'U': 2}
 
 def generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchtotal,
-                         excludeuid=None, organizer=None, same_calendar_user=False):
+                         excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False):
     """
     Run a free busy report on the specified calendar collection
     accumulating the free busy info for later processing.
@@ -306,7 +306,7 @@
     
     # First check the privilege on this collection
     try:
-        d = waitForDeferred(calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),)))
+        d = waitForDeferred(calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), principal=organizerPrincipal))
         yield d
         d.getResult()
     except AccessDeniedError:
@@ -370,7 +370,7 @@
         child = child.getResult()
 
         try:
-            d = waitForDeferred(child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces))
+            d = waitForDeferred(child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces, principal=organizerPrincipal))
             yield d
             d.getResult()
         except AccessDeniedError:

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/notify.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/notify.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/notify.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1295,13 +1295,10 @@
         #
         # Configure Memcached Client Pool
         #
-        if config.Memcached.ClientEnabled:
-            memcachepool.installPool(
-                IPv4Address(
-                    'TCP',
-                    config.Memcached.BindAddress,
-                    config.Memcached.Port),
-                config.Memcached.MaxClients)
+        memcachepool.installPools(
+            config.Memcached.Pools,
+            config.Memcached.MaxClients,
+        )
 
         multiService = service.MultiService()
 

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/partitions.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/partitions.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/partitions.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/partitions.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,71 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.client.pool import installPool
+from twistedcaldav.log import Logger
+from twistedcaldav.py.plistlib import readPlist
+
+"""
+Collection of classes for managing partition information for a group of servers.
+"""
+
+log = Logger()
+
+class Partitions(object):
+
+    def __init__(self):
+        
+        self.clear()
+
+    def clear(self):
+        self.partitions = {}
+        self.ownUID = ""
+        self.maxClients = 5
+
+    def readConfig(self, plistpath):
+        try:
+            dataDict = readPlist(plistpath)
+        except (IOError, OSError):                                    
+            log.error("Configuration file does not exist or is inaccessible: %s" % (self._configFileName,))
+            return
+        
+        for partition in dataDict.get("partitions", ()):
+            uid = partition.get("uid", None)
+            url = partition.get("url", None)
+            if uid and url:
+                self.partitions[uid] = url
+
+    def setSelfPartition(self, uid):
+        self.ownUID = uid
+
+    def setMaxClients(self, maxClients):
+        self.maxClients = maxClients
+
+    def getPartitionURL(self, uid):
+        # When the UID matches this server return an empty string
+        return self.partitions.get(uid, None) if uid != self.ownUID else ""
+
+    def installReverseProxies(self):
+        
+        for partition, url in self.partitions.iteritems():
+            if partition != self.ownUID:
+                installPool(
+                    partition,
+                    url,
+                    self.maxClients,
+                )
+
+partitions = Partitions()

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/root.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/root.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/root.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -71,7 +71,7 @@
 
         self.contentFilters = []
 
-        if config.Memcached['ClientEnabled']:
+        if config.Memcached['Pools']['Default']['ClientEnabled']:
             self.responseCache = MemcacheResponseCache(self.fp)
 
             CalendarHomeFile.cacheNotifierFactory = MemcacheChangeNotifier

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/schedule.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/schedule.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2008 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -21,36 +21,31 @@
 __all__ = [
     "ScheduleInboxResource",
     "ScheduleOutboxResource",
+    "IScheduleInboxResource",
 ]
 
-import md5
-import time
-
 from twisted.internet import reactor
-from twisted.internet.defer import maybeDeferred, succeed, inlineCallbacks, returnValue,\
-    Deferred
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
 from twisted.python.failure import Failure
 from twisted.web2 import responsecode
-from twisted.web2.http import HTTPError, Response
-from twisted.web2.http_headers import MimeType
 from twisted.web2.dav import davxml
 from twisted.web2.dav.http import ErrorResponse, errorForFailure, messageForFailure, statusForFailure
 from twisted.web2.dav.idav import IDAVResource
 from twisted.web2.dav.resource import AccessDeniedError
 from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError, Response
+from twisted.web2.http_headers import MimeType
 
 from twistedcaldav import caldavxml
-from twistedcaldav import itip
-from twistedcaldav.log import LoggingMixIn
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.ical import Component
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.log import LoggingMixIn
+from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.scheduling.itip import handleRequest
+from twistedcaldav.scheduling.scheduler import CalDAVScheduler, IScheduleScheduler
+    
 
 class CalendarSchedulingCollectionResource (CalDAVResource):
     """
@@ -205,7 +200,7 @@
                     
                     # Now process each one in order
                     for child, calendar in children:
-                        reactor.callLater(0.0, itip.handleRequest, *(request, principal, self, calendar, child)) #@UndefinedVariable
+                        reactor.callLater(0.0, handleRequest, *(request, principal, self, calendar, child)) #@UndefinedVariable
     
                     if (len(children)):
                         response_text = "Started auto-processing of %d iTIP messages." % (len(children,))
@@ -270,345 +265,14 @@
         # Check authentication and access controls
         yield self.authorize(request, (caldavxml.Schedule(),))
 
-        # Must be content-type text/calendar
-        contentType = request.headers.getHeader("content-type")
-        if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
-            self.log_error("MIME type %s not allowed in calendar collection" % (contentType,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-    
-        # Must have Originator header
-        originator = request.headers.getRawHeaders("originator")
-        if originator is None or (len(originator) != 1):
-            self.log_error("POST request must have Originator header")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
-        else:
-            originator = originator[0]
-    
-        # Verify that Originator is a valid calendar user (has an INBOX)
-        originatorPrincipal = self.principalForCalendarUserAddress(originator)
-        if originatorPrincipal is None:
-            self.log_error("Could not find principal for originator: %s" % (originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        # This is a local CALDAV scheduling operation.
+        scheduler = CalDAVScheduler(request, self)
 
-        inboxURL = originatorPrincipal.scheduleInboxURL()
-        if inboxURL is None:
-            self.log_error("Could not find inbox for originator: %s" % (originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-    
-        # Verify that Originator matches the authenticated user
-        if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != self.currentPrincipal(request):
-            self.log_error("Originator: %s does not match authorized user: %s" % (originator, self.currentPrincipal(request).children[0],))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        # Do the POST processing treating
+        result = (yield scheduler.doSchedulingViaPOST(use_request_headers=True))
+        returnValue(result.response())
 
-        # Get list of Recipient headers
-        rawRecipients = request.headers.getRawHeaders("recipient")
-        if rawRecipients is None or (len(rawRecipients) == 0):
-            self.log_error("POST request must have at least one Recipient header")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
 
-        # Recipient header may be comma separated list
-        recipients = []
-        for rawRecipient in rawRecipients:
-            for r in rawRecipient.split(","):
-                r = r.strip()
-                if len(r):
-                    recipients.append(r)
-
-        timeRange = TimeRange(start="20000101", end="20000102")
-        recipientsState = {"OK":0, "BAD":0}
-
-        # Parse the calendar object from the HTTP request stream
-        try:
-            calendar = yield Component.fromIStream(request.stream)
-            calendardata = str(calendar)
-        except:
-            self.log_error("Error while handling POST: %s" % (Failure(),))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- 
-        # Must be a valid calendar
-        try:
-            calendar.validCalendarForCalDAV()
-        except ValueError:
-            self.log_error("POST request calendar component is not valid: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
-        # Must have a METHOD
-        if not calendar.isValidMethod():
-            self.log_error("POST request must have valid METHOD property in calendar component: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-        
-        # Verify iTIP behaviour
-        if not calendar.isValidITIP():
-            self.log_error("POST request must have a calendar component that satisfies iTIP requirements: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
-        # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
-        if calendar.hasProperty(Component.ACCESS_PROPERTY):
-            self.log_error("X-CALENDARSERVER-ACCESS not allowed in a calendar component POST request: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
-    
-        # Verify that the ORGANIZER's cu address maps to the request.uri
-        organizer = calendar.getOrganizer()
-        if organizer is None:
-            organizerPrincipal = None
-        else:
-            organizerPrincipal = self.principalForCalendarUserAddress(organizer)
-
-        if organizerPrincipal is None:
-            self.log_error("ORGANIZER in calendar data is not valid: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
-        # Prevent spoofing of ORGANIZER with specific METHODs
-        if (
-            calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER") and
-            organizerPrincipal.record != self.parent.record
-        ):
-            self.log_error("ORGANIZER in calendar data does not match owner of Outbox: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
-        # Prevent spoofing when doing reply-like METHODs
-        if calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
-            # Verify that there is a single ATTENDEE property and that the Originator has permission
-            # to send on behalf of that ATTENDEE
-            attendees = calendar.getAttendees()
-        
-            # Must have only one
-            if len(attendees) != 1:
-                self.log_error("ATTENDEE list in calendar data is wrong: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-            
-            # Attendee's Outbox MUST be the request URI
-            attendeePrincipal = self.principalForCalendarUserAddress(attendees[0])
-            if attendeePrincipal is None or attendeePrincipal.record != self.parent.record:
-                self.log_error("ATTENDEE in calendar data does not match owner of Outbox: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-
-        # For free-busy do immediate determination of iTIP result rather than fan-out
-        self.log_debug("METHOD: %s, Component: %s" % (calendar.propertyValue("METHOD"), calendar.mainType(),))
-        if (calendar.propertyValue("METHOD") == "REQUEST") and (calendar.mainType() == "VFREEBUSY"):
-            # Extract time range from VFREEBUSY object
-            vfreebusies = [v for v in calendar.subcomponents() if v.name() == "VFREEBUSY"]
-            if len(vfreebusies) != 1:
-                self.log_error("iTIP data is not valid for a VFREEBUSY request: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-            dtstart = vfreebusies[0].getStartDateUTC()
-            dtend = vfreebusies[0].getEndDateUTC()
-            if dtstart is None or dtend is None:
-                self.log_error("VFREEBUSY start/end not valid: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-            timeRange.start = dtstart
-            timeRange.end = dtend
-
-            # Look for masked UID
-            excludeUID = calendar.getMaskUID()
-
-            # Do free busy operation
-            freebusy = True
-        else:
-            # Do regular invite (fan-out)
-            freebusy = False
-
-        #
-        # Accounting
-        #
-        # Note that we associate logging with the organizer, not the
-        # originator, which is good for looking for why something
-        # shows up in a given principal's calendars, rather than
-        # tracking the activities of a specific user.
-        #
-        if not hasattr(request, "extendedLogItems"):
-            request.extendedLogItems = {}
-        if freebusy:
-            if accountingEnabled("iTIP-VFREEBUSY", organizerPrincipal):
-                emitAccounting(
-                    "iTIP-VFREEBUSY", organizerPrincipal,
-                    "Originator: %s\nRecipients: %s\n\n%s"
-                    % (originator, ", ".join(recipients), calendardata)
-                )
-            request.extendedLogItems["freebusy"] = len(recipients)
-    
-        else:
-            if accountingEnabled("iTIP", organizerPrincipal):
-                emitAccounting(
-                    "iTIP", organizerPrincipal,
-                    "Originator: %s\nRecipients: %s\n\n%s"
-                    % (originator, ", ".join(recipients), calendardata)
-                )
-            request.extendedLogItems["itip.%s" % (calendar.propertyValue("METHOD").lower(),)] = len(recipients)
-        request.extendedLogItems["cl"] = str(len(calendardata))
-
-        # Prepare for multiple responses
-        responses = ScheduleResponseQueue("POST", responsecode.OK)
-    
-        # Loop over each recipient and do appropriate action.
-        autoresponses = []
-        for recipient in recipients:
-
-            # Yield to the reactor once through each loop if more than one attendee
-            if len(recipients) > 1:
-                d = Deferred()
-                def _timedDeferred():
-                    d.callback(True)
-                reactor.callLater(0.0, _timedDeferred)
-                yield d
-
-            # Get the principal resource for this recipient
-            principal = self.principalForCalendarUserAddress(recipient)
-
-            # Map recipient to their inbox
-            inbox = None
-            if principal is None:
-                self.log_error("No schedulable principal for calendar user address: %r" % (recipient,))
-            else:
-                inboxURL = principal.scheduleInboxURL()
-                if inboxURL:
-                    inbox = yield request.locateResource(inboxURL)
-                else:
-                    self.log_error("No schedule inbox for principal: %s" % (principal,))
-
-            if inbox is None:
-                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
-                responses.add(recipient, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
-                recipientsState["BAD"] += 1
-            
-                # Process next recipient
-                continue
-            else:
-                #
-                # Check access controls
-                #
-                try:
-                    yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(organizerPrincipal.principalURL())))
-                except AccessDeniedError:
-                    self.log_error("Could not access Inbox for recipient: %s" % (recipient,))
-                    err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permisions")))
-                    responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                    recipientsState["BAD"] += 1
-                
-                    # Process next recipient
-                    continue
-    
-                # Different behaviour for free-busy vs regular invite
-                if freebusy:
-                    # Extract the ATTENDEE property matching current recipient from the calendar data
-                    cuas = principal.calendarUserAddresses()
-                    attendeeProp = calendar.getAttendeeProperty(cuas)
-            
-                    # Find the current recipients calendar-free-busy-set
-                    fbset = yield principal.calendarFreeBusyURIs(request)
-
-                    # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-                    fbinfo = ([], [], [])
-                
-                    try:
-                        # Process the availability property from the Inbox.
-                        has_prop = yield inbox.hasProperty((calendarserver_namespace, "calendar-availability"), request)
-                        if has_prop:
-                            availability = yield inbox.readProperty((calendarserver_namespace, "calendar-availability"), request)
-                            availability = availability.calendar()
-                            report_common.processAvailabilityFreeBusy(availability, fbinfo, timeRange)
-
-                        # Check to see if the recipient is the same calendar user as the organizer.
-                        # Needed for masked UID stuff.
-                        same_calendar_user = organizerPrincipal.principalURL() == principal.principalURL()
-
-                        # Now process free-busy set calendars
-                        matchtotal = 0
-                        for calendarResourceURL in fbset:
-
-                            calendarResource = yield request.locateResource(calendarResourceURL)
-                            if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
-                                # We will ignore missing calendars. If the recipient has failed to
-                                # properly manage the free busy set that should not prevent us from working.
-                                continue
-                         
-                            matchtotal = yield report_common.generateFreeBusyInfo(
-                                request,
-                                calendarResource,
-                                fbinfo,
-                                timeRange,
-                                matchtotal,
-                                excludeuid = excludeUID,
-                                organizer = organizer,
-                                same_calendar_user = same_calendar_user
-                            )
-                    
-                        # Build VFREEBUSY iTIP reply for this recipient
-                        fbresult = report_common.buildFreeBusyResult(
-                            fbinfo,
-                            timeRange,
-                            organizer = calendar.getOrganizerProperty(),
-                            attendee = attendeeProp,
-                            uid = calendar.resourceUID(),
-                            method="REPLY"
-                        )
-
-                        responses.add(recipient, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
-                        recipientsState["OK"] += 1
-                
-                    except:
-                        self.log_error("Could not determine free busy information: %s" % (recipient,))
-                        err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-                        responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                        recipientsState["BAD"] += 1
-                
-                else:
-                    # Hash the iCalendar data for use as the last path element of the URI path
-                    name = md5.new(calendardata + str(time.time()) + inbox.fp.path).hexdigest() + ".ics"
-                
-                    # Get a resource for the new item
-                    childURL = joinURL(inboxURL, name)
-                    child = yield request.locateResource(childURL)
-            
-                    try:
-                        # Copy calendar to inbox (doing fan-out)
-                        yield maybeDeferred(
-                            storeCalendarObjectResource,
-                            request=request,
-                            sourcecal = False,
-                            destination = child,
-                            destination_uri = childURL,
-                            calendardata = calendardata,
-                            destinationparent = inbox,
-                            destinationcal = True,
-                            isiTIP = True
-                        )
-                    except: # FIXME: bare except
-                        self.log_error("Could not store data in Inbox : %s" % (inbox,))
-                        err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-                        responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                        recipientsState["BAD"] += 1
-                    else:
-                        responses.add(recipient, responsecode.OK, reqstatus="2.0;Success")
-                        recipientsState["OK"] += 1
-        
-                        # Store CALDAV:originator property
-                        child.writeDeadProperty(caldavxml.Originator(davxml.HRef(originator)))
-                    
-                        # Store CALDAV:recipient property
-                        child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient)))
-                    
-                        # Look for auto-schedule option
-                        if principal.autoSchedule():
-                            autoresponses.append((principal, inbox, child))
-
-        # Now we have to do auto-respond
-        if len(autoresponses) != 0:
-            # First check that we have a method that we can auto-respond to
-            if not itip.canAutoRespond(calendar):
-                autoresponses = []
-            else:
-                request.extendedLogItems["itip.auto"] = len(autoresponses)
-            
-        # Now do the actual auto response
-        for principal, inbox, child in autoresponses:
-            # Add delayed reactor task to handle iTIP responses
-            reactor.callLater(0.0, itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
-            #reactor.callInThread(itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
-
-        # Return with final response if we are done
-        returnValue(responses.response())
-
 class ScheduleResponseResponse (Response):
     """
     ScheduleResponse L{Response} object.
@@ -696,3 +360,79 @@
             return ScheduleResponseResponse(self.responses, self.location)
         else:
             return self.success_response
+
+class IScheduleInboxResource (CalDAVResource):
+    """
+    iSchedule Inbox resource.
+
+    Extends L{DAVResource} to provide iSchedule inbox functionality.
+    """
+
+    def __init__(self, parent):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
+
+        CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+        self.parent = parent
+
+    def defaultAccessControlList(self):
+        privs = (
+            davxml.Privilege(davxml.Read()),
+            davxml.Privilege(caldavxml.Schedule()),
+        )
+        if config.Scheduling.CalDAV.OldDraftCompatibility:
+            privs += (davxml.Privilege(caldavxml.Schedule()),)
+
+        return davxml.ACL(
+            # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
+            davxml.ACE(
+                davxml.Principal(davxml.All()),
+                davxml.Grant(*privs),
+                davxml.Protected(),
+            ),
+        )
+
+    def resourceType(self):
+        return davxml.ResourceType.ischeduleinbox
+
+    def isCollection(self):
+        return False
+
+    def isCalendarCollection(self):
+        return False
+
+    def isPseudoCalendarCollection(self):
+        return False
+
+    def render(self, request):
+        output = """<html>
+<head>
+<title>Server To Server Inbox Resource</title>
+</head>
+<body>
+<h1>Server To Server Inbox Resource.</h1>
+</body
+</html>"""
+
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
+
+    @inlineCallbacks
+    def http_POST(self, request):
+        """
+        The server-to-server POST method.
+        """
+
+        # Check authentication and access controls
+        yield self.authorize(request, (caldavxml.Schedule(),))
+
+        # This is a server-to-server scheduling operation.
+        scheduler = IScheduleScheduler(request, self)
+
+        # Do the POST processing treating this as a non-local schedule
+        result = (yield scheduler.doSchedulingViaPOST(use_request_headers=True))
+        returnValue(result.response())

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/__init__.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-CalDAV scheduling.
-"""

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/__init__.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/__init__.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+CalDAV scheduling.
+"""

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/addressmapping.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/addressmapping.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/addressmapping.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,91 +0,0 @@
-##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twistedcaldav.config import config
-from twistedcaldav.log import Logger
-from twistedcaldav.memcacher import Memcacher
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
-from twistedcaldav.scheduling.cuaddress import LocalCalendarUser,\
-    RemoteCalendarUser, InvalidCalendarUser, PartitionedCalendarUser
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
-
-__all__ = [
-    "ScheduleAddressMapper",
-    "mapper",
-]
-
-log = Logger()
-
-"""
-Handle mapping a calendar user address to a schedule delivery type.
-"""
-
-class ScheduleAddressMapper(object):
-    """
-    Class that maps a calendar user address into a delivery service type.
-    """
-    
-    def __init__(self):
-        
-        # We are going to cache mappings whilst running
-        self.cache = Memcacher("ScheduleAddressMapper", no_invalidation=True)
-
-    @inlineCallbacks
-    def getCalendarUser(self, cuaddr, principal):
-        
-        # If we have a principal always treat the user as local or partitioned
-        if principal:
-            returnValue(LocalCalendarUser(cuaddr, principal) if principal.locallyHosted() else PartitionedCalendarUser(cuaddr, principal))
-
-        # Get the type
-        cuaddr_type = (yield self.getCalendarUserServiceType(cuaddr))
-        if cuaddr_type == DeliveryService.serviceType_caldav:
-            returnValue(InvalidCalendarUser(cuaddr))
-        elif cuaddr_type == DeliveryService.serviceType_ischedule:
-            returnValue(RemoteCalendarUser(cuaddr))
-        else:
-            returnValue(InvalidCalendarUser(cuaddr))
-
-    @inlineCallbacks
-    def getCalendarUserServiceType(self, cuaddr):
-
-        # Try cache first
-        cuaddr_type = (yield self.cache.get(str(cuaddr)))
-        if cuaddr_type is None:
-            
-            serviceTypes = (ScheduleViaCalDAV,)
-            if config.Scheduling[DeliveryService.serviceType_ischedule]["Enabled"]:
-                serviceTypes += (ScheduleViaISchedule,)
-            for service in serviceTypes:
-                if service.matchCalendarUserAddress(cuaddr):
-                    yield self.cache.set(str(cuaddr), service.serviceType())
-                    returnValue(service.serviceType())
-
-        returnValue(cuaddr_type)
-
-    def isCalendarUserInMyDomain(self, cuaddr):
-
-        # Check whether it is a possible local address
-        def _gotResult(serviceType):
-            return serviceType == DeliveryService.serviceType_caldav
-            
-        d = self.getCalendarUserServiceType(cuaddr)
-        d.addCallback(_gotResult)
-        return d
-
-mapper = ScheduleAddressMapper()

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/addressmapping.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/addressmapping.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/addressmapping.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/addressmapping.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,91 @@
+##
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+from twistedcaldav.memcacher import Memcacher
+from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser,\
+    RemoteCalendarUser, InvalidCalendarUser, PartitionedCalendarUser
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
+
+__all__ = [
+    "ScheduleAddressMapper",
+    "mapper",
+]
+
+log = Logger()
+
+"""
+Handle mapping a calendar user address to a schedule delivery type.
+"""
+
+class ScheduleAddressMapper(object):
+    """
+    Class that maps a calendar user address into a delivery service type.
+    """
+    
+    def __init__(self):
+        
+        # We are going to cache mappings whilst running
+        self.cache = Memcacher("ScheduleAddressMapper", no_invalidation=True)
+
+    @inlineCallbacks
+    def getCalendarUser(self, cuaddr, principal):
+        
+        # If we have a principal always treat the user as local or partitioned
+        if principal:
+            returnValue(LocalCalendarUser(cuaddr, principal) if principal.locallyHosted() else PartitionedCalendarUser(cuaddr, principal))
+
+        # Get the type
+        cuaddr_type = (yield self.getCalendarUserServiceType(cuaddr))
+        if cuaddr_type == DeliveryService.serviceType_caldav:
+            returnValue(InvalidCalendarUser(cuaddr))
+        elif cuaddr_type == DeliveryService.serviceType_ischedule:
+            returnValue(RemoteCalendarUser(cuaddr))
+        else:
+            returnValue(InvalidCalendarUser(cuaddr))
+
+    @inlineCallbacks
+    def getCalendarUserServiceType(self, cuaddr):
+
+        # Try cache first
+        cuaddr_type = (yield self.cache.get(str(cuaddr)))
+        if cuaddr_type is None:
+            
+            serviceTypes = (ScheduleViaCalDAV,)
+            if config.Scheduling[DeliveryService.serviceType_ischedule]["Enabled"]:
+                serviceTypes += (ScheduleViaISchedule,)
+            for service in serviceTypes:
+                if service.matchCalendarUserAddress(cuaddr):
+                    yield self.cache.set(str(cuaddr), service.serviceType())
+                    returnValue(service.serviceType())
+
+        returnValue(cuaddr_type)
+
+    def isCalendarUserInMyDomain(self, cuaddr):
+
+        # Check whether it is a possible local address
+        def _gotResult(serviceType):
+            return serviceType == DeliveryService.serviceType_caldav
+            
+        d = self.getCalendarUserServiceType(cuaddr)
+        d.addCallback(_gotResult)
+        return d
+
+mapper = ScheduleAddressMapper()

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/caldav.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,267 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-from twistedcaldav.extensions import ErrorResponse
-
-import time
-
-try:
-    from hashlib import md5
-except ImportError:
-    from md5 import new as md5
-
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
-from twisted.python.failure import Failure
-from twisted.web2 import responsecode
-from twisted.web2.dav import davxml
-from twisted.web2.dav.resource import AccessDeniedError
-from twisted.web2.dav.util import joinURL
-from twisted.web2.http import HTTPError
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.log import Logger
-from twistedcaldav.method import report_common
-from twistedcaldav.resource import isCalendarCollectionResource
-from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser,\
-    PartitionedCalendarUser
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.scheduling.itip import handleRequest
-
-"""
-Class that handles delivery of scheduling messages via CalDAV.
-"""
-
-__all__ = [
-    "ScheduleViaCalDAV",
-]
-
-log = Logger()
-
-class ScheduleViaCalDAV(DeliveryService):
-    
-    def __init__(self, scheduler, recipients, responses, freebusy):
-
-        self.scheduler = scheduler
-        self.recipients = recipients
-        self.responses = responses
-        self.freebusy = freebusy
-
-    @classmethod
-    def serviceType(cls):
-        return DeliveryService.serviceType_caldav
-
-    @classmethod
-    def matchCalendarUserAddress(cls, cuaddr):
-
-        # Check for local address matches first
-        if cuaddr.startswith("mailto:") and config.Scheduling[cls.serviceType()]["EmailDomain"]:
-            splits = cuaddr[7:].split("?")
-            domain = config.Scheduling[cls.serviceType()]["EmailDomain"]
-            if splits[0].endswith(domain):
-                return True
-
-        elif (cuaddr.startswith("http://") or cuaddr.startswith("https://")) and config.Scheduling[cls.serviceType()]["HTTPDomain"]:
-            splits = cuaddr.split(":")[0][2:].split("?")
-            domain = config.Scheduling[cls.serviceType()]["HTTPDomain"]
-            if splits[0].endswith(domain):
-                return True
-
-        elif cuaddr.startswith("/"):
-            # Assume relative HTTP URL - i.e. on this server
-            return True
-        
-        # Do default match
-        return super(ScheduleViaCalDAV, cls).matchCalendarUserAddress(cuaddr)
-
-    @inlineCallbacks
-    def generateSchedulingResponses(self):
-        
-        # Extract the ORGANIZER property and UID value from the calendar data for use later
-        organizerProp = self.scheduler.calendar.getOrganizerProperty()
-        uid = self.scheduler.calendar.resourceUID()
-
-        organizerPrincipal = None
-        if type(self.scheduler.organizer) in (LocalCalendarUser, PartitionedCalendarUser,):
-            organizerPrincipal = davxml.Principal(davxml.HRef(self.scheduler.organizer.principal.principalURL()))
-
-        for recipient in self.recipients:
-
-            #
-            # Check access controls
-            #
-            if organizerPrincipal:
-                try:
-                    yield recipient.inbox.checkPrivileges(self.scheduler.request, (caldavxml.Schedule(),), principal=organizerPrincipal)
-                except AccessDeniedError:
-                    log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
-                    err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permissions")))
-                    self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
-                
-                    # Process next recipient
-                    continue
-            else:
-                # TODO: need to figure out how best to do server-to-server authorization.
-                # First thing would be to check for DAV:unauthenticated privilege.
-                # Next would be to allow the calendar user address of the organizer/originator to be used
-                # as a principal. 
-                pass
-
-            # Different behavior for free-busy vs regular invite
-            if self.freebusy:
-                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid)
-            else:
-                yield self.generateResponse(recipient, self.responses)
-
-    @inlineCallbacks
-    def generateResponse(self, recipient, responses):
-        # Hash the iCalendar data for use as the last path element of the URI path
-        calendar_str = self.scheduler.calendardata
-        name = md5(calendar_str + str(time.time()) + recipient.inbox.fp.path).hexdigest() + ".ics"
-    
-        # Get a resource for the new item
-        childURL = joinURL(recipient.inboxURL, name)
-        child = (yield self.scheduler.request.locateResource(childURL))
-
-        try:
-            # Copy calendar to inbox (doing fan-out)
-            from twistedcaldav.method.put_common import storeCalendarObjectResource
-            yield storeCalendarObjectResource(
-                request = self.scheduler.request,
-                sourcecal = False,
-                destination = child,
-                destination_uri = childURL,
-                calendardata = self.scheduler.calendardata,
-                destinationparent = recipient.inbox,
-                destinationcal = True,
-                isiTIP = True
-            )
-        except: # FIXME: bare except
-            self.log_error("Could not store data in Inbox : %s" % (recipient.inbox,))
-            err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-            responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
-            returnValue(False)
-        else:
-            responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
-
-            # Store CALDAV:originator property
-            child.writeDeadProperty(caldavxml.Originator(davxml.HRef(self.scheduler.originator.cuaddr)))
-        
-            # Store CALDAV:recipient property
-            child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient.cuaddr)))
-        
-            # Look for auto-schedule option
-            if recipient.principal.autoSchedule():
-                if not hasattr(self.scheduler.request, "extendedLogItems"):
-                    self.scheduler.request.extendedLogItems = {}
-                self.scheduler.request.extendedLogItems["itip.auto"] = self.scheduler.request.extendedLogItems.get("itip.auto", 0) + 1
-
-                # Add delayed reactor task to handle iTIP responses
-                reactor.callLater(0.0, handleRequest, *(self.scheduler.request, recipient.principal, recipient.inbox, self.scheduler.calendar.duplicate(), child)) #@UndefinedVariable
-    
-    @inlineCallbacks
-    def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid):
-
-        # Extract the ATTENDEE property matching current recipient from the calendar data
-        cuas = recipient.principal.calendarUserAddresses()
-        attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
-
-        remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
-
-        try:
-            fbresult = (yield self.generateAttendeeFreeBusyResponse(
-                recipient,
-                organizerProp,
-                organizerPrincipal,
-                uid,
-                attendeeProp,
-                remote,
-            ))
-        except:
-            log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
-            err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-            responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
-            returnValue(False)
-        else:
-            responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS, calendar=fbresult)
-            returnValue(True)
-    
-    @inlineCallbacks
-    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote):
-
-        # Find the current recipients calendar-free-busy-set
-        fbset = (yield recipient.principal.calendarFreeBusyURIs(self.scheduler.request))
-
-        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-        fbinfo = ([], [], [])
-    
-        # Process the availability property from the Inbox.
-        has_prop = (yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
-        if has_prop:
-            availability = (yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
-            availability = availability.calendar()
-            report_common.processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
-
-        # Check to see if the recipient is the same calendar user as the organizer.
-        # Needed for masked UID stuff.
-        if isinstance(self.scheduler.organizer, LocalCalendarUser):
-            same_calendar_user = self.scheduler.organizer.principal.principalURL() == recipient.principal.principalURL()
-        else:
-            same_calendar_user = False
-
-        # Now process free-busy set calendars
-        matchtotal = 0
-        for calendarResourceURL in fbset:
-
-            # Yield to the reactor once through each loop
-            d = Deferred()
-            def _timedDeferred():
-                d.callback(True)
-            reactor.callLater(0.0, _timedDeferred)
-            yield d
-
-            calendarResource = (yield self.scheduler.request.locateResource(calendarResourceURL))
-            if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
-                # We will ignore missing calendars. If the recipient has failed to
-                # properly manage the free busy set that should not prevent us from working.
-                continue
-         
-            matchtotal = (yield report_common.generateFreeBusyInfo(
-                self.scheduler.request,
-                calendarResource,
-                fbinfo,
-                self.scheduler.timeRange,
-                matchtotal,
-                excludeuid = self.scheduler.excludeUID,
-                organizer = self.scheduler.organizer.cuaddr,
-                organizerPrincipal = organizerPrincipal,
-                same_calendar_user = same_calendar_user,
-            ))
-    
-        # Build VFREEBUSY iTIP reply for this recipient
-        fbresult = report_common.buildFreeBusyResult(
-            fbinfo,
-            self.scheduler.timeRange,
-            organizer = organizerProp,
-            attendee = attendeeProp,
-            uid = uid,
-            method = "REPLY"
-        )
-
-        returnValue(fbresult)

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/caldav.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,267 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from twistedcaldav.extensions import ErrorResponse
+
+import time
+
+try:
+    from hashlib import md5
+except ImportError:
+    from md5 import new as md5
+
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
+from twisted.python.failure import Failure
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.log import Logger
+from twistedcaldav.method import report_common
+from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser,\
+    PartitionedCalendarUser
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from twistedcaldav.scheduling.itip import handleRequest
+
+"""
+Class that handles delivery of scheduling messages via CalDAV.
+"""
+
+__all__ = [
+    "ScheduleViaCalDAV",
+]
+
+log = Logger()
+
+class ScheduleViaCalDAV(DeliveryService):
+    
+    def __init__(self, scheduler, recipients, responses, freebusy):
+
+        self.scheduler = scheduler
+        self.recipients = recipients
+        self.responses = responses
+        self.freebusy = freebusy
+
+    @classmethod
+    def serviceType(cls):
+        return DeliveryService.serviceType_caldav
+
+    @classmethod
+    def matchCalendarUserAddress(cls, cuaddr):
+
+        # Check for local address matches first
+        if cuaddr.startswith("mailto:") and config.Scheduling[cls.serviceType()]["EmailDomain"]:
+            splits = cuaddr[7:].split("?")
+            domain = config.Scheduling[cls.serviceType()]["EmailDomain"]
+            if splits[0].endswith(domain):
+                return True
+
+        elif (cuaddr.startswith("http://") or cuaddr.startswith("https://")) and config.Scheduling[cls.serviceType()]["HTTPDomain"]:
+            splits = cuaddr.split(":")[0][2:].split("?")
+            domain = config.Scheduling[cls.serviceType()]["HTTPDomain"]
+            if splits[0].endswith(domain):
+                return True
+
+        elif cuaddr.startswith("/"):
+            # Assume relative HTTP URL - i.e. on this server
+            return True
+        
+        # Do default match
+        return super(ScheduleViaCalDAV, cls).matchCalendarUserAddress(cuaddr)
+
+    @inlineCallbacks
+    def generateSchedulingResponses(self):
+        
+        # Extract the ORGANIZER property and UID value from the calendar data for use later
+        organizerProp = self.scheduler.calendar.getOrganizerProperty()
+        uid = self.scheduler.calendar.resourceUID()
+
+        organizerPrincipal = None
+        if type(self.scheduler.organizer) in (LocalCalendarUser, PartitionedCalendarUser,):
+            organizerPrincipal = davxml.Principal(davxml.HRef(self.scheduler.organizer.principal.principalURL()))
+
+        for recipient in self.recipients:
+
+            #
+            # Check access controls
+            #
+            if organizerPrincipal:
+                try:
+                    yield recipient.inbox.checkPrivileges(self.scheduler.request, (caldavxml.Schedule(),), principal=organizerPrincipal)
+                except AccessDeniedError:
+                    log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
+                    err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permissions")))
+                    self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
+                
+                    # Process next recipient
+                    continue
+            else:
+                # TODO: need to figure out how best to do server-to-server authorization.
+                # First thing would be to check for DAV:unauthenticated privilege.
+                # Next would be to allow the calendar user address of the organizer/originator to be used
+                # as a principal. 
+                pass
+
+            # Different behavior for free-busy vs regular invite
+            if self.freebusy:
+                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid)
+            else:
+                yield self.generateResponse(recipient, self.responses)
+
+    @inlineCallbacks
+    def generateResponse(self, recipient, responses):
+        # Hash the iCalendar data for use as the last path element of the URI path
+        calendar_str = self.scheduler.calendardata
+        name = md5(calendar_str + str(time.time()) + recipient.inbox.fp.path).hexdigest() + ".ics"
+    
+        # Get a resource for the new item
+        childURL = joinURL(recipient.inboxURL, name)
+        child = (yield self.scheduler.request.locateResource(childURL))
+
+        try:
+            # Copy calendar to inbox (doing fan-out)
+            from twistedcaldav.method.put_common import storeCalendarObjectResource
+            yield storeCalendarObjectResource(
+                request = self.scheduler.request,
+                sourcecal = False,
+                destination = child,
+                destination_uri = childURL,
+                calendardata = self.scheduler.calendardata,
+                destinationparent = recipient.inbox,
+                destinationcal = True,
+                isiTIP = True
+            )
+        except: # FIXME: bare except
+            self.log_error("Could not store data in Inbox : %s" % (recipient.inbox,))
+            err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+            responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
+            returnValue(False)
+        else:
+            responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
+
+            # Store CALDAV:originator property
+            child.writeDeadProperty(caldavxml.Originator(davxml.HRef(self.scheduler.originator.cuaddr)))
+        
+            # Store CALDAV:recipient property
+            child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient.cuaddr)))
+        
+            # Look for auto-schedule option
+            if recipient.principal.autoSchedule():
+                if not hasattr(self.scheduler.request, "extendedLogItems"):
+                    self.scheduler.request.extendedLogItems = {}
+                self.scheduler.request.extendedLogItems["itip.auto"] = self.scheduler.request.extendedLogItems.get("itip.auto", 0) + 1
+
+                # Add delayed reactor task to handle iTIP responses
+                reactor.callLater(0.0, handleRequest, *(self.scheduler.request, recipient.principal, recipient.inbox, self.scheduler.calendar.duplicate(), child)) #@UndefinedVariable
+    
+    @inlineCallbacks
+    def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid):
+
+        # Extract the ATTENDEE property matching current recipient from the calendar data
+        cuas = recipient.principal.calendarUserAddresses()
+        attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
+
+        remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
+
+        try:
+            fbresult = (yield self.generateAttendeeFreeBusyResponse(
+                recipient,
+                organizerProp,
+                organizerPrincipal,
+                uid,
+                attendeeProp,
+                remote,
+            ))
+        except:
+            log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
+            err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+            responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
+            returnValue(False)
+        else:
+            responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS, calendar=fbresult)
+            returnValue(True)
+    
+    @inlineCallbacks
+    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote):
+
+        # Find the current recipients calendar-free-busy-set
+        fbset = (yield recipient.principal.calendarFreeBusyURIs(self.scheduler.request))
+
+        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+        fbinfo = ([], [], [])
+    
+        # Process the availability property from the Inbox.
+        has_prop = (yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
+        if has_prop:
+            availability = (yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
+            availability = availability.calendar()
+            report_common.processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
+
+        # Check to see if the recipient is the same calendar user as the organizer.
+        # Needed for masked UID stuff.
+        if isinstance(self.scheduler.organizer, LocalCalendarUser):
+            same_calendar_user = self.scheduler.organizer.principal.principalURL() == recipient.principal.principalURL()
+        else:
+            same_calendar_user = False
+
+        # Now process free-busy set calendars
+        matchtotal = 0
+        for calendarResourceURL in fbset:
+
+            # Yield to the reactor once through each loop
+            d = Deferred()
+            def _timedDeferred():
+                d.callback(True)
+            reactor.callLater(0.0, _timedDeferred)
+            yield d
+
+            calendarResource = (yield self.scheduler.request.locateResource(calendarResourceURL))
+            if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
+                # We will ignore missing calendars. If the recipient has failed to
+                # properly manage the free busy set that should not prevent us from working.
+                continue
+         
+            matchtotal = (yield report_common.generateFreeBusyInfo(
+                self.scheduler.request,
+                calendarResource,
+                fbinfo,
+                self.scheduler.timeRange,
+                matchtotal,
+                excludeuid = self.scheduler.excludeUID,
+                organizer = self.scheduler.organizer.cuaddr,
+                organizerPrincipal = organizerPrincipal,
+                same_calendar_user = same_calendar_user,
+            ))
+    
+        # Build VFREEBUSY iTIP reply for this recipient
+        fbresult = report_common.buildFreeBusyResult(
+            fbinfo,
+            self.scheduler.timeRange,
+            organizer = organizerProp,
+            attendee = attendeeProp,
+            uid = uid,
+            method = "REPLY"
+        )
+
+        returnValue(fbresult)

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/cuaddress.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/cuaddress.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/cuaddress.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,94 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twistedcaldav.log import Logger
-from twistedcaldav.scheduling.delivery import DeliveryService
-
-__all__ = [
-    "LocalCalendarUser",
-    "RemoteCalendarUser",
-    "InvalidCalendarUser",
-    "normalizeCUAddr",
-]
-
-log = Logger()
-
-class CalendarUser(object):
-    def __init__(self, cuaddr):
-        self.cuaddr = cuaddr
-        self.serviceType = None
-
-class LocalCalendarUser(CalendarUser):
-    def __init__(self, cuaddr, principal, inbox=None, inboxURL=None):
-        self.cuaddr = cuaddr
-        self.principal = principal
-        self.inbox = inbox
-        self.inboxURL = inboxURL
-        self.serviceType = DeliveryService.serviceType_caldav
-    
-    def __str__(self):
-        return "Local calendar user: %s" % (self.cuaddr,)
-
-class PartitionedCalendarUser(CalendarUser):
-    def __init__(self, cuaddr, principal):
-        self.cuaddr = cuaddr
-        self.principal = principal
-        self.serviceType = DeliveryService.serviceType_ischedule
-
-    def __str__(self):
-        return "Partitioned calendar user: %s" % (self.cuaddr,)
-
-class RemoteCalendarUser(CalendarUser):
-    def __init__(self, cuaddr):
-        self.cuaddr = cuaddr
-        self.extractDomain()
-        self.serviceType = DeliveryService.serviceType_ischedule
-
-    def __str__(self):
-        return "Remote calendar user: %s" % (self.cuaddr,)
-    
-    def extractDomain(self):
-        if self.cuaddr.startswith("mailto:"):
-            splits = self.cuaddr[7:].split("?")
-            self.domain = splits[0].split("@")[1]
-        elif self.cuaddr.startswith("http://") or self.cuaddr.startswith("https://"):
-            splits = self.cuaddr.split(":")[1][2:].split("/")
-            self.domain = splits[0]
-        else:
-            self.domain = ""
-
-class InvalidCalendarUser(CalendarUser):
-    
-    def __str__(self):
-        return "Invalid calendar user: %s" % (self.cuaddr,)
-
-
-def normalizeCUAddr(addr):
-    """
-    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
-    """
-    lower = addr.lower()
-    if lower.startswith("mailto:"):
-        addr = lower
-    if (addr.startswith("/") or
-        addr.startswith("http:") or
-        addr.startswith("https:")):
-        return addr.rstrip("/")
-    else:
-        return addr

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/cuaddress.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/cuaddress.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/cuaddress.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/cuaddress.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,94 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.delivery import DeliveryService
+
+__all__ = [
+    "LocalCalendarUser",
+    "RemoteCalendarUser",
+    "InvalidCalendarUser",
+    "normalizeCUAddr",
+]
+
+log = Logger()
+
+class CalendarUser(object):
+    def __init__(self, cuaddr):
+        self.cuaddr = cuaddr
+        self.serviceType = None
+
+class LocalCalendarUser(CalendarUser):
+    def __init__(self, cuaddr, principal, inbox=None, inboxURL=None):
+        self.cuaddr = cuaddr
+        self.principal = principal
+        self.inbox = inbox
+        self.inboxURL = inboxURL
+        self.serviceType = DeliveryService.serviceType_caldav
+    
+    def __str__(self):
+        return "Local calendar user: %s" % (self.cuaddr,)
+
+class PartitionedCalendarUser(CalendarUser):
+    def __init__(self, cuaddr, principal):
+        self.cuaddr = cuaddr
+        self.principal = principal
+        self.serviceType = DeliveryService.serviceType_ischedule
+
+    def __str__(self):
+        return "Partitioned calendar user: %s" % (self.cuaddr,)
+
+class RemoteCalendarUser(CalendarUser):
+    def __init__(self, cuaddr):
+        self.cuaddr = cuaddr
+        self.extractDomain()
+        self.serviceType = DeliveryService.serviceType_ischedule
+
+    def __str__(self):
+        return "Remote calendar user: %s" % (self.cuaddr,)
+    
+    def extractDomain(self):
+        if self.cuaddr.startswith("mailto:"):
+            splits = self.cuaddr[7:].split("?")
+            self.domain = splits[0].split("@")[1]
+        elif self.cuaddr.startswith("http://") or self.cuaddr.startswith("https://"):
+            splits = self.cuaddr.split(":")[1][2:].split("/")
+            self.domain = splits[0]
+        else:
+            self.domain = ""
+
+class InvalidCalendarUser(CalendarUser):
+    
+    def __str__(self):
+        return "Invalid calendar user: %s" % (self.cuaddr,)
+
+
+def normalizeCUAddr(addr):
+    """
+    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
+    """
+    lower = addr.lower()
+    if lower.startswith("mailto:"):
+        addr = lower
+    if (addr.startswith("/") or
+        addr.startswith("http:") or
+        addr.startswith("https:")):
+        return addr.rstrip("/")
+    else:
+        return addr

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/delivery.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/delivery.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,63 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twistedcaldav.config import config
-from twistedcaldav.log import Logger
-
-import re
-
-__all__ = [
-    "DeliveryService",
-]
-
-log = Logger()
-
-class DeliveryService(object):
-    """
-    Abstract base class that defines a delivery method for a scheduling message.
-    """
-    
-    # Known types
-    
-    serviceType_caldav    = 'CalDAV'
-    serviceType_ischedule = 'iSchedule'
-
-    def __init__(self, scheduler, recipients, responses, freebusy):
-
-        self.scheduler = scheduler
-        self.recipients = recipients
-        self.responses = responses
-        self.freebusy = freebusy
-
-    @classmethod
-    def serviceType(cls):
-        raise NotImplementedError
-
-    @classmethod
-    def matchCalendarUserAddress(cls, cuaddr):
-        
-        # Do the pattern match
-        for pattern in config.Scheduling[cls.serviceType()]["AddressPatterns"]:
-            try:
-                if re.match(pattern, cuaddr) is not None:
-                    return True
-            except re.error:
-                log.error("Invalid regular expression for Scheduling configuration '%s/LocalAddresses': %s" % (cls.serviceType(), pattern,))
-
-        return False
-
-    def generateSchedulingResponses(self):
-        raise NotImplementedError

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/delivery.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/delivery.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/delivery.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/delivery.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,63 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+
+import re
+
+__all__ = [
+    "DeliveryService",
+]
+
+log = Logger()
+
+class DeliveryService(object):
+    """
+    Abstract base class that defines a delivery method for a scheduling message.
+    """
+    
+    # Known types
+    
+    serviceType_caldav    = 'CalDAV'
+    serviceType_ischedule = 'iSchedule'
+
+    def __init__(self, scheduler, recipients, responses, freebusy):
+
+        self.scheduler = scheduler
+        self.recipients = recipients
+        self.responses = responses
+        self.freebusy = freebusy
+
+    @classmethod
+    def serviceType(cls):
+        raise NotImplementedError
+
+    @classmethod
+    def matchCalendarUserAddress(cls, cuaddr):
+        
+        # Do the pattern match
+        for pattern in config.Scheduling[cls.serviceType()]["AddressPatterns"]:
+            try:
+                if re.match(pattern, cuaddr) is not None:
+                    return True
+            except re.error:
+                log.error("Invalid regular expression for Scheduling configuration '%s/LocalAddresses': %s" % (cls.serviceType(), pattern,))
+
+        return False
+
+    def generateSchedulingResponses(self):
+        raise NotImplementedError

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/ischedule.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,224 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks, DeferredList
-from twisted.internet.protocol import ClientCreator
-
-from twisted.python.failure import Failure
-
-from twisted.web2 import responsecode
-from twisted.web2.client.http import ClientRequest
-from twisted.web2.client.http import HTTPClientProtocol
-from twisted.web2.dav.util import davXMLFromStream, joinURL
-from twisted.web2.http import HTTPError
-from twisted.web2.http_headers import Headers
-from twisted.web2.http_headers import MimeType
-
-from twisted.internet.ssl import DefaultOpenSSLContextFactory
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.extensions import ErrorResponse
-from twistedcaldav.log import Logger
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers,\
-    IScheduleServerRecord
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.util import utf8String
-from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser,\
-    PartitionedCalendarUser
-
-import OpenSSL
-
-"""
-Server to server utility functions and client requests.
-"""
-
-__all__ = [
-    "ScheduleViaISchedule",
-]
-
-log = Logger()
-
-class ScheduleViaISchedule(DeliveryService):
-    
-    @classmethod
-    def serviceType(cls):
-        return DeliveryService.serviceType_ischedule
-
-    @classmethod
-    def matchCalendarUserAddress(cls, cuaddr):
-
-        # TODO: here is where we would attempt service discovery based on the cuaddr.
-        
-        # Do default match
-        return super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr)
-
-    def generateSchedulingResponses(self):
-        """
-        Generate scheduling responses for remote recipients.
-        """
-        
-        # Group recipients by server so that we can do a single request with multiple recipients
-        # to each different server.
-        groups = {}
-        servermgr = IScheduleServers()
-        for recipient in self.recipients:
-            if isinstance(recipient, RemoteCalendarUser):
-                # Map the recipient's domain to a server
-                server = servermgr.mapDomain(recipient.domain)
-            elif isinstance(recipient, PartitionedCalendarUser):
-                server = self._getServerForPartitionedUser(recipient)
-            else:
-                assert False, "Incorrect calendar user address class"
-            if not server:
-                # Cannot do server-to-server for this recipient.
-                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
-                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
-            
-                # Process next recipient
-                continue
-            
-            if not server.allow_to:
-                # Cannot do server-to-server outgoing requests for this server.
-                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
-                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-            
-                # Process next recipient
-                continue
-            
-            groups.setdefault(server, []).append(recipient)
-
-        if len(groups) == 0:
-            return
-
-        # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
-        # we will generate for each request. That way we can have parallel requests in progress
-        # rather than serialize them.
-        deferreds = []
-        for server, recipients in groups.iteritems():
-            requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses)
-            deferreds.append(requestor.doRequest())
-
-        return DeferredList(deferreds)
-
-    def _getServerForPartitionedUser(self, recipient):
-        
-        if not hasattr(self, "partitionedServers"):
-            self.partitionedServers = {}
-            
-        partition = recipient.principal.hostedURL()
-        if partition not in self.partitionedServers:
-            self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
-            self.partitionedServers[partition].unNormalizeAddresses = False
-        
-        return self.partitionedServers[partition]
-
-class IScheduleRequest(object):
-    
-    def __init__(self, scheduler, server, recipients, responses):
-
-        self.scheduler = scheduler
-        self.server = server
-        self.recipients = recipients
-        self.responses = responses
-        
-        self._generateHeaders()
-        self._prepareData()
-        
-    @inlineCallbacks
-    def doRequest(self):
-        
-        # Generate an HTTP client request
-        try:
-            from twisted.internet import reactor
-            if self.server.ssl:
-                context = DefaultOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, sslmethod=OpenSSL.SSL.SSLv3_METHOD)
-                proto = (yield ClientCreator(reactor, HTTPClientProtocol).connectSSL(self.server.host, self.server.port, context))
-            else:
-                proto = (yield ClientCreator(reactor, HTTPClientProtocol).connectTCP(self.server.host, self.server.port))
-            
-            request = ClientRequest("POST", self.server.path, self.headers, self.data)
-            yield log.logRequest("debug", "Sending server-to-server POST request:", request)
-            response = (yield proto.submitRequest(request))
-    
-            yield log.logResponse("debug", "Received server-to-server POST response:", response)
-            xml = (yield davXMLFromStream(response.stream))
-    
-            self._parseResponse(xml)
-
-        except Exception, e:
-            # Generated failed responses for each recipient
-            log.err("Could not do server-to-server request : %s %s" % (self, e))
-            for recipient in self.recipients:
-                err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
-                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
-    def _generateHeaders(self):
-        self.headers = Headers()
-        self.headers.setHeader('Host', utf8String(self.server.host + ":%s" % (self.server.port,)))
-        
-        # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
-        self.headers.addRawHeader('Originator', utf8String(self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee))
-        self._doAuthentication()
-        for recipient in self.recipients:
-            self.headers.addRawHeader('Recipient', utf8String(recipient.cuaddr))
-        self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
-
-    def _doAuthentication(self):
-        if self.server.authentication and self.server.authentication[0] == "basic":
-            self.headers.setHeader(
-                'Authorization',
-                ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
-            )
-
-    def _prepareData(self):
-        if self.server.unNormalizeAddresses and self.scheduler.method == "PUT": 
-            def lookupFunction(cuaddr):
-                principal = self.scheduler.resource.principalForCalendarUserAddress(cuaddr)
-                if principal is None:
-                    return (None, None, None)
-                else:
-                    return (principal.record.fullName.decode("utf-8"),
-                        principal.record.guid,
-                        principal.record.calendarUserAddresses)
-
-            normalizedCalendar = self.scheduler.calendar.duplicate()
-            normalizedCalendar.normalizeCalendarUserAddresses(lookupFunction, toUUID=False)
-        else:
-            normalizedCalendar = self.scheduler.calendar
-        self.data = str(normalizedCalendar)
-
-    def _parseResponse(self, xml):
-
-        # Check for correct root element
-        schedule_response = xml.root_element
-        if not isinstance(schedule_response, caldavxml.ScheduleResponse) or not schedule_response.children:
-            raise HTTPError(responsecode.BAD_REQUEST)
-        
-        # Parse each response - do this twice: once looking for errors that will
-        # result in all recipients shown as failures; the second loop adds all the
-        # valid responses to the actual result.
-        for response in schedule_response.children:
-            if not isinstance(response, caldavxml.Response) or not response.children:
-                raise HTTPError(responsecode.BAD_REQUEST)
-            recipient = response.childOfType(caldavxml.Recipient)
-            request_status = response.childOfType(caldavxml.RequestStatus)
-            if not recipient or not request_status:
-                raise HTTPError(responsecode.BAD_REQUEST)
-        for response in schedule_response.children:
-            self.responses.clone(response)

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/ischedule.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,224 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, DeferredList
+from twisted.internet.protocol import ClientCreator
+
+from twisted.python.failure import Failure
+
+from twisted.web2 import responsecode
+from twisted.web2.client.http import ClientRequest
+from twisted.web2.client.http import HTTPClientProtocol
+from twisted.web2.dav.util import davXMLFromStream, joinURL
+from twisted.web2.http import HTTPError
+from twisted.web2.http_headers import Headers
+from twisted.web2.http_headers import MimeType
+
+from twisted.internet.ssl import DefaultOpenSSLContextFactory
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.extensions import ErrorResponse
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.ischeduleservers import IScheduleServers,\
+    IScheduleServerRecord
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from twistedcaldav.util import utf8String
+from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser,\
+    PartitionedCalendarUser
+
+import OpenSSL
+
+"""
+Server to server utility functions and client requests.
+"""
+
+__all__ = [
+    "ScheduleViaISchedule",
+]
+
+log = Logger()
+
+class ScheduleViaISchedule(DeliveryService):
+    
+    @classmethod
+    def serviceType(cls):
+        return DeliveryService.serviceType_ischedule
+
+    @classmethod
+    def matchCalendarUserAddress(cls, cuaddr):
+
+        # TODO: here is where we would attempt service discovery based on the cuaddr.
+        
+        # Do default match
+        return super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr)
+
+    def generateSchedulingResponses(self):
+        """
+        Generate scheduling responses for remote recipients.
+        """
+        
+        # Group recipients by server so that we can do a single request with multiple recipients
+        # to each different server.
+        groups = {}
+        servermgr = IScheduleServers()
+        for recipient in self.recipients:
+            if isinstance(recipient, RemoteCalendarUser):
+                # Map the recipient's domain to a server
+                server = servermgr.mapDomain(recipient.domain)
+            elif isinstance(recipient, PartitionedCalendarUser):
+                server = self._getServerForPartitionedUser(recipient)
+            else:
+                assert False, "Incorrect calendar user address class"
+            if not server:
+                # Cannot do server-to-server for this recipient.
+                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
+                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
+            
+                # Process next recipient
+                continue
+            
+            if not server.allow_to:
+                # Cannot do server-to-server outgoing requests for this server.
+                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
+                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
+            
+                # Process next recipient
+                continue
+            
+            groups.setdefault(server, []).append(recipient)
+
+        if len(groups) == 0:
+            return
+
+        # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
+        # we will generate for each request. That way we can have parallel requests in progress
+        # rather than serialize them.
+        deferreds = []
+        for server, recipients in groups.iteritems():
+            requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses)
+            deferreds.append(requestor.doRequest())
+
+        return DeferredList(deferreds)
+
+    def _getServerForPartitionedUser(self, recipient):
+        
+        if not hasattr(self, "partitionedServers"):
+            self.partitionedServers = {}
+            
+        partition = recipient.principal.hostedURL()
+        if partition not in self.partitionedServers:
+            self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
+            self.partitionedServers[partition].unNormalizeAddresses = False
+        
+        return self.partitionedServers[partition]
+
+class IScheduleRequest(object):
+    
+    def __init__(self, scheduler, server, recipients, responses):
+
+        self.scheduler = scheduler
+        self.server = server
+        self.recipients = recipients
+        self.responses = responses
+        
+        self._generateHeaders()
+        self._prepareData()
+        
+    @inlineCallbacks
+    def doRequest(self):
+        
+        # Generate an HTTP client request
+        try:
+            from twisted.internet import reactor
+            if self.server.ssl:
+                context = DefaultOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, sslmethod=OpenSSL.SSL.SSLv3_METHOD)
+                proto = (yield ClientCreator(reactor, HTTPClientProtocol).connectSSL(self.server.host, self.server.port, context))
+            else:
+                proto = (yield ClientCreator(reactor, HTTPClientProtocol).connectTCP(self.server.host, self.server.port))
+            
+            request = ClientRequest("POST", self.server.path, self.headers, self.data)
+            yield log.logRequest("debug", "Sending server-to-server POST request:", request)
+            response = (yield proto.submitRequest(request))
+    
+            yield log.logResponse("debug", "Received server-to-server POST response:", response)
+            xml = (yield davXMLFromStream(response.stream))
+    
+            self._parseResponse(xml)
+
+        except Exception, e:
+            # Generated failed responses for each recipient
+            log.err("Could not do server-to-server request : %s %s" % (self, e))
+            for recipient in self.recipients:
+                err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
+                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+    def _generateHeaders(self):
+        self.headers = Headers()
+        self.headers.setHeader('Host', utf8String(self.server.host + ":%s" % (self.server.port,)))
+        
+        # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
+        self.headers.addRawHeader('Originator', utf8String(self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee))
+        self._doAuthentication()
+        for recipient in self.recipients:
+            self.headers.addRawHeader('Recipient', utf8String(recipient.cuaddr))
+        self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
+
+    def _doAuthentication(self):
+        if self.server.authentication and self.server.authentication[0] == "basic":
+            self.headers.setHeader(
+                'Authorization',
+                ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
+            )
+
+    def _prepareData(self):
+        if self.server.unNormalizeAddresses and self.scheduler.method == "PUT": 
+            def lookupFunction(cuaddr):
+                principal = self.scheduler.resource.principalForCalendarUserAddress(cuaddr)
+                if principal is None:
+                    return (None, None, None)
+                else:
+                    return (principal.record.fullName.decode("utf-8"),
+                        principal.record.guid,
+                        principal.record.calendarUserAddresses)
+
+            normalizedCalendar = self.scheduler.calendar.duplicate()
+            normalizedCalendar.normalizeCalendarUserAddresses(lookupFunction, toUUID=False)
+        else:
+            normalizedCalendar = self.scheduler.calendar
+        self.data = str(normalizedCalendar)
+
+    def _parseResponse(self, xml):
+
+        # Check for correct root element
+        schedule_response = xml.root_element
+        if not isinstance(schedule_response, caldavxml.ScheduleResponse) or not schedule_response.children:
+            raise HTTPError(responsecode.BAD_REQUEST)
+        
+        # Parse each response - do this twice: once looking for errors that will
+        # result in all recipients shown as failures; the second loop adds all the
+        # valid responses to the actual result.
+        for response in schedule_response.children:
+            if not isinstance(response, caldavxml.Response) or not response.children:
+                raise HTTPError(responsecode.BAD_REQUEST)
+            recipient = response.childOfType(caldavxml.Recipient)
+            request_status = response.childOfType(caldavxml.RequestStatus)
+            if not recipient or not request_status:
+                raise HTTPError(responsecode.BAD_REQUEST)
+        for response in schedule_response.children:
+            self.responses.clone(response)

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/ischeduleservers.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,207 +0,0 @@
-##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.python.filepath import FilePath
-
-from twistedcaldav.config import config
-from twistedcaldav.log import Logger
-from twistedcaldav.scheduling.delivery import DeliveryService
-
-import xml.dom.minidom
-
-"""
-XML based iSchedule configuration file handling.
-"""
-
-__all__ = [
-    "IScheduleServers",
-]
-
-log = Logger()
-
-class IScheduleServers(object):
-    
-    _fileInfo = None
-    _xmlFile = None
-    _servers = None
-    _domainMap = None
-    
-    def __init__(self):
-        
-        self._loadConfig()
-
-    def _loadConfig(self):
-        if IScheduleServers._servers is None:
-            IScheduleServers._xmlFile = FilePath(config.Scheduling[DeliveryService.serviceType_ischedule]["Servers"])
-        IScheduleServers._xmlFile.restat()
-        fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
-        if fileInfo != IScheduleServers._fileInfo:
-            parser = IScheduleServersParser(IScheduleServers._xmlFile)
-            IScheduleServers._servers = parser.servers
-            self._mapDomains()
-            IScheduleServers._fileInfo = fileInfo
-        
-    def _mapDomains(self):
-        IScheduleServers._domainMap = {}
-        for server in IScheduleServers._servers:
-            for domain in server.domains:
-                IScheduleServers._domainMap[domain] = server
-    
-    def mapDomain(self, domain):
-        """
-        Map a calendar user address domain to a suitable server that can
-        handle server-to-server requests for that user.
-        """
-        return IScheduleServers._domainMap.get(domain)
-
-ELEMENT_SERVERS                 = "servers"
-ELEMENT_SERVER                  = "server"
-ELEMENT_URI                     = "uri"
-ELEMENT_AUTHENTICATION          = "authentication"
-ATTRIBUTE_TYPE                  = "type"
-ATTRIBUTE_BASICAUTH             = "basic"
-ELEMENT_USER                    = "user"
-ELEMENT_PASSWORD                = "password"
-ELEMENT_ALLOW_REQUESTS_FROM     = "allow-requests-from"
-ELEMENT_ALLOW_REQUESTS_TO       = "allow-requests-to"
-ELEMENT_DOMAINS                 = "domains"
-ELEMENT_DOMAIN                  = "domain"
-ELEMENT_CLIENT_HOSTS            = "hosts"
-ELEMENT_HOST                    = "host"
-
-class IScheduleServersParser(object):
-    """
-    Server-to-server configuration file parser.
-    """
-    def __repr__(self):
-        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
-
-    def __init__(self, xmlFile):
-
-        self.servers = []
-        
-        # Read in XML
-        fd = open(xmlFile.path, "r")
-        doc = xml.dom.minidom.parse(fd)
-        fd.close()
-
-        # Verify that top-level element is correct
-        servers_node = doc._get_documentElement()
-        if servers_node._get_localName() != ELEMENT_SERVERS:
-            log.error("Ignoring file %r because it is not a server-to-server config file" % (self.xmlFile,))
-            return
-        self._parseXML(servers_node)
-        
-    def _parseXML(self, node):
-        """
-        Parse the XML root node from the server-to-server configuration document.
-        @param node: the L{Node} to parse.
-        """
-
-        for child in node._get_childNodes():
-            child_name = child._get_localName()
-            if child_name is None:
-                continue
-            elif child_name == ELEMENT_SERVER:
-                self.servers.append(IScheduleServerRecord())
-                self.servers[-1].parseXML(child)
-                
-class IScheduleServerRecord (object):
-    """
-    Contains server-to-server details.
-    """
-    def __init__(self, uri=None):
-        """
-        @param recordType: record type for directory entry.
-        """
-        self.uri = ""
-        self.authentication = None
-        self.allow_from = False
-        self.allow_to = True
-        self.domains = []
-        self.client_hosts = []
-        self.unNormalizeAddresses = True
-        
-        if uri:
-            self.uri = uri
-            self._parseDetails()
-
-    def parseXML(self, node):
-        for child in node._get_childNodes():
-            child_name = child._get_localName()
-            if child_name is None:
-                continue
-            elif child_name == ELEMENT_URI:
-                if child.firstChild is not None:
-                    self.uri = child.firstChild.data.encode("utf-8")
-            elif child_name == ELEMENT_AUTHENTICATION:
-                self._parseAuthentication(child)
-            elif child_name == ELEMENT_ALLOW_REQUESTS_FROM:
-                self.allow_from = True
-            elif child_name == ELEMENT_ALLOW_REQUESTS_TO:
-                self.allow_to = True
-            elif child_name == ELEMENT_DOMAINS:
-                self._parseList(child, ELEMENT_DOMAIN, self.domains)
-            elif child_name == ELEMENT_CLIENT_HOSTS:
-                self._parseList(child, ELEMENT_HOST, self.client_hosts)
-            else:
-                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child_name,))
-        
-        self._parseDetails()
-
-    def _parseList(self, node, element_name, appendto):
-        for child in node._get_childNodes():
-            if child._get_localName() == element_name:
-                if child.firstChild is not None:
-                    appendto.append(child.firstChild.data.encode("utf-8"))
-
-    def _parseAuthentication(self, node):
-        if node.hasAttribute(ATTRIBUTE_TYPE):
-            atype = node.getAttribute(ATTRIBUTE_TYPE).encode("utf-8")
-            if atype != ATTRIBUTE_BASICAUTH:
-                return
-        else:
-            return
-
-        for child in node._get_childNodes():
-            if child._get_localName() == ELEMENT_USER:
-                if child.firstChild is not None:
-                    user = child.firstChild.data.encode("utf-8")
-            elif child._get_localName() == ELEMENT_PASSWORD:
-                if child.firstChild is not None:
-                    password = child.firstChild.data.encode("utf-8")
-        
-        self.authentication = ("basic", user, password,)
-
-    def _parseDetails(self):
-        # Extract scheme, host, port and path
-        if self.uri.startswith("http://"):
-            self.ssl = False
-            rest = self.uri[7:]
-        elif self.uri.startswith("https://"):
-            self.ssl = True
-            rest = self.uri[8:]
-        
-        splits = rest.split("/", 1)
-        hostport = splits[0].split(":")
-        self.host = hostport[0]
-        if len(hostport) > 1:
-            self.port = int(hostport[1])
-        else:
-            self.port = {False:80, True:443}[self.ssl]
-        self.path = "/"
-        if len(splits) > 1:
-            self.path += splits[1]

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/ischeduleservers.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,207 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.python.filepath import FilePath
+
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.delivery import DeliveryService
+
+import xml.dom.minidom
+
+"""
+XML based iSchedule configuration file handling.
+"""
+
+__all__ = [
+    "IScheduleServers",
+]
+
+log = Logger()
+
+class IScheduleServers(object):
+    
+    _fileInfo = None
+    _xmlFile = None
+    _servers = None
+    _domainMap = None
+    
+    def __init__(self):
+        
+        self._loadConfig()
+
+    def _loadConfig(self):
+        if IScheduleServers._servers is None:
+            IScheduleServers._xmlFile = FilePath(config.Scheduling[DeliveryService.serviceType_ischedule]["Servers"])
+        IScheduleServers._xmlFile.restat()
+        fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
+        if fileInfo != IScheduleServers._fileInfo:
+            parser = IScheduleServersParser(IScheduleServers._xmlFile)
+            IScheduleServers._servers = parser.servers
+            self._mapDomains()
+            IScheduleServers._fileInfo = fileInfo
+        
+    def _mapDomains(self):
+        IScheduleServers._domainMap = {}
+        for server in IScheduleServers._servers:
+            for domain in server.domains:
+                IScheduleServers._domainMap[domain] = server
+    
+    def mapDomain(self, domain):
+        """
+        Map a calendar user address domain to a suitable server that can
+        handle server-to-server requests for that user.
+        """
+        return IScheduleServers._domainMap.get(domain)
+
+ELEMENT_SERVERS                 = "servers"
+ELEMENT_SERVER                  = "server"
+ELEMENT_URI                     = "uri"
+ELEMENT_AUTHENTICATION          = "authentication"
+ATTRIBUTE_TYPE                  = "type"
+ATTRIBUTE_BASICAUTH             = "basic"
+ELEMENT_USER                    = "user"
+ELEMENT_PASSWORD                = "password"
+ELEMENT_ALLOW_REQUESTS_FROM     = "allow-requests-from"
+ELEMENT_ALLOW_REQUESTS_TO       = "allow-requests-to"
+ELEMENT_DOMAINS                 = "domains"
+ELEMENT_DOMAIN                  = "domain"
+ELEMENT_CLIENT_HOSTS            = "hosts"
+ELEMENT_HOST                    = "host"
+
+class IScheduleServersParser(object):
+    """
+    Server-to-server configuration file parser.
+    """
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+    def __init__(self, xmlFile):
+
+        self.servers = []
+        
+        # Read in XML
+        fd = open(xmlFile.path, "r")
+        doc = xml.dom.minidom.parse(fd)
+        fd.close()
+
+        # Verify that top-level element is correct
+        servers_node = doc._get_documentElement()
+        if servers_node._get_localName() != ELEMENT_SERVERS:
+            log.error("Ignoring file %r because it is not a server-to-server config file" % (self.xmlFile,))
+            return
+        self._parseXML(servers_node)
+        
+    def _parseXML(self, node):
+        """
+        Parse the XML root node from the server-to-server configuration document.
+        @param node: the L{Node} to parse.
+        """
+
+        for child in node._get_childNodes():
+            child_name = child._get_localName()
+            if child_name is None:
+                continue
+            elif child_name == ELEMENT_SERVER:
+                self.servers.append(IScheduleServerRecord())
+                self.servers[-1].parseXML(child)
+                
+class IScheduleServerRecord (object):
+    """
+    Contains server-to-server details.
+    """
+    def __init__(self, uri=None):
+        """
+        @param recordType: record type for directory entry.
+        """
+        self.uri = ""
+        self.authentication = None
+        self.allow_from = False
+        self.allow_to = True
+        self.domains = []
+        self.client_hosts = []
+        self.unNormalizeAddresses = True
+        
+        if uri:
+            self.uri = uri
+            self._parseDetails()
+
+    def parseXML(self, node):
+        for child in node._get_childNodes():
+            child_name = child._get_localName()
+            if child_name is None:
+                continue
+            elif child_name == ELEMENT_URI:
+                if child.firstChild is not None:
+                    self.uri = child.firstChild.data.encode("utf-8")
+            elif child_name == ELEMENT_AUTHENTICATION:
+                self._parseAuthentication(child)
+            elif child_name == ELEMENT_ALLOW_REQUESTS_FROM:
+                self.allow_from = True
+            elif child_name == ELEMENT_ALLOW_REQUESTS_TO:
+                self.allow_to = True
+            elif child_name == ELEMENT_DOMAINS:
+                self._parseList(child, ELEMENT_DOMAIN, self.domains)
+            elif child_name == ELEMENT_CLIENT_HOSTS:
+                self._parseList(child, ELEMENT_HOST, self.client_hosts)
+            else:
+                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child_name,))
+        
+        self._parseDetails()
+
+    def _parseList(self, node, element_name, appendto):
+        for child in node._get_childNodes():
+            if child._get_localName() == element_name:
+                if child.firstChild is not None:
+                    appendto.append(child.firstChild.data.encode("utf-8"))
+
+    def _parseAuthentication(self, node):
+        if node.hasAttribute(ATTRIBUTE_TYPE):
+            atype = node.getAttribute(ATTRIBUTE_TYPE).encode("utf-8")
+            if atype != ATTRIBUTE_BASICAUTH:
+                return
+        else:
+            return
+
+        for child in node._get_childNodes():
+            if child._get_localName() == ELEMENT_USER:
+                if child.firstChild is not None:
+                    user = child.firstChild.data.encode("utf-8")
+            elif child._get_localName() == ELEMENT_PASSWORD:
+                if child.firstChild is not None:
+                    password = child.firstChild.data.encode("utf-8")
+        
+        self.authentication = ("basic", user, password,)
+
+    def _parseDetails(self):
+        # Extract scheme, host, port and path
+        if self.uri.startswith("http://"):
+            self.ssl = False
+            rest = self.uri[7:]
+        elif self.uri.startswith("https://"):
+            self.ssl = True
+            rest = self.uri[8:]
+        
+        splits = rest.split("/", 1)
+        hostport = splits[0].split(":")
+        self.host = hostport[0]
+        if len(hostport) > 1:
+            self.port = int(hostport[1])
+        else:
+            self.port = {False:80, True:443}[self.ssl]
+        self.path = "/"
+        if len(splits) > 1:
+            self.path += splits[1]

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/itip.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/itip.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,990 +0,0 @@
-##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-iTIP (RFC2446) processing.
-"""
-
-#
-# This is currently used for handling auto-replies to schedule requests arriving
-# in an inbox. It is called in a delayed fashion via reactor.callLater.
-#
-# We assume that all the components/calendars we deal with have been determined
-# as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
-# types, etc.
-#
-# The logic for component matching needs a lot more work as it currently does not
-# know how to deal with overridden instances.
-#
-
-import datetime
-import md5
-import time
-
-from twisted.python.failure import Failure
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
-from twisted.web2.dav import davxml
-from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
-from twisted.web2.dav.util import joinURL
-from twisted.web2.dav.fileop import delete
-from twisted.web2.dav.resource import AccessDeniedError
-
-from twistedcaldav import caldavxml
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.log import Logger
-from twistedcaldav.ical import Property, iCalendarProductID
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
-from twistedcaldav.resource import isCalendarCollectionResource
-
-log = Logger()
-
-__version__ = "0.0"
-
-__all__ = [
-    "handleRequest",
-    "canAutoRespond",
-]
-
-class iTipException(Exception):
-    pass
-
-def handleRequest(request, principal, inbox, calendar, child):
-    """
-    Handle an iTIP response automatically.
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    @return: L{Deferred} that is a L{deferredGenerator}
-    """
-    
-    method = calendar.propertyValue("METHOD")
-    if method == "REQUEST":
-        f = processRequest
-    elif method == "ADD":
-        f = processAdd
-    elif method == "CANCEL":
-        f = processCancel
-
-    return f(request, principal, inbox, calendar, child)
-
- at inlineCallbacks
-def processRequest(request, principal, inbox, calendar, child):
-    """
-    Process a METHOD=REQUEST.
-
-    Steps:
-    
-      1. See if this updates existing ones in Inbox.
-          1. If so,
-              1. Remove existing ones in Inbox.
-              2. See if this updates existing ones in free-busy-set calendars.
-              3. Remove existing ones in those calendars.
-              4. See if this fits into a free slot:
-                  1. If not, send REPLY with failure status
-                  2. If so
-                      1. send REPLY with success
-                      2. add to f-b-s calendar
-          2. If not,
-              1. remove the one we got - its 'stale'
-          3. Delete the request from the Inbox.
-    
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    """
-    
-    log.info("Auto-processing iTIP REQUEST for: %s" % (str(principal),))
-    processed = "ignored"
-
-    # First determine whether this is a full or partial update. A full update is one containing the master
-    # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
-    # being changed.
-    
-    new_master = calendar.masterComponent()
-
-    # Next we want to try and find a match to any components on existing calendars listed as contributing
-    # to free-busy as we will need to update those with the new one.
-    calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-    
-    if new_master:
-        # So we have a full update. That means we need to delete any existing events completely and
-        # replace with the ones provided so long as the new one is newer.
-        
-        # If we have a match then we need to check whether we are updating etc
-        check_reply = False
-        if calmatch:
-            # See whether the new component is older than any existing ones and throw it away if so
-            newinfo = (None,) + getComponentSyncInfo(new_master)
-            cal = updatecal.iCalendar(calmatch)
-            old_master = cal.masterComponent()
-            if old_master:
-                info = getSyncInfo(calmatch, cal)
-            else:
-                info = None
-            if info is None or compareSyncInfo(info, newinfo) < 0:
-                # Existing resource is older and will be replaced
-                check_reply = True
-            else:
-                processed = "older"
-        else:
-            # We have a new request which we can reply to
-            check_reply = True
-            
-        if check_reply:
-            # Process the reply by determining PARTSTAT and sending the reply and booking the event.
-            doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-            
-            try:
-                if accepted:
-                    if calmatch:
-                        newchild = yield writeResource(request, calURL, updatecal, calmatch, calendar)
-                        log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
-                    else:
-                        newchild = yield writeResource(request, calURL, updatecal, None, calendar)
-                        log.info("Added new calendar component in %s." % (calURL,))
-                else:
-                    if calmatch:
-                        yield deleteResource(updatecal, calmatch)
-                        log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
-                        
-                # Send a reply if needed. 
-                if doreply:
-                    log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
-                    newchild = yield writeReply(request, principal, replycal, inbox)
-                    if newchild:
-                        newInboxResource(child, newchild)
-                processed = "processed"
-            except:
-                # FIXME: bare except
-                log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-                raise iTipException()
-            
-    else:
-        # So we have a partial update. That means we have to do partial updates to instances in
-        # the existing calendar component.
-
-        # If we have a match then we need to check whether we are updating etc
-        check_reply = False
-        if calmatch:
-            # Check each component to see whether its new
-            cal = updatecal.iCalendar(calmatch)
-            old_master = cal.masterComponent()
-            processed = "older"
-            new_components = [component for component in calendar.subcomponents()]
-            for component in new_components:
-                if component.name() == "VTIMEZONE":
-                    continue
-                
-                newinfo = (None,) + getComponentSyncInfo(component)
-                old_component = findMatchingComponent(component, cal)
-                if old_component:
-                    info = (None,) + getComponentSyncInfo(old_component)
-                elif old_master:
-                    info = (None,) + getComponentSyncInfo(old_master)
-                else:
-                    info = None
-                    
-                if info is None or compareSyncInfo(info, newinfo) < 0:
-                    # Existing resource is older and will be replaced
-                    check_reply = True
-                    processed = "processed"
-                else:
-                    calendar.removeComponent(component)
-        else:
-            # We have a new request which we can reply to
-            check_reply = True
-
-        if check_reply:
-            # Process the reply by determining PARTSTAT and sending the reply and booking the event.
-            doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-            
-            try:
-                if calmatch:
-                    # Merge the new instances with the old ones
-                    mergeComponents(calendar, cal)
-                    newchild = yield writeResource(request, calURL, updatecal, calmatch, cal)
-                    log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
-                else:
-                    if accepted:
-                        newchild = yield writeResource(request, calURL, updatecal, None, calendar)
-                        log.info("Added new calendar component in %s." % (calURL,))
-                        
-                # Do reply if needed. 
-                if doreply:
-                    log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
-                    newchild = yield writeReply(request, principal, replycal, inbox)
-                    if newchild:
-                        newInboxResource(child, newchild)
-                    
-                processed = "processed"
-            except:
-                # FIXME: bare except
-                log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-                raise iTipException()
-
-    # Remove the now processed incoming request.
-    try:
-        yield deleteResource(inbox, child.fp.basename())
-        log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
-                   child.fp.basename(),
-                   {
-                     "processed": "processed",
-                     "older"    : "ignored: older",
-                     "ignored"  : "ignored: no match"
-                   }[processed]
-                ))
-    except:
-        # FIXME: bare except
-        log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-        raise iTipException()
-
-def processAdd(request, principal, inbox, calendar, child):
-    """
-    Process a METHOD=ADD.
-
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    """
-    log.info("Auto-processing iTIP ADD for: %s" % (str(principal),))
-    raise NotImplementedError()
-
- at inlineCallbacks
-def processCancel(request, principal, inbox, calendar, child):
-    """
-    Process a METHOD=CANCEL.
-
-    Policy find all components that match UID, SEQ and R-ID and remove them.
-
-    Steps:
-    
-      1. See if this updates existing ones in Inbox.
-      2. Remove existing ones in Inbox.
-      3. See if this updates existing ones in free-busy-set calendars.
-      4. Remove existing ones in those calendars.
-      5. Remove the incoming request.
-
-    NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
-    instance may be cancelled. What we need to do for this is:
-    
-      1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
-         delete the old one.
-      2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
-         leave the new and existing ones in the Inbox.
-      3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
-         remove the overridden component from the f-b-s resource.
-      4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
-    
-    TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
-    
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    """
-    
-    log.info("Auto-processing iTIP CANCEL for: %s" % (str(principal),))
-    processed = "ignored"
-
-    # Get all component info for this iTIP message
-    newinfo = getSyncInfo(child.fp.basename(), calendar)
-    info = getAllInfo(inbox, calendar, child)
-
-    # First see if we have a recurrence id which will force extra work
-    has_rid = False
-    if newinfo[4] is not None:
-        has_rid = True
-    else:
-        for i in info:
-            if i[4] is not None:
-                has_rid = True
-                break
-            
-    if not has_rid:
-        # Compare the new one with each existing one.
-        delete_child = yield processOthersInInbox(info, newinfo, inbox, child)
-        if delete_child:
-            return
-
-        # Next we want to try and find a match to any components on existing calendars listed as contributing
-        # to free-busy as we will need to update those with the new one.
-        calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-        
-        # If we have a match then we need to check whether we are updating etc
-        if calmatch:
-            # See whether the current component is older than any existing ones and throw it away if so
-            cal = updatecal.iCalendar(calmatch)
-            info = getSyncInfo(calmatch, cal)
-            if compareSyncInfo(info, newinfo) < 0:
-                # Delete existing resource which has been cancelled
-                try:
-                    yield deleteResource(updatecal, calmatch)
-                    log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
-                except:
-                    # FIXME: bare except
-                    log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-                    raise iTipException()
-                processed = "processed"
-            else:
-                processed = "older"
-        else:
-            # Nothing to do except delete the inbox item as we have nothing to cancel.
-            processed = "ignored"
-    else:
-        # Try and find a match to any components on existing calendars listed as contributing
-        # to free-busy as we will need to update those with the new one.
-        calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-        
-        # If we have a match then we need to check whether we are updating etc
-        if calmatch:
-            # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
-            # So we need to iterate over each iTIP component.
-
-            # Get the existing calendar object
-            existing_calendar = updatecal.iCalendar(calmatch)
-            existing_master = existing_calendar.masterComponent()
-            exdates = []
-
-            for component in calendar.subcomponents():
-                if component.name() == "VTIMEZONE":
-                    continue
-            
-                # Find matching component in existing calendar
-                old_component = findMatchingComponent(component, existing_calendar)
-                
-                if old_component:
-                    # We are cancelling an overridden component, so we need to check the
-                    # SEQUENCE/DTSAMP with the master.
-                    if compareComponents(old_component, component) < 0:
-                        # Exclude the cancelled instance
-                        exdates.append(component.getRecurrenceIDUTC())
-                        
-                        # Remove the existing component.
-                        existing_calendar.removeComponent(old_component)
-                elif existing_master:
-                    # We are trying to CANCEL a non-overridden instance, so we need to
-                    # check SEQUENCE/DTSTAMP with the master.
-                    if compareComponents(existing_master, component) < 0:
-                        # Exclude the cancelled instance
-                        exdates.append(component.getRecurrenceIDUTC())
-
-            # If we have any EXDATEs lets add them to the existing calendar object and write
-            # it back.
-            if exdates:
-                if existing_master:
-                    existing_master.addProperty(Property("EXDATE", exdates))
-
-                # See if there are still components in the calendar - we might have deleted the last overridden instance
-                # in which case the calendar object is empty (except for VTIMEZONEs).
-                if existing_calendar.mainType() is None:
-                    # Delete the now empty calendar object
-                    yield deleteResource(updatecal, calmatch)
-                    log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
-                else:
-                    # Update the existing calendar object
-                    yield writeResource(request, calURL, updatecal, calmatch, existing_calendar)
-                    log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
-                processed = "processed"
-            else:
-                processed = "older"
-        else:
-            # Nothing to do except delete the inbox item as we have nothing to cancel.
-            processed = "ignored"
-
-    # Remove the now processed incoming request.
-    try:
-        yield deleteResource(inbox, child.fp.basename())
-        log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
-                  child.fp.basename(),
-                  {
-                    "processed": "processed",
-                    "older"    : "ignored: older",
-                    "ignored"  : "ignored: no match"
-                  }[processed]
-                ))
-    except:
-        # FIXME: bare except
-        log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-        raise iTipException()
-
- at inlineCallbacks
-def checkForReply(request, principal, calendar):
-    """
-    Check whether a reply to the given iTIP message is needed. We will not process a reply
-    if RSVP=FALSE. A reply will either be positive (accepted
-    invitation) or negative (denied invitation). In addition we will modify calendar to reflect
-    any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
-    
-    BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
-    At the moment we will treat a failure on one instances as a DECLINE of the entire set.
-
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @return: C{True} if a reply is needed, C{False} otherwise.
-    """
-    
-    # We need to figure out whether the specified component will clash with any others in the f-b-set calendars
-    accepted = True
-        
-    # First expand current one to get instances (only go 1 year into the future)
-    default_future_expansion_duration = datetime.timedelta(days=356*1)
-    expand_max = datetime.date.today() + default_future_expansion_duration
-    instances = calendar.expandTimeRanges(expand_max)
-    
-    # Extract UID from primary component as we want to ignore this one if we match it
-    # in any calendars.
-    comp = calendar.mainComponent(allow_multiple=True)
-    uid = comp.propertyValue("UID")
-
-    # Now compare each instance time-range with the index and see if there is an overlap
-    fbset = yield principal.calendarFreeBusyURIs(request)
-
-    for calURL in fbset:
-        testcal = yield request.locateResource(calURL)
-        
-        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-        fbinfo = ([], [], [])
-        
-        # Now do search for overlapping time-range
-        for instance in instances.instances.itervalues():
-            try:
-                tr = caldavxml.TimeRange(start="20000101", end="20000101")
-                tr.start = instance.start
-                tr.end = instance.end
-                yield report_common.generateFreeBusyInfo(request, testcal, fbinfo, tr, 0, uid)
-                
-                # If any fbinfo entries exist we have an overlap
-                if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
-                    accepted = False
-                    break
-            except NumberOfMatchesWithinLimits:
-                accepted = False
-                log.info("Exceeded number of matches whilst trying to find free-time.")
-                break
-            
-        if not accepted:
-            break
-     
-    # Extract the ATTENDEE property matching current recipient from the calendar data
-    cuas = principal.calendarUserAddresses()
-    attendeeProps = calendar.getAttendeeProperties(cuas)
-    if not attendeeProps:
-        returnValue((False, None, accepted))
-
-    # Look for specific parameters
-    rsvp = True
-    for attendeeProp in attendeeProps:
-        if "RSVP" in attendeeProp.params():
-            if attendeeProp.params()["RSVP"][0] == "FALSE":
-                rsvp = False
-    
-            # Now modify the original component
-            del attendeeProp.params()["RSVP"]
-
-    if accepted:
-        partstat = "ACCEPTED"
-    else:
-        partstat = "DECLINED"
-    for attendeeProp in attendeeProps:
-        if "PARTSTAT" in attendeeProp.params():
-            attendeeProp.params()["PARTSTAT"][0] = partstat
-        else:
-            attendeeProp.params()["PARTSTAT"] = [partstat]
-    
-    # Now create a new calendar object for the reply
-    
-    # First get useful props from the original
-    replycal = calendar.duplicate()
-    
-    # Change METHOD
-    replycal.getProperty("METHOD").setValue("REPLY")
-    
-    # Change PRODID to this server
-    replycal.getProperty("PRODID").setValue(iCalendarProductID)
-    
-    # Add REQUEST-STATUS
-    for component in replycal.subcomponents():
-        if accepted:
-            component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
-        else:
-            component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
-
-    # Remove all attendees other than ourselves
-    for component in replycal.subcomponents():
-        if component.name() == "VTIMEZONE":
-            continue
-        attendeeProp = component.getAttendeeProperty(cuas)
-        attendees = tuple(component.properties("ATTENDEE"))
-        for attendee in attendees:
-            if attendeeProp is None or (attendee.value() != attendeeProp.value()):
-                component.removeProperty(attendee)
-
-    returnValue((rsvp, replycal, accepted))
-
- at inlineCallbacks
-def writeReply(request, principal, replycal, ainbox):
-    """
-    Write an iTIP message reply into the specified Inbox.
-    
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param replycal: the L{Component} for the iTIP message reply.
-    @param ainbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    """
-    
-    # Get the Inbox of the ORGANIZER
-    organizer = replycal.getOrganizer()
-    assert organizer is not None
-    organizerPrincipal = ainbox.principalForCalendarUserAddress(organizer)
-    assert organizerPrincipal is not None
-    inboxURL = organizerPrincipal.scheduleInboxURL()
-    assert inboxURL
-    
-    # Check for local or partitioned organizer
-    if organizerPrincipal.locallyHosted():
-        # Determine whether current principal has CALDAV:schedule right on that Inbox
-        inbox = yield request.locateResource(inboxURL)
-    
-        try:
-            yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL())))
-        except AccessDeniedError:
-            log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
-            returnValue(None)
-        
-        # Now deposit the new calendar into the inbox
-        result = yield writeResource(request, inboxURL, inbox, None, replycal)
-    else:
-        # Send reply to Organizer
-
-        # This is a local CALDAV scheduling operation.
-        from twistedcaldav.scheduling.scheduler import CalDAVScheduler
-        scheduler = CalDAVScheduler(request, ainbox)
-
-        # Do the POST processing treating
-        yield scheduler.doSchedulingViaPUT(tuple(principal.calendarUserAddresses())[0], (organizer,), replycal, True)
-        result = None
-
-    if accountingEnabled("iTIP", organizerPrincipal):
-        emitAccounting(
-            "iTIP", organizerPrincipal,
-            "Originator: %s\nRecipients: %s\n\n%s"
-            % (principal.principalURL(), str(organizer), str(replycal))
-        )
-
-    returnValue(result)
-
- at inlineCallbacks
-def writeResource(request, collURL, collection, name, calendar):
-    """
-    Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
-    resource or by creating a new one.
-    
-    @param request: the L{IRequest} for the current request.
-    @param collURL: the C{str} containing the URL of the calendar collection.
-    @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
-    @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
-    @param calendar: the L{Component} calendar to write.
-    @return: C{tuple} of L{Deferred}, L{CalDAVFile}
-    """
-    
-    # Create a new name if one was not provided
-    if name is None:
-        name =  md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
-
-    # Get a resource for the new item
-    newchildURL = joinURL(collURL, name)
-    newchild = yield request.locateResource(newchildURL)
-    
-    # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
-    # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
-    # NB Only do this when writing to something other than an Inbox or Outbox
-    itipper = True
-    if collection.isCalendarCollection():
-        method = calendar.getProperty("METHOD")
-        if method:
-            calendar.removeProperty(method)
-        itipper = False
-    
-    # Now write it to the resource
-    try:
-        yield storeCalendarObjectResource(
-            request=request,
-            sourcecal = False,
-            destination = newchild,
-            destination_uri = newchildURL,
-            calendardata = str(calendar),
-            destinationparent = collection,
-            destinationcal = True,
-            isiTIP = itipper
-        )
-    except:
-        # FIXME: bare except
-        return
-    
-    returnValue(newchild)
-
-def newInboxResource(child, newchild):
-    """
-    Copy recipient and organizer properties from one iTIP resource, to another,
-    switching them as appropriate for a reply, and also set the state.
-    
-    @param child: the L{CalDAVFile} for the original iTIP message.
-    @param newchild: the L{CalDAVFile} for the iTIP message reply.
-    """
-    # Make previous Recipient the new Originator
-    if child.hasDeadProperty(caldavxml.Recipient):
-        recip = child.readDeadProperty(caldavxml.Recipient)
-        if recip.children:
-            # Store CALDAV:originator property
-            newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
-    
-    # Make previous Originator the new Recipient
-    if child.hasDeadProperty(caldavxml.Originator):
-        orig = child.readDeadProperty(caldavxml.Originator)
-        if orig.children:
-            # Store CALDAV:originator property
-            newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
-  
-def deleteResource(collection, name):
-    """
-    Delete the calendar resource in the specified calendar.
-    
-    @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
-    @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
-    @return: L{Deferred}
-    """
-    
-    delchild = collection.getChild(name)
-    index = collection.index()
-    index.deleteResource(delchild.fp.basename())
-    
-    def _deletedResourced(result):
-        # Change CTag on the parent calendar collection
-        return collection.updateCTag().addCallback(lambda _: result)
-
-    d = maybeDeferred(delete, "", delchild.fp, "0")
-    d.addCallback(_deletedResourced)
-    return d
-
-def canAutoRespond(calendar):
-    """
-    Check whether the METHOD of this iTIP calendar object is one we can process. Also,
-    we will only handle VEVENTs right now.
-
-    @param calendar: L{Component} for calendar to examine.
-    @return: C{True} if we can auto-respond, C{False} if not.
-    """
-
-    try:
-        method = calendar.propertyValue("METHOD")
-        if method not in ("REQUEST", "ADD", "CANCEL"):
-            return False
-        if calendar.mainType() not in ("VEVENT"):
-            return False
-    except ValueError:
-        return False
-    
-    return True
-
- at inlineCallbacks
-def processOthersInInbox(info, newinfo, inbox, child):
-    # Compare the new one with each existing one.
-    delete_child = False
-    for i in info:
-        # For any that are older, delete them.
-        if compareSyncInfo(i, newinfo) < 0:
-            try:
-                yield deleteResource(inbox, i[0])
-                log.info("Deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
-            except:
-                # FIXME: bare except
-                log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-                raise iTipException()
-        else:
-            # For any that are newer or the same, mark the new one to be deleted.
-            delete_child = True
-
-    # Delete the new one if so marked.
-    if delete_child:
-        try:
-            yield deleteResource(inbox, child.fp.basename())
-            log.info("Deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
-        except:
-            # FIXME: bare except
-            log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-            raise iTipException()
-    
-    returnValue(delete_child)
-
- at inlineCallbacks
-def findCalendarMatch(request, principal, calendar):
-    # Try and find a match to any components on existing calendars listed as contributing
-    # to free-busy as we will need to update those with the new one.
-    
-    # Find the current recipients calendar-free-busy-set
-    fbset = yield principal.calendarFreeBusyURIs(request)
-
-    # Find the first calendar in the list with a component matching the one we are processing
-    calmatch = None
-    updatecal = None
-    calURL = None
-    for calURL in fbset:
-        updatecal = yield request.locateResource(calURL)
-        if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
-            # We will ignore missing calendars. If the recipient has failed to
-            # properly manage the free busy set that should not prevent us from working.
-            continue
-        calmatch = matchComponentInCalendar(updatecal, calendar)
-        if calmatch:
-            log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
-            break
-    
-    if calmatch is None and len(fbset):
-        calURL = fbset[0]
-        updatecal = yield request.locateResource(calURL)
-
-    returnValue((calmatch, updatecal, calURL))
-
-def matchComponentInCalendar(collection, calendar):
-    """
-    See if the component in the provided iTIP calendar object matches any in the specified calendar
-    collection.
-    
-    @param collection: L{CalDAVFile} for the calendar collection to examine.
-    @param calendar: L{Component} for calendar to examine.
-    @return: C{list} of resource names found.
-    """
-
-    try:
-        # Extract UID from primary component (note we allow multiple components to be present
-        # because CANCEL requests can have multiple components).
-        comp = calendar.mainComponent(allow_multiple=True)
-        uid = comp.propertyValue("UID")
-        
-        # Now use calendar collection index to find all other resources with the same UID
-        index = collection.index()
-        result = index.resourceNamesForUID(uid)
-        
-        # There can be only one
-        if len(result) > 0: 
-            return result[0]
-        else:
-            return None
-    except ValueError:
-        return None
-
-def findMatchingComponent(component, calendar):
-    """
-    See if any overridden component in the provided iTIP calendar object matches the specified component.
-    
-    @param component: the component to try and match.
-    @type component: L{Component}
-    @param calendar: the calendar to find a match in.
-    @type calendar: L{Component}
-    @return: L{Component} for matching component,
-        or C{None} if not found.
-    """
-
-    # Extract RECURRENCE-ID value from component
-    rid = component.getRecurrenceIDUTC()
-    
-    # Return the one that matches in the calendar
-    return calendar.overriddenComponent(rid)
-
-def mergeComponents(newcal, oldcal):
-    """
-    Merge the overridden instance components in newcal into oldcal replacing any
-    matching components there.
-
-    @param newcal: the new overridden instances to use.
-    @type newcal: L{Component}
-    @param oldcal: the component to merge into.
-    @type oldcal: L{Component}
-    """
-    
-    # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
-    # use a TZID that is already specified in the old component set.
-
-    # We will update the SEQUENCE on the master to the highest value of the current one on the master
-    # or the ones in the components we are changing.
-
-    for component in newcal.subcomponents():
-        if component.name() == "VTIMEZONE":
-            continue
-        
-        rid = component.getRecurrenceIDUTC()
-        old_component = oldcal.overriddenComponent(rid)
-        if old_component:
-            oldcal.removeComponent(old_component)
-        oldcal.addComponent(component)
-
-def getAllInfo(collection, calendar, ignore):
-    """
-    Find each component in the calendar collection that has a matching UID with
-    the supplied component, and get useful synchronization details from it, ignoring
-    the one with the supplied resource name.
-
-    @param collection: the L{CalDAVFile} for the calendar collection.
-    @param calendar: the L{Component} for the component being compared with.
-    @param ignore: the C{str} containing the name of a resource to ignore,
-        or C{None} if none to ignore.
-    @return: C{list} of synchronization information for each resource found.
-    """
-    names = []
-    try:
-        # Extract UID from primary component (note we allow multiple components to be present
-        # because CANCEL requests can have multiple components).
-        comp = calendar.mainComponent(allow_multiple=True)
-        uid = comp.propertyValue("UID")
-        
-        # Now use calendar collection index to find all other resources with the same UID
-        index = collection.index()
-        names = index.resourceNamesForUID(uid)
-        
-        # Remove the one we want to ignore
-        if ignore is not None:
-            names = [name for name in names if name != ignore.fp.basename()]
-    except ValueError:
-        return []
-    
-    # Now get info for each name
-    result = []
-    for name in names:
-        cal = collection.iCalendar(name)
-        result.append(getSyncInfo(name, cal))
-
-    return result
-    
-def getSyncInfo(name, calendar):
-    """
-    Get property value details needed to synchronize iTIP components.
-    
-    @param calendar: L{Component} for calendar to check.
-    @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
-    """
-    try:
-        # Extract components from primary component (note we allow multiple components to be present
-        # because CANCEL requests can have multiple components).
-        comp = calendar.mainComponent(allow_multiple=True)
-        uid, seq, dtstamp, rid = getComponentSyncInfo(comp)
-        
-    except ValueError:
-        return (name, None, None, None, None)
-    
-    return (name, uid, seq, dtstamp, rid)
-
-def getComponentSyncInfo(component):
-    """
-    Get property value details needed to synchronize iTIP components.
-    
-    @param component: L{Component} to check.
-    @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
-    """
-    try:
-        # Extract items from component
-        uid = component.propertyValue("UID")
-        seq = component.propertyValue("SEQUENCE")
-        if seq:
-            seq = int(seq)
-        dtstamp = component.propertyValue("DTSTAMP")
-        rid = component.propertyValue("RECURRENCE-ID")
-        
-    except ValueError:
-        return (None, None, None, None)
-    
-    return (uid, seq, dtstamp, rid)
-
-def compareComponents(component1, component2):
-    """
-    Compare synchronization information for two components to see if they match according to iTIP.
-
-    @param component1: first component to check.
-    @type component1: L{Component}
-    @param component2: second component to check.
-    @type component2: L{Component}
-    
-    @return: 0, 1, -1 as per compareSyncInfo.
-    """
-    info1 = (None,) + getComponentSyncInfo(component1)
-    info2 = (None,) + getComponentSyncInfo(component2)
-    return compareSyncInfo(info1, info2)
-
-def compareSyncInfo(info1, info2):
-    """
-    Compare two synchronization information records.
-    
-    @param info1: a C{tuple} as returned by L{getSyncInfo}.
-    @param info2: a C{tuple} as returned by L{getSyncInfo}.
-    @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
-    """
-    # UIDs MUST match
-    assert info1[1] == info2[1]
-    
-    # Look for sequence
-    if (info1[2] is not None) and (info2[2] is not None):
-        if info1[2] > info2[2]:
-            return 1
-        if info1[2] < info2[2]:
-            return -1
-    elif (info1[2] is not None) and (info2[2] is None):
-        return 1
-    elif (info1[2] is None) and (info2[2] is not None):
-        return -1
-
-    # Look for DTSTAMP
-    if (info1[3] is not None) and (info2[3] is not None):
-        if info1[3] > info2[3]:
-            return 1
-        if info1[3] < info2[3]:
-            return -1
-    elif (info1[3] is not None) and (info2[3] is None):
-        return 1
-    elif (info1[3] is None) and (info2[3] is not None):
-        return -1
-
-    return 0
-
-class iTIPRequestStatus(object):
-    """
-    String constants for various iTIP status codes we use.
-    """
-    
-    MESSAGE_PENDING_CODE    = "1.0"
-    MESSAGE_SENT_CODE       = "1.1"
-    MESSAGE_DELIVERED_CODE  = "1.2"
-
-    MESSAGE_PENDING         = MESSAGE_PENDING_CODE + ";Scheduling message send is pending"
-    MESSAGE_SENT            = MESSAGE_SENT_CODE + ";Scheduling message has been sent"
-    MESSAGE_DELIVERED       = MESSAGE_DELIVERED_CODE + ";Scheduling message has been delivered"
-    
-    SUCCESS                 = "2.0;Success"
-
-    INVALID_CALENDAR_USER   = "3.7;Invalid Calendar User"
-    NO_AUTHORITY            = "3.8;No authority"
-
-    BAD_REQUEST             = "5.0;Service cannot handle request"
-    SERVICE_UNAVAILABLE     = "5.1;Service unavailable"
-    INVALID_SERVICE         = "5.2;Invalid calendar service"
-    NO_USER_SUPPORT         = "5.3;No scheduling support for user"

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/itip.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/itip.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/itip.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/itip.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,990 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+iTIP (RFC2446) processing.
+"""
+
+#
+# This is currently used for handling auto-replies to schedule requests arriving
+# in an inbox. It is called in a delayed fashion via reactor.callLater.
+#
+# We assume that all the components/calendars we deal with have been determined
+# as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
+# types, etc.
+#
+# The logic for component matching needs a lot more work as it currently does not
+# know how to deal with overridden instances.
+#
+
+import datetime
+import md5
+import time
+
+from twisted.python.failure import Failure
+from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
+from twisted.web2.dav import davxml
+from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
+from twisted.web2.dav.util import joinURL
+from twisted.web2.dav.fileop import delete
+from twisted.web2.dav.resource import AccessDeniedError
+
+from twistedcaldav import caldavxml
+from twistedcaldav.accounting import accountingEnabled, emitAccounting
+from twistedcaldav.log import Logger
+from twistedcaldav.ical import Property, iCalendarProductID
+from twistedcaldav.method import report_common
+from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.resource import isCalendarCollectionResource
+
+log = Logger()
+
+__version__ = "0.0"
+
+__all__ = [
+    "handleRequest",
+    "canAutoRespond",
+]
+
+class iTipException(Exception):
+    pass
+
+def handleRequest(request, principal, inbox, calendar, child):
+    """
+    Handle an iTIP response automatically.
+    @param request: the L{twisted.web2.server.Request} for the current request.
+    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
+    @param calendar: the L{Component} for the iTIP message we are processing.
+    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
+    @return: L{Deferred} that is a L{deferredGenerator}
+    """
+    
+    method = calendar.propertyValue("METHOD")
+    if method == "REQUEST":
+        f = processRequest
+    elif method == "ADD":
+        f = processAdd
+    elif method == "CANCEL":
+        f = processCancel
+
+    return f(request, principal, inbox, calendar, child)
+
+ at inlineCallbacks
+def processRequest(request, principal, inbox, calendar, child):
+    """
+    Process a METHOD=REQUEST.
+
+    Steps:
+    
+      1. See if this updates existing ones in Inbox.
+          1. If so,
+              1. Remove existing ones in Inbox.
+              2. See if this updates existing ones in free-busy-set calendars.
+              3. Remove existing ones in those calendars.
+              4. See if this fits into a free slot:
+                  1. If not, send REPLY with failure status
+                  2. If so
+                      1. send REPLY with success
+                      2. add to f-b-s calendar
+          2. If not,
+              1. remove the one we got - its 'stale'
+          3. Delete the request from the Inbox.
+    
+    @param request: the L{twisted.web2.server.Request} for the current request.
+    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
+    @param calendar: the L{Component} for the iTIP message we are processing.
+    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
+    """
+    
+    log.info("Auto-processing iTIP REQUEST for: %s" % (str(principal),))
+    processed = "ignored"
+
+    # First determine whether this is a full or partial update. A full update is one containing the master
+    # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
+    # being changed.
+    
+    new_master = calendar.masterComponent()
+
+    # Next we want to try and find a match to any components on existing calendars listed as contributing
+    # to free-busy as we will need to update those with the new one.
+    calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
+    
+    if new_master:
+        # So we have a full update. That means we need to delete any existing events completely and
+        # replace with the ones provided so long as the new one is newer.
+        
+        # If we have a match then we need to check whether we are updating etc
+        check_reply = False
+        if calmatch:
+            # See whether the new component is older than any existing ones and throw it away if so
+            newinfo = (None,) + getComponentSyncInfo(new_master)
+            cal = updatecal.iCalendar(calmatch)
+            old_master = cal.masterComponent()
+            if old_master:
+                info = getSyncInfo(calmatch, cal)
+            else:
+                info = None
+            if info is None or compareSyncInfo(info, newinfo) < 0:
+                # Existing resource is older and will be replaced
+                check_reply = True
+            else:
+                processed = "older"
+        else:
+            # We have a new request which we can reply to
+            check_reply = True
+            
+        if check_reply:
+            # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+            doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
+            
+            try:
+                if accepted:
+                    if calmatch:
+                        newchild = yield writeResource(request, calURL, updatecal, calmatch, calendar)
+                        log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+                    else:
+                        newchild = yield writeResource(request, calURL, updatecal, None, calendar)
+                        log.info("Added new calendar component in %s." % (calURL,))
+                else:
+                    if calmatch:
+                        yield deleteResource(updatecal, calmatch)
+                        log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
+                        
+                # Send a reply if needed. 
+                if doreply:
+                    log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
+                    newchild = yield writeReply(request, principal, replycal, inbox)
+                    if newchild:
+                        newInboxResource(child, newchild)
+                processed = "processed"
+            except:
+                # FIXME: bare except
+                log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+                raise iTipException()
+            
+    else:
+        # So we have a partial update. That means we have to do partial updates to instances in
+        # the existing calendar component.
+
+        # If we have a match then we need to check whether we are updating etc
+        check_reply = False
+        if calmatch:
+            # Check each component to see whether its new
+            cal = updatecal.iCalendar(calmatch)
+            old_master = cal.masterComponent()
+            processed = "older"
+            new_components = [component for component in calendar.subcomponents()]
+            for component in new_components:
+                if component.name() == "VTIMEZONE":
+                    continue
+                
+                newinfo = (None,) + getComponentSyncInfo(component)
+                old_component = findMatchingComponent(component, cal)
+                if old_component:
+                    info = (None,) + getComponentSyncInfo(old_component)
+                elif old_master:
+                    info = (None,) + getComponentSyncInfo(old_master)
+                else:
+                    info = None
+                    
+                if info is None or compareSyncInfo(info, newinfo) < 0:
+                    # Existing resource is older and will be replaced
+                    check_reply = True
+                    processed = "processed"
+                else:
+                    calendar.removeComponent(component)
+        else:
+            # We have a new request which we can reply to
+            check_reply = True
+
+        if check_reply:
+            # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+            doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
+            
+            try:
+                if calmatch:
+                    # Merge the new instances with the old ones
+                    mergeComponents(calendar, cal)
+                    newchild = yield writeResource(request, calURL, updatecal, calmatch, cal)
+                    log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+                else:
+                    if accepted:
+                        newchild = yield writeResource(request, calURL, updatecal, None, calendar)
+                        log.info("Added new calendar component in %s." % (calURL,))
+                        
+                # Do reply if needed. 
+                if doreply:
+                    log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
+                    newchild = yield writeReply(request, principal, replycal, inbox)
+                    if newchild:
+                        newInboxResource(child, newchild)
+                    
+                processed = "processed"
+            except:
+                # FIXME: bare except
+                log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+                raise iTipException()
+
+    # Remove the now processed incoming request.
+    try:
+        yield deleteResource(inbox, child.fp.basename())
+        log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
+                   child.fp.basename(),
+                   {
+                     "processed": "processed",
+                     "older"    : "ignored: older",
+                     "ignored"  : "ignored: no match"
+                   }[processed]
+                ))
+    except:
+        # FIXME: bare except
+        log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+        raise iTipException()
+
+def processAdd(request, principal, inbox, calendar, child):
+    """
+    Process a METHOD=ADD.
+
+    @param request: the L{twisted.web2.server.Request} for the current request.
+    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
+    @param calendar: the L{Component} for the iTIP message we are processing.
+    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
+    """
+    log.info("Auto-processing iTIP ADD for: %s" % (str(principal),))
+    raise NotImplementedError()
+
+ at inlineCallbacks
+def processCancel(request, principal, inbox, calendar, child):
+    """
+    Process a METHOD=CANCEL.
+
+    Policy find all components that match UID, SEQ and R-ID and remove them.
+
+    Steps:
+    
+      1. See if this updates existing ones in Inbox.
+      2. Remove existing ones in Inbox.
+      3. See if this updates existing ones in free-busy-set calendars.
+      4. Remove existing ones in those calendars.
+      5. Remove the incoming request.
+
+    NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
+    instance may be cancelled. What we need to do for this is:
+    
+      1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
+         delete the old one.
+      2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
+         leave the new and existing ones in the Inbox.
+      3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
+         remove the overridden component from the f-b-s resource.
+      4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
+    
+    TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
+    
+    @param request: the L{twisted.web2.server.Request} for the current request.
+    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
+    @param calendar: the L{Component} for the iTIP message we are processing.
+    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
+    """
+    
+    log.info("Auto-processing iTIP CANCEL for: %s" % (str(principal),))
+    processed = "ignored"
+
+    # Get all component info for this iTIP message
+    newinfo = getSyncInfo(child.fp.basename(), calendar)
+    info = getAllInfo(inbox, calendar, child)
+
+    # First see if we have a recurrence id which will force extra work
+    has_rid = False
+    if newinfo[4] is not None:
+        has_rid = True
+    else:
+        for i in info:
+            if i[4] is not None:
+                has_rid = True
+                break
+            
+    if not has_rid:
+        # Compare the new one with each existing one.
+        delete_child = yield processOthersInInbox(info, newinfo, inbox, child)
+        if delete_child:
+            return
+
+        # Next we want to try and find a match to any components on existing calendars listed as contributing
+        # to free-busy as we will need to update those with the new one.
+        calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
+        
+        # If we have a match then we need to check whether we are updating etc
+        if calmatch:
+            # See whether the current component is older than any existing ones and throw it away if so
+            cal = updatecal.iCalendar(calmatch)
+            info = getSyncInfo(calmatch, cal)
+            if compareSyncInfo(info, newinfo) < 0:
+                # Delete existing resource which has been cancelled
+                try:
+                    yield deleteResource(updatecal, calmatch)
+                    log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
+                except:
+                    # FIXME: bare except
+                    log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+                    raise iTipException()
+                processed = "processed"
+            else:
+                processed = "older"
+        else:
+            # Nothing to do except delete the inbox item as we have nothing to cancel.
+            processed = "ignored"
+    else:
+        # Try and find a match to any components on existing calendars listed as contributing
+        # to free-busy as we will need to update those with the new one.
+        calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
+        
+        # If we have a match then we need to check whether we are updating etc
+        if calmatch:
+            # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
+            # So we need to iterate over each iTIP component.
+
+            # Get the existing calendar object
+            existing_calendar = updatecal.iCalendar(calmatch)
+            existing_master = existing_calendar.masterComponent()
+            exdates = []
+
+            for component in calendar.subcomponents():
+                if component.name() == "VTIMEZONE":
+                    continue
+            
+                # Find matching component in existing calendar
+                old_component = findMatchingComponent(component, existing_calendar)
+                
+                if old_component:
+                    # We are cancelling an overridden component, so we need to check the
+                    # SEQUENCE/DTSAMP with the master.
+                    if compareComponents(old_component, component) < 0:
+                        # Exclude the cancelled instance
+                        exdates.append(component.getRecurrenceIDUTC())
+                        
+                        # Remove the existing component.
+                        existing_calendar.removeComponent(old_component)
+                elif existing_master:
+                    # We are trying to CANCEL a non-overridden instance, so we need to
+                    # check SEQUENCE/DTSTAMP with the master.
+                    if compareComponents(existing_master, component) < 0:
+                        # Exclude the cancelled instance
+                        exdates.append(component.getRecurrenceIDUTC())
+
+            # If we have any EXDATEs lets add them to the existing calendar object and write
+            # it back.
+            if exdates:
+                if existing_master:
+                    existing_master.addProperty(Property("EXDATE", exdates))
+
+                # See if there are still components in the calendar - we might have deleted the last overridden instance
+                # in which case the calendar object is empty (except for VTIMEZONEs).
+                if existing_calendar.mainType() is None:
+                    # Delete the now empty calendar object
+                    yield deleteResource(updatecal, calmatch)
+                    log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
+                else:
+                    # Update the existing calendar object
+                    yield writeResource(request, calURL, updatecal, calmatch, existing_calendar)
+                    log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
+                processed = "processed"
+            else:
+                processed = "older"
+        else:
+            # Nothing to do except delete the inbox item as we have nothing to cancel.
+            processed = "ignored"
+
+    # Remove the now processed incoming request.
+    try:
+        yield deleteResource(inbox, child.fp.basename())
+        log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
+                  child.fp.basename(),
+                  {
+                    "processed": "processed",
+                    "older"    : "ignored: older",
+                    "ignored"  : "ignored: no match"
+                  }[processed]
+                ))
+    except:
+        # FIXME: bare except
+        log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+        raise iTipException()
+
+ at inlineCallbacks
+def checkForReply(request, principal, calendar):
+    """
+    Check whether a reply to the given iTIP message is needed. We will not process a reply
+    if RSVP=FALSE. A reply will either be positive (accepted
+    invitation) or negative (denied invitation). In addition we will modify calendar to reflect
+    any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
+    
+    BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
+    At the moment we will treat a failure on one instances as a DECLINE of the entire set.
+
+    @param request: the L{twisted.web2.server.Request} for the current request.
+    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+    @param calendar: the L{Component} for the iTIP message we are processing.
+    @return: C{True} if a reply is needed, C{False} otherwise.
+    """
+    
+    # We need to figure out whether the specified component will clash with any others in the f-b-set calendars
+    accepted = True
+        
+    # First expand current one to get instances (only go 1 year into the future)
+    default_future_expansion_duration = datetime.timedelta(days=356*1)
+    expand_max = datetime.date.today() + default_future_expansion_duration
+    instances = calendar.expandTimeRanges(expand_max)
+    
+    # Extract UID from primary component as we want to ignore this one if we match it
+    # in any calendars.
+    comp = calendar.mainComponent(allow_multiple=True)
+    uid = comp.propertyValue("UID")
+
+    # Now compare each instance time-range with the index and see if there is an overlap
+    fbset = yield principal.calendarFreeBusyURIs(request)
+
+    for calURL in fbset:
+        testcal = yield request.locateResource(calURL)
+        
+        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+        fbinfo = ([], [], [])
+        
+        # Now do search for overlapping time-range
+        for instance in instances.instances.itervalues():
+            try:
+                tr = caldavxml.TimeRange(start="20000101", end="20000101")
+                tr.start = instance.start
+                tr.end = instance.end
+                yield report_common.generateFreeBusyInfo(request, testcal, fbinfo, tr, 0, uid)
+                
+                # If any fbinfo entries exist we have an overlap
+                if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
+                    accepted = False
+                    break
+            except NumberOfMatchesWithinLimits:
+                accepted = False
+                log.info("Exceeded number of matches whilst trying to find free-time.")
+                break
+            
+        if not accepted:
+            break
+     
+    # Extract the ATTENDEE property matching current recipient from the calendar data
+    cuas = principal.calendarUserAddresses()
+    attendeeProps = calendar.getAttendeeProperties(cuas)
+    if not attendeeProps:
+        returnValue((False, None, accepted))
+
+    # Look for specific parameters
+    rsvp = True
+    for attendeeProp in attendeeProps:
+        if "RSVP" in attendeeProp.params():
+            if attendeeProp.params()["RSVP"][0] == "FALSE":
+                rsvp = False
+    
+            # Now modify the original component
+            del attendeeProp.params()["RSVP"]
+
+    if accepted:
+        partstat = "ACCEPTED"
+    else:
+        partstat = "DECLINED"
+    for attendeeProp in attendeeProps:
+        if "PARTSTAT" in attendeeProp.params():
+            attendeeProp.params()["PARTSTAT"][0] = partstat
+        else:
+            attendeeProp.params()["PARTSTAT"] = [partstat]
+    
+    # Now create a new calendar object for the reply
+    
+    # First get useful props from the original
+    replycal = calendar.duplicate()
+    
+    # Change METHOD
+    replycal.getProperty("METHOD").setValue("REPLY")
+    
+    # Change PRODID to this server
+    replycal.getProperty("PRODID").setValue(iCalendarProductID)
+    
+    # Add REQUEST-STATUS
+    for component in replycal.subcomponents():
+        if accepted:
+            component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
+        else:
+            component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
+
+    # Remove all attendees other than ourselves
+    for component in replycal.subcomponents():
+        if component.name() == "VTIMEZONE":
+            continue
+        attendeeProp = component.getAttendeeProperty(cuas)
+        attendees = tuple(component.properties("ATTENDEE"))
+        for attendee in attendees:
+            if attendeeProp is None or (attendee.value() != attendeeProp.value()):
+                component.removeProperty(attendee)
+
+    returnValue((rsvp, replycal, accepted))
+
+ at inlineCallbacks
+def writeReply(request, principal, replycal, ainbox):
+    """
+    Write an iTIP message reply into the specified Inbox.
+    
+    @param request: the L{twisted.web2.server.Request} for the current request.
+    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+    @param replycal: the L{Component} for the iTIP message reply.
+    @param ainbox: the L{ScheduleInboxFile} for the principal's Inbox.
+    """
+    
+    # Get the Inbox of the ORGANIZER
+    organizer = replycal.getOrganizer()
+    assert organizer is not None
+    organizerPrincipal = ainbox.principalForCalendarUserAddress(organizer)
+    assert organizerPrincipal is not None
+    inboxURL = organizerPrincipal.scheduleInboxURL()
+    assert inboxURL
+    
+    # Check for local or partitioned organizer
+    if organizerPrincipal.locallyHosted():
+        # Determine whether current principal has CALDAV:schedule right on that Inbox
+        inbox = yield request.locateResource(inboxURL)
+    
+        try:
+            yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL())))
+        except AccessDeniedError:
+            log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
+            returnValue(None)
+        
+        # Now deposit the new calendar into the inbox
+        result = yield writeResource(request, inboxURL, inbox, None, replycal)
+    else:
+        # Send reply to Organizer
+
+        # This is a local CALDAV scheduling operation.
+        from twistedcaldav.scheduling.scheduler import CalDAVScheduler
+        scheduler = CalDAVScheduler(request, ainbox)
+
+        # Do the POST processing treating
+        yield scheduler.doSchedulingViaPUT(tuple(principal.calendarUserAddresses())[0], (organizer,), replycal, True)
+        result = None
+
+    if accountingEnabled("iTIP", organizerPrincipal):
+        emitAccounting(
+            "iTIP", organizerPrincipal,
+            "Originator: %s\nRecipients: %s\n\n%s"
+            % (principal.principalURL(), str(organizer), str(replycal))
+        )
+
+    returnValue(result)
+
+ at inlineCallbacks
+def writeResource(request, collURL, collection, name, calendar):
+    """
+    Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
+    resource or by creating a new one.
+    
+    @param request: the L{IRequest} for the current request.
+    @param collURL: the C{str} containing the URL of the calendar collection.
+    @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
+    @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
+    @param calendar: the L{Component} calendar to write.
+    @return: C{tuple} of L{Deferred}, L{CalDAVFile}
+    """
+    
+    # Create a new name if one was not provided
+    if name is None:
+        name =  md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
+
+    # Get a resource for the new item
+    newchildURL = joinURL(collURL, name)
+    newchild = yield request.locateResource(newchildURL)
+    
+    # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
+    # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
+    # NB Only do this when writing to something other than an Inbox or Outbox
+    itipper = True
+    if collection.isCalendarCollection():
+        method = calendar.getProperty("METHOD")
+        if method:
+            calendar.removeProperty(method)
+        itipper = False
+    
+    # Now write it to the resource
+    try:
+        yield storeCalendarObjectResource(
+            request=request,
+            sourcecal = False,
+            destination = newchild,
+            destination_uri = newchildURL,
+            calendardata = str(calendar),
+            destinationparent = collection,
+            destinationcal = True,
+            isiTIP = itipper
+        )
+    except:
+        # FIXME: bare except
+        return
+    
+    returnValue(newchild)
+
+def newInboxResource(child, newchild):
+    """
+    Copy recipient and organizer properties from one iTIP resource, to another,
+    switching them as appropriate for a reply, and also set the state.
+    
+    @param child: the L{CalDAVFile} for the original iTIP message.
+    @param newchild: the L{CalDAVFile} for the iTIP message reply.
+    """
+    # Make previous Recipient the new Originator
+    if child.hasDeadProperty(caldavxml.Recipient):
+        recip = child.readDeadProperty(caldavxml.Recipient)
+        if recip.children:
+            # Store CALDAV:originator property
+            newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
+    
+    # Make previous Originator the new Recipient
+    if child.hasDeadProperty(caldavxml.Originator):
+        orig = child.readDeadProperty(caldavxml.Originator)
+        if orig.children:
+            # Store CALDAV:originator property
+            newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
+  
+def deleteResource(collection, name):
+    """
+    Delete the calendar resource in the specified calendar.
+    
+    @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
+    @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
+    @return: L{Deferred}
+    """
+    
+    delchild = collection.getChild(name)
+    index = collection.index()
+    index.deleteResource(delchild.fp.basename())
+    
+    def _deletedResourced(result):
+        # Change CTag on the parent calendar collection
+        return collection.updateCTag().addCallback(lambda _: result)
+
+    d = maybeDeferred(delete, "", delchild.fp, "0")
+    d.addCallback(_deletedResourced)
+    return d
+
+def canAutoRespond(calendar):
+    """
+    Check whether the METHOD of this iTIP calendar object is one we can process. Also,
+    we will only handle VEVENTs right now.
+
+    @param calendar: L{Component} for calendar to examine.
+    @return: C{True} if we can auto-respond, C{False} if not.
+    """
+
+    try:
+        method = calendar.propertyValue("METHOD")
+        if method not in ("REQUEST", "ADD", "CANCEL"):
+            return False
+        if calendar.mainType() not in ("VEVENT"):
+            return False
+    except ValueError:
+        return False
+    
+    return True
+
+ at inlineCallbacks
+def processOthersInInbox(info, newinfo, inbox, child):
+    # Compare the new one with each existing one.
+    delete_child = False
+    for i in info:
+        # For any that are older, delete them.
+        if compareSyncInfo(i, newinfo) < 0:
+            try:
+                yield deleteResource(inbox, i[0])
+                log.info("Deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
+            except:
+                # FIXME: bare except
+                log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+                raise iTipException()
+        else:
+            # For any that are newer or the same, mark the new one to be deleted.
+            delete_child = True
+
+    # Delete the new one if so marked.
+    if delete_child:
+        try:
+            yield deleteResource(inbox, child.fp.basename())
+            log.info("Deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
+        except:
+            # FIXME: bare except
+            log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+            raise iTipException()
+    
+    returnValue(delete_child)
+
+ at inlineCallbacks
+def findCalendarMatch(request, principal, calendar):
+    # Try and find a match to any components on existing calendars listed as contributing
+    # to free-busy as we will need to update those with the new one.
+    
+    # Find the current recipients calendar-free-busy-set
+    fbset = yield principal.calendarFreeBusyURIs(request)
+
+    # Find the first calendar in the list with a component matching the one we are processing
+    calmatch = None
+    updatecal = None
+    calURL = None
+    for calURL in fbset:
+        updatecal = yield request.locateResource(calURL)
+        if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
+            # We will ignore missing calendars. If the recipient has failed to
+            # properly manage the free busy set that should not prevent us from working.
+            continue
+        calmatch = matchComponentInCalendar(updatecal, calendar)
+        if calmatch:
+            log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
+            break
+    
+    if calmatch is None and len(fbset):
+        calURL = fbset[0]
+        updatecal = yield request.locateResource(calURL)
+
+    returnValue((calmatch, updatecal, calURL))
+
+def matchComponentInCalendar(collection, calendar):
+    """
+    See if the component in the provided iTIP calendar object matches any in the specified calendar
+    collection.
+    
+    @param collection: L{CalDAVFile} for the calendar collection to examine.
+    @param calendar: L{Component} for calendar to examine.
+    @return: C{list} of resource names found.
+    """
+
+    try:
+        # Extract UID from primary component (note we allow multiple components to be present
+        # because CANCEL requests can have multiple components).
+        comp = calendar.mainComponent(allow_multiple=True)
+        uid = comp.propertyValue("UID")
+        
+        # Now use calendar collection index to find all other resources with the same UID
+        index = collection.index()
+        result = index.resourceNamesForUID(uid)
+        
+        # There can be only one
+        if len(result) > 0: 
+            return result[0]
+        else:
+            return None
+    except ValueError:
+        return None
+
+def findMatchingComponent(component, calendar):
+    """
+    See if any overridden component in the provided iTIP calendar object matches the specified component.
+    
+    @param component: the component to try and match.
+    @type component: L{Component}
+    @param calendar: the calendar to find a match in.
+    @type calendar: L{Component}
+    @return: L{Component} for matching component,
+        or C{None} if not found.
+    """
+
+    # Extract RECURRENCE-ID value from component
+    rid = component.getRecurrenceIDUTC()
+    
+    # Return the one that matches in the calendar
+    return calendar.overriddenComponent(rid)
+
+def mergeComponents(newcal, oldcal):
+    """
+    Merge the overridden instance components in newcal into oldcal replacing any
+    matching components there.
+
+    @param newcal: the new overridden instances to use.
+    @type newcal: L{Component}
+    @param oldcal: the component to merge into.
+    @type oldcal: L{Component}
+    """
+    
+    # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
+    # use a TZID that is already specified in the old component set.
+
+    # We will update the SEQUENCE on the master to the highest value of the current one on the master
+    # or the ones in the components we are changing.
+
+    for component in newcal.subcomponents():
+        if component.name() == "VTIMEZONE":
+            continue
+        
+        rid = component.getRecurrenceIDUTC()
+        old_component = oldcal.overriddenComponent(rid)
+        if old_component:
+            oldcal.removeComponent(old_component)
+        oldcal.addComponent(component)
+
+def getAllInfo(collection, calendar, ignore):
+    """
+    Find each component in the calendar collection that has a matching UID with
+    the supplied component, and get useful synchronization details from it, ignoring
+    the one with the supplied resource name.
+
+    @param collection: the L{CalDAVFile} for the calendar collection.
+    @param calendar: the L{Component} for the component being compared with.
+    @param ignore: the C{str} containing the name of a resource to ignore,
+        or C{None} if none to ignore.
+    @return: C{list} of synchronization information for each resource found.
+    """
+    names = []
+    try:
+        # Extract UID from primary component (note we allow multiple components to be present
+        # because CANCEL requests can have multiple components).
+        comp = calendar.mainComponent(allow_multiple=True)
+        uid = comp.propertyValue("UID")
+        
+        # Now use calendar collection index to find all other resources with the same UID
+        index = collection.index()
+        names = index.resourceNamesForUID(uid)
+        
+        # Remove the one we want to ignore
+        if ignore is not None:
+            names = [name for name in names if name != ignore.fp.basename()]
+    except ValueError:
+        return []
+    
+    # Now get info for each name
+    result = []
+    for name in names:
+        cal = collection.iCalendar(name)
+        result.append(getSyncInfo(name, cal))
+
+    return result
+    
+def getSyncInfo(name, calendar):
+    """
+    Get property value details needed to synchronize iTIP components.
+    
+    @param calendar: L{Component} for calendar to check.
+    @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+    """
+    try:
+        # Extract components from primary component (note we allow multiple components to be present
+        # because CANCEL requests can have multiple components).
+        comp = calendar.mainComponent(allow_multiple=True)
+        uid, seq, dtstamp, rid = getComponentSyncInfo(comp)
+        
+    except ValueError:
+        return (name, None, None, None, None)
+    
+    return (name, uid, seq, dtstamp, rid)
+
+def getComponentSyncInfo(component):
+    """
+    Get property value details needed to synchronize iTIP components.
+    
+    @param component: L{Component} to check.
+    @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+    """
+    try:
+        # Extract items from component
+        uid = component.propertyValue("UID")
+        seq = component.propertyValue("SEQUENCE")
+        if seq:
+            seq = int(seq)
+        dtstamp = component.propertyValue("DTSTAMP")
+        rid = component.propertyValue("RECURRENCE-ID")
+        
+    except ValueError:
+        return (None, None, None, None)
+    
+    return (uid, seq, dtstamp, rid)
+
+def compareComponents(component1, component2):
+    """
+    Compare synchronization information for two components to see if they match according to iTIP.
+
+    @param component1: first component to check.
+    @type component1: L{Component}
+    @param component2: second component to check.
+    @type component2: L{Component}
+    
+    @return: 0, 1, -1 as per compareSyncInfo.
+    """
+    info1 = (None,) + getComponentSyncInfo(component1)
+    info2 = (None,) + getComponentSyncInfo(component2)
+    return compareSyncInfo(info1, info2)
+
+def compareSyncInfo(info1, info2):
+    """
+    Compare two synchronization information records.
+    
+    @param info1: a C{tuple} as returned by L{getSyncInfo}.
+    @param info2: a C{tuple} as returned by L{getSyncInfo}.
+    @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
+    """
+    # UIDs MUST match
+    assert info1[1] == info2[1]
+    
+    # Look for sequence
+    if (info1[2] is not None) and (info2[2] is not None):
+        if info1[2] > info2[2]:
+            return 1
+        if info1[2] < info2[2]:
+            return -1
+    elif (info1[2] is not None) and (info2[2] is None):
+        return 1
+    elif (info1[2] is None) and (info2[2] is not None):
+        return -1
+
+    # Look for DTSTAMP
+    if (info1[3] is not None) and (info2[3] is not None):
+        if info1[3] > info2[3]:
+            return 1
+        if info1[3] < info2[3]:
+            return -1
+    elif (info1[3] is not None) and (info2[3] is None):
+        return 1
+    elif (info1[3] is None) and (info2[3] is not None):
+        return -1
+
+    return 0
+
+class iTIPRequestStatus(object):
+    """
+    String constants for various iTIP status codes we use.
+    """
+    
+    MESSAGE_PENDING_CODE    = "1.0"
+    MESSAGE_SENT_CODE       = "1.1"
+    MESSAGE_DELIVERED_CODE  = "1.2"
+
+    MESSAGE_PENDING         = MESSAGE_PENDING_CODE + ";Scheduling message send is pending"
+    MESSAGE_SENT            = MESSAGE_SENT_CODE + ";Scheduling message has been sent"
+    MESSAGE_DELIVERED       = MESSAGE_DELIVERED_CODE + ";Scheduling message has been delivered"
+    
+    SUCCESS                 = "2.0;Success"
+
+    INVALID_CALENDAR_USER   = "3.7;Invalid Calendar User"
+    NO_AUTHORITY            = "3.8;No authority"
+
+    BAD_REQUEST             = "5.0;Service cannot handle request"
+    SERVICE_UNAVAILABLE     = "5.1;Service unavailable"
+    INVALID_SERVICE         = "5.2;Invalid calendar service"
+    NO_USER_SUPPORT         = "5.3;No scheduling support for user"

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/scheduler.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,921 +0,0 @@
-##
-# Copyright (c) 2005-2008 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-from twisted.python.failure import Failure
-
-from twisted.web2 import responsecode
-from twisted.web2.dav import davxml
-from twisted.web2.dav.http import errorForFailure, messageForFailure, statusForFailure
-from twisted.web2.http import HTTPError, Response
-from twisted.web2.http_headers import MimeType
-
-from twistedcaldav import caldavxml
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.caldavxml import caldav_namespace, TimeRange
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.extensions import ErrorResponse
-from twistedcaldav.ical import Component
-from twistedcaldav.log import Logger, LoggingMixIn
-from twistedcaldav.scheduling import addressmapping
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
-from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
-    LocalCalendarUser, RemoteCalendarUser, PartitionedCalendarUser
-from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-
-import itertools
-import re
-import socket
-import urlparse
-
-"""
-CalDAV/Server-to-Server scheduling behavior.
-"""
-
-__all__ = [
-    "Scheduler",
-    "CalDAVScheduler",
-    "IScheduleScheduler",
-    "IMIPScheduler",
-    "DirectScheduler",
-]
-
-
-log = Logger()
-
-class Scheduler(object):
-    
-    def __init__(self, request, resource):
-        self.request = request
-        self.resource = resource
-        self.originator = None
-        self.recipients = None
-        self.calendar = None
-        self.calendardata = None
-        self.organizer = None
-        self.attendee = None
-        self.isiTIPRequest = None
-        self.timeRange = None
-        self.excludeUID = None
-        self.fakeTheResult = False
-        self.method = "Unknown"
-        self.internal_request = False
-    
-    @inlineCallbacks
-    def doSchedulingViaPOST(self, use_request_headers=False):
-        """
-        The Scheduling POST operation on an Outbox.
-        """
-    
-        self.method = "POST"
-
-        # Load various useful bits doing some basic checks on those
-        yield self.loadCalendarFromRequest()
-        
-        if use_request_headers:
-            self.loadFromRequestHeaders()
-        else:
-            yield self.loadFromRequestData()
-
-        if not hasattr(self.request, "extendedLogItems"):
-            self.request.extendedLogItems = {}
-        self.request.extendedLogItems["recipients"] = len(self.recipients)
-        self.request.extendedLogItems["cl"] = str(len(self.calendardata))
-    
-        # Do some extra authorization checks
-        self.checkAuthorization()
-
-        result = (yield self.doScheduling())
-        returnValue(result)
-
-    def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
-        """
-        The implicit scheduling PUT operation.
-        """
-    
-        self.method = "PUT"
-
-        # Load various useful bits doing some basic checks on those
-        self.originator = originator
-        self.recipients = recipients
-        self.calendar = calendar
-        self.internal_request = internal_request
-
-        # Do some extra authorization checks
-        self.checkAuthorization()
-
-        return self.doScheduling()
-
-    @inlineCallbacks
-    def doScheduling(self):
-        # Check validity of Originator header.
-        yield self.checkOriginator()
-    
-        # Get recipient details.
-        yield self.checkRecipients()
-    
-        # Check calendar data.
-        self.checkCalendarData()
-    
-        # Check validity of ORGANIZER
-        yield self.checkOrganizer()
-    
-        # Do security checks (e.g. spoofing)
-        yield self.securityChecks()
-    
-        # Generate accounting information
-        self.doAccounting()
-
-        # Do some final checks after we have gathered all our information
-        self.finalChecks()
-
-        # Do scheduling tasks
-        result = (yield self.generateSchedulingResponse())
-
-        returnValue(result)
-
-    @inlineCallbacks
-    def loadFromRequestData(self):
-        yield self.loadOriginatorFromRequestDetails()
-        self.loadRecipientsFromCalendarData()
-        
-    @inlineCallbacks
-    def loadOriginatorFromRequestDetails(self):
-        # Get the originator who is the authenticated user
-        originatorPrincipal = None
-        originator = ""
-        authz_principal = self.resource.currentPrincipal(self.request).children[0]
-        if isinstance(authz_principal, davxml.HRef):
-            originatorPrincipalURL = str(authz_principal)
-            if originatorPrincipalURL:
-                originatorPrincipal = (yield self.request.locateResource(originatorPrincipalURL))
-                if originatorPrincipal:
-                    # Pick the first mailto cu address or the first other type
-                    for item in originatorPrincipal.calendarUserAddresses():
-                        if not originator:
-                            originator = item
-                        if item.startswith("mailto:"):
-                            originator = item
-                            break
-
-        if not originator:
-            log.err("%s request must have Originator" % (self.method,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
-        else:
-            self.originator = originator
-
-    def loadRecipientsFromCalendarData(self):
-
-        # Get the ATTENDEEs
-        attendees = set()
-        for attendee, _ignore in self.calendar.getAttendeesByInstance():
-            attendees.add(attendee)
-        
-        if not attendees:
-            log.err("%s request must have at least one Recipient" % (self.method,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
-        else:
-            self.recipients = list(attendees)
-
-    def loadFromRequestHeaders(self):
-        """
-        Load Originator and Recipient from request headers.
-        """
-        self.loadOriginatorFromRequestHeaders()
-        self.loadRecipientsFromRequestHeaders()
-
-    def loadOriginatorFromRequestHeaders(self):
-        # Must have Originator header
-        originator = self.request.headers.getRawHeaders("originator")
-        if originator is None or (len(originator) != 1):
-            log.err("%s request must have Originator header" % (self.method,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
-        else:
-            self.originator = originator[0]
-    
-    def loadRecipientsFromRequestHeaders(self):
-        # Get list of Recipient headers
-        rawRecipients = self.request.headers.getRawHeaders("recipient")
-        if rawRecipients is None or (len(rawRecipients) == 0):
-            log.err("%s request must have at least one Recipient header" % (self.method,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
-    
-        # Recipient header may be comma separated list
-        self.recipients = []
-        for rawRecipient in rawRecipients:
-            for r in rawRecipient.split(","):
-                r = r.strip()
-                if len(r):
-                    self.recipients.append(r)
-        
-    @inlineCallbacks
-    def loadCalendarFromRequest(self):
-        # Must be content-type text/calendar
-        contentType = self.request.headers.getHeader("content-type")
-        if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
-            log.err("MIME type %s not allowed in calendar collection" % (contentType,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-    
-        # Parse the calendar object from the HTTP request stream
-        try:
-            self.calendar = (yield Component.fromIStream(self.request.stream))
-            self.calendardata = str(self.calendar)
-        except:
-            # FIXME: Bare except
-            log.err("Error while handling %s: %s" % (self.method, Failure(),))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Can't parse calendar data"))
-
-    def checkAuthorization(self):
-        raise NotImplementedError
-
-    def checkOriginator(self):
-        raise NotImplementedError
-
-    def checkRecipients(self):
-        raise NotImplementedError
-
-    def checkOrganizer(self):
-        raise NotImplementedError
-
-    def checkOrganizerAsOriginator(self):
-        raise NotImplementedError
-
-    def checkAttendeeAsOriginator(self):
-        raise NotImplementedError
-
-    def checkCalendarData(self):
-        # Must be a valid calendar
-        try:
-            self.calendar.validCalendarForCalDAV()
-        except ValueError, e:
-            log.err("%s request calendar component is not valid:%s %s" % (self.method, e, self.calendardata,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Calendar component is not valid"))
-    
-        # Must have a METHOD
-        if not self.calendar.isValidMethod():
-            log.err("%s request must have valid METHOD property in calendar component: %s" % (self.method, self.calendardata,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Must have valid METHOD property"))
-        
-        # Verify iTIP behavior
-        if not self.calendar.isValidITIP():
-            log.err("%s request must have a calendar component that satisfies iTIP requirements: %s" % (self.method, self.calendardata,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Must have a calendar component that satisfies iTIP requirements"))
-
-        # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
-        if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
-            log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component %s request: %s" % (self.method, self.calendardata,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
-
-        # Determine iTIP method mode
-        if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
-            self.isiTIPRequest = True
-
-        elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
-            self.isiTIPRequest = False
-
-            # Verify that there is a single ATTENDEE property
-            attendees = self.calendar.getAttendees()
-        
-            # Must have only one
-            if len(attendees) != 1:
-                log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendardata,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-            self.attendee = attendees[0]
-
-        else:
-            msg = "Unknown iTIP METHOD: %s" % (self.calendar.propertyValue("METHOD"),)
-            log.err(msg)
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description=msg))
-
-    def checkForFreeBusy(self):
-        if not hasattr(self, "isfreebusy"):
-            if (self.calendar.propertyValue("METHOD") == "REQUEST") and (self.calendar.mainType() == "VFREEBUSY"):
-                # Extract time range from VFREEBUSY object
-                vfreebusies = [v for v in self.calendar.subcomponents() if v.name() == "VFREEBUSY"]
-                if len(vfreebusies) != 1:
-                    log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (self.calendardata,))
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="iTIP data is not valid for a VFREEBUSY request"))
-                dtstart = vfreebusies[0].getStartDateUTC()
-                dtend = vfreebusies[0].getEndDateUTC()
-                if dtstart is None or dtend is None:
-                    log.err("VFREEBUSY start/end not valid: %s" % (self.calendardata,))
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="VFREEBUSY start/end not valid"))
-                self.timeRange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
-                self.timeRange.start = dtstart
-                self.timeRange.end = dtend
-        
-                # Look for masked UID
-                self.excludeUID = self.calendar.getMaskUID()
-        
-                # Do free busy operation
-                self.isfreebusy = True
-            else:
-                # Do regular invite (fan-out)
-                self.isfreebusy = False
-        
-        return self.isfreebusy
-    
-    def securityChecks(self):
-        raise NotImplementedError
-
-    def doAccounting(self):
-        #
-        # Accounting
-        #
-        # Note that we associate logging with the organizer, not the
-        # originator, which is good for looking for why something
-        # shows up in a given principal's calendars, rather than
-        # tracking the activities of a specific user.
-        #
-        if isinstance(self.organizer, LocalCalendarUser):
-            accountingType = "iTIP-VFREEBUSY" if self.calendar.mainType() == "VFREEBUSY" else "iTIP"
-            if accountingEnabled(accountingType, self.organizer.principal):
-                emitAccounting(
-                    accountingType, self.organizer.principal,
-                    "Originator: %s\nRecipients:\n%sMethod:%s\n\n%s"
-                    % (
-                        str(self.originator),
-                        str("".join(["    %s\n" % (recipient,) for recipient in self.recipients])),
-                        str(self.method),
-                        self.calendardata
-                    )
-                )
-
-    def finalChecks(self):
-        """
-        Final checks before doing the actual scheduling.
-        """
-        pass
-
-    @inlineCallbacks
-    def generateSchedulingResponse(self):
-
-        log.info("METHOD: %s, Component: %s" % (self.calendar.propertyValue("METHOD"), self.calendar.mainType(),))
-
-        # For free-busy do immediate determination of iTIP result rather than fan-out
-        freebusy = self.checkForFreeBusy()
-
-        # Prepare for multiple responses
-        responses = ScheduleResponseQueue(self.method, responsecode.OK)
-    
-        # Loop over each recipient and aggregate into lists by service types.
-        caldav_recipients = []
-        remote_recipients = []
-        for recipient in self.recipients:
-    
-            if self.fakeTheResult:
-                responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS if freebusy else iTIPRequestStatus.MESSAGE_DELIVERED)
-                
-            elif isinstance(recipient, LocalCalendarUser):
-                caldav_recipients.append(recipient)
-
-            elif isinstance(recipient, PartitionedCalendarUser):
-                remote_recipients.append(recipient)
-
-            elif isinstance(recipient, RemoteCalendarUser):
-                remote_recipients.append(recipient)
-
-            else:
-                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
-                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.INVALID_CALENDAR_USER)
-            
-        # Now process local recipients
-        if caldav_recipients:
-            yield self.generateLocalSchedulingResponses(caldav_recipients, responses, freebusy)
-
-        # To reduce chatter, we suppress certain messages
-        if not getattr(self.request, 'suppressRefresh', False):
-
-            # Now process remote recipients
-            if remote_recipients:
-                yield self.generateRemoteSchedulingResponses(remote_recipients, responses, freebusy)
-
-        # Return with final response if we are done
-        returnValue(responses)
-    
-    def generateLocalSchedulingResponses(self, recipients, responses, freebusy):
-        """
-        Generate scheduling responses for CalDAV recipients.
-        """
-
-        # Create the scheduler and run it.
-        requestor = ScheduleViaCalDAV(self, recipients, responses, freebusy)
-        return requestor.generateSchedulingResponses()
-
-    def generateRemoteSchedulingResponses(self, recipients, responses, freebusy):
-        """
-        Generate scheduling responses for remote recipients.
-        """
-
-        # Create the scheduler and run it.
-        requestor = ScheduleViaISchedule(self, recipients, responses, freebusy)
-        return requestor.generateSchedulingResponses()
-
-class CalDAVScheduler(Scheduler):
-
-    def __init__(self, request, resource):
-        super(CalDAVScheduler, self).__init__(request, resource)
-        self.doingPOST = False
-
-    def doSchedulingViaPOST(self, use_request_headers=False):
-        """
-        The Scheduling POST operation on an Outbox.
-        """
-        self.doingPOST = True
-        return super(CalDAVScheduler, self).doSchedulingViaPOST(use_request_headers)
-
-    def checkAuthorization(self):
-        # Must have an authenticated user
-        if not self.internal_request and self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
-            log.err("Unauthenticated originators not allowed: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
-    def checkOriginator(self):
-        """
-        Check the validity of the Originator header. Extract the corresponding principal.
-        """
-    
-        # Verify that Originator is a valid calendar user
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        if originatorPrincipal is None:
-            # Local requests MUST have a principal.
-            log.err("Could not find principal for originator: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-        else:
-            # Must have a valid Inbox.
-            inboxURL = originatorPrincipal.scheduleInboxURL()
-            if inboxURL is None:
-                log.err("Could not find inbox for originator: %s" % (self.originator,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-        
-            # Verify that Originator matches the authenticated user, but not if this is a server
-            # generated request
-            if not self.internal_request:
-                authn_principal = self.resource.currentPrincipal(self.request)
-                if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != authn_principal:
-                    log.err("Originator: %s does not match authorized user: %s" % (self.originator, authn_principal.children[0],))
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
-            self.originator = LocalCalendarUser(self.originator, originatorPrincipal)
-
-    @inlineCallbacks
-    def checkRecipients(self):
-        """
-        Check the validity of the Recipient header values. Map these into local or
-        remote CalendarUsers.
-        """
-        
-        results = []
-        for recipient in self.recipients:
-            # Get the principal resource for this recipient
-            principal = self.resource.principalForCalendarUserAddress(recipient)
-            
-            # If no principal we may have a remote recipient but we should check whether
-            # the address is one that ought to be on our server and treat that as a missing
-            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
-            if principal is None:
-                address = (yield addressmapping.mapper.getCalendarUser(recipient, principal))
-                if isinstance(address, InvalidCalendarUser):
-                    log.err("Unknown calendar user address: %s" % (recipient,))
-                results.append(address)
-            else:
-                # Map recipient to their inbox
-                inbox = None
-                inboxURL = principal.scheduleInboxURL()
-                if inboxURL:
-                    inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
-
-                if inbox:
-                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL) if principal.locallyHosted() else PartitionedCalendarUser(recipient, principal))
-                else:
-                    log.err("No schedule inbox for principal: %s" % (principal,))
-                    results.append(InvalidCalendarUser(recipient))
-        
-        self.recipients = results
-
-    @inlineCallbacks
-    def checkOrganizer(self):
-        """
-        Check the validity of the ORGANIZER value. ORGANIZER must be local.
-        """
-        
-        # Verify that the ORGANIZER's cu address maps to a valid user
-        organizer = self.calendar.getOrganizer()
-        if organizer:
-            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
-            if organizerPrincipal:
-                outboxURL = organizerPrincipal.scheduleOutboxURL()
-                if outboxURL:
-                    self.organizer = LocalCalendarUser(organizer, organizerPrincipal)
-                else:
-                    log.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendardata,))
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-            else:
-                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
-                if localUser:
-                    log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendardata,))
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-                else:
-                    self.organizer = RemoteCalendarUser(organizer) 
-        else:
-            log.err("ORGANIZER missing in calendar data: %s" % (self.calendardata,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
-    def checkOrganizerAsOriginator(self):
-
-        # Make sure that the ORGANIZER is local
-        if not isinstance(self.organizer, LocalCalendarUser):
-            log.err("ORGANIZER is not local to server in calendar data: %s" % (self.calendardata,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
-        # Make sure that the ORGANIZER's Outbox is the request URI
-        if self.doingPOST and self.organizer.principal.scheduleOutboxURL() != self.request.uri:
-            log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendardata,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
-    def checkAttendeeAsOriginator(self):
-        """
-        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
-        Only local attendees are allowed for message originating from this server.
-        """
-        
-        # Attendee's Outbox MUST be the request URI
-        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
-        if attendeePrincipal:
-            if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != self.request.uri:
-                log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendardata,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-        else:
-            log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendardata,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-    
-    def securityChecks(self):
-        """
-        Check that the originator has the appropriate rights to send this type of iTIP message.
-        """
-    
-        # Prevent spoofing of ORGANIZER with specific METHODs when local
-        if self.isiTIPRequest:
-            self.checkOrganizerAsOriginator()
-    
-        # Prevent spoofing when doing reply-like METHODs
-        else:
-            self.checkAttendeeAsOriginator()
-
-    def finalChecks(self):
-        """
-        Final checks before doing the actual scheduling.
-        """
-        
-        # With implicit scheduling only certain types of iTIP operations are allowed for POST.
-        # This server does not do implicit so we let everything through
-        pass
-
-class RemoteScheduler(Scheduler):
-
-    def checkOrganizer(self):
-        """
-        Delay ORGANIZER check until we know what their role is.
-        """
-        pass
-
-    @inlineCallbacks
-    def checkRecipients(self):
-        """
-        Check the validity of the Recipient header values. These must all be local as there
-        is no concept of server-to-server relaying.
-        """
-        
-        results = []
-        for recipient in self.recipients:
-            # Get the principal resource for this recipient
-            principal = self.resource.principalForCalendarUserAddress(recipient)
-            
-            # If no principal we may have a remote recipient but we should check whether
-            # the address is one that ought to be on our server and treat that as a missing
-            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
-            if principal is None:
-                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(recipient))
-                if localUser:
-                    log.err("No principal for calendar user address: %s" % (recipient,))
-                else:
-                    log.err("Unknown calendar user address: %s" % (recipient,))
-                results.append(InvalidCalendarUser(recipient))
-            else:
-                # Map recipient to their inbox
-                inbox = None
-                inboxURL = principal.scheduleInboxURL()
-                if inboxURL:
-                    inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
-
-                if inbox:
-                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL) if principal.locallyHosted() else PartitionedCalendarUser(recipient, principal))
-                else:
-                    log.err("No schedule inbox for principal: %s" % (principal,))
-                    results.append(InvalidCalendarUser(recipient))
-        
-        self.recipients = results
-
-class IScheduleScheduler(RemoteScheduler):
-
-    def checkAuthorization(self):
-        # Must have an unauthenticated user
-        if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
-            log.err("Authenticated originators not allowed: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
-    @inlineCallbacks
-    def checkOriginator(self):
-        """
-        Check the validity of the Originator header.
-        """
-    
-        # For remote requests we do not allow the originator to be a local user or one within our domain.
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
-        if originatorPrincipal or localUser:
-            if originatorPrincipal.locallyHosted():
-                log.err("Cannot use originator that is on this server: %s" % (self.originator,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-            else:
-                self.originator = PartitionedCalendarUser(self.originator, originatorPrincipal)
-                #self._validPartitionServer()
-        else:
-            self.originator = RemoteCalendarUser(self.originator)
-            self._validiScheduleServer()
-
-    def _validiScheduleServer(self):
-        """
-        Check the validity of the iSchedule host.
-        """
-    
-        # We will only accept originator in known domains.
-        servermgr = IScheduleServers()
-        server = servermgr.mapDomain(self.originator.domain)
-        if not server or not server.allow_from:
-            log.err("Originator not on recognized server: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-        else:
-            # Get the request IP and map to hostname.
-            clientip = self.request.remoteAddr.host
-            
-            # First compare as dotted IP
-            matched = False
-            compare_with = (server.host,) + tuple(server.client_hosts)
-            if clientip in compare_with:
-                matched = True
-            else:
-                # Now do hostname lookup
-                try:
-                    host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
-                    for host in itertools.chain((host,), aliases):
-                        # Try simple match first
-                        if host in compare_with:
-                            matched = True
-                            break
-                        
-                        # Try pattern match next
-                        for pattern in compare_with:
-                            try:
-                                if re.match(pattern, host) is not None:
-                                    matched = True
-                                    break
-                            except re.error:
-                                log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
-                        else:
-                            continue
-                        break
-                except socket.herror, e:
-                    log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-                        
-            if not matched:
-                log.err("Originator not on allowed server: %s" % (self.originator,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
-    def _validPartitionServer(self, principal):
-        """
-        Check the validity of the partitioned host.
-        """
-
-        # Extract expected host/port
-        expected_uri = principal.hostedURL()
-        expected_uri = urlparse.urlparse(expected_uri)
-    
-        # Get the request IP and map to hostname.
-        clientip = self.request.remoteAddr.host
-        
-        # First compare as dotted IP
-        matched = False
-        if clientip == expected_uri.hostname:
-            matched = True
-        else:
-            # Now do hostname lookup
-            try:
-                host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
-                for hostname in itertools.chain((host,), aliases):
-                    # Try host match
-                    if hostname == expected_uri.hostname:
-                        matched = True
-                        break
-            except socket.herror, e:
-                log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-        
-        if not matched:
-            log.err("Originator not on allowed server: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
-    @inlineCallbacks
-    def checkOrganizerAsOriginator(self):
-        """
-        Check the validity of the ORGANIZER value. ORGANIZER must not be local.
-        """
-        
-        # Verify that the ORGANIZER's cu address does not map to a valid user
-        organizer = self.calendar.getOrganizer()
-        if organizer:
-            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
-            if organizerPrincipal:
-                if organizerPrincipal.locallyHosted():
-                    log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendardata,))
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-                else:
-                    # Check that the origin server is the correct partition
-                    self.organizer = PartitionedCalendarUser(organizer, organizerPrincipal)
-                    self._validPartitionServer(self.organizer.principal)
-            else:
-                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
-                if localUser:
-                    log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendardata,))
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-                else:
-                    self.organizer = RemoteCalendarUser(organizer)
-        else:
-            log.err("ORGANIZER missing in calendar data: %s" % (self.calendardata,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
-    @inlineCallbacks
-    def checkAttendeeAsOriginator(self):
-        """
-        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
-        Only local attendees are allowed for message originating from this server.
-        """
-        
-        # Attendee cannot be local.
-        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
-        if attendeePrincipal:
-            if attendeePrincipal.locallyHosted():
-                log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendardata,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-            else:
-                self._validPartitionServer(attendeePrincipal)                
-        else:
-            localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
-            if localUser:
-                log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendardata,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-    
-        # TODO: in this case we should check that the ORGANIZER is the sole recipient.
-
-    @inlineCallbacks
-    def securityChecks(self):
-        """
-        Check that the originator has the appropriate rights to send this type of iTIP message.
-        """
-
-        # Prevent spoofing of ORGANIZER with specific METHODs when local
-        if self.isiTIPRequest:
-            yield self.checkOrganizerAsOriginator()
-    
-        # Prevent spoofing when doing reply-like METHODs
-        else:
-            yield self.checkAttendeeAsOriginator()
-
-class ScheduleResponseResponse (Response):
-    """
-    ScheduleResponse L{Response} object.
-    Renders itself as a CalDAV:schedule-response XML document.
-    """
-    def __init__(self, xml_responses, location=None):
-        """
-        @param xml_responses: an iterable of davxml.Response objects.
-        @param location:      the value of the location header to return in the response,
-            or None.
-        """
-
-        Response.__init__(self, code=responsecode.OK,
-                          stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
-
-        self.headers.setHeader("content-type", MimeType("text", "xml"))
-    
-        if location is not None:
-            self.headers.setHeader("location", location)
-
-class ScheduleResponseQueue (LoggingMixIn):
-    """
-    Stores a list of (typically error) responses for use in a
-    L{ScheduleResponse}.
-    """
-    def __init__(self, method, success_response):
-        """
-        @param method: the name of the method generating the queue.
-        @param success_response: the response to return in lieu of a
-            L{ScheduleResponse} if no responses are added to this queue.
-        """
-        self.responses         = []
-        self.method            = method
-        self.success_response  = success_response
-        self.location          = None
-
-    def setLocation(self, location):
-        """
-        @param location:      the value of the location header to return in the response,
-            or None.
-        """
-        self.location = location
-
-    def add(self, recipient, what, reqstatus=None, calendar=None):
-        """
-        Add a response.
-        @param recipient: the recipient for this response.
-        @param what: a status code or a L{Failure} for the given recipient.
-        @param status: the iTIP request-status for the given recipient.
-        @param calendar: the calendar data for the given recipient response.
-        """
-        if type(what) is int:
-            code    = what
-            error   = None
-            message = responsecode.RESPONSES[code]
-        elif isinstance(what, Failure):
-            code    = statusForFailure(what)
-            error   = errorForFailure(what)
-            message = messageForFailure(what)
-        else:
-            raise AssertionError("Unknown data type: %r" % (what,))
-
-        if code > 400: # Error codes only
-            self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
-
-        children = []
-        children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
-        children.append(caldavxml.RequestStatus(reqstatus))
-        if calendar is not None:
-            children.append(caldavxml.CalendarData.fromCalendar(calendar))
-        if error is not None:
-            children.append(error)
-        if message is not None:
-            children.append(davxml.ResponseDescription(message))
-        self.responses.append(caldavxml.Response(*children))
-
-    def clone(self, clone):
-        """
-        Add a response cloned from an existing caldavxml.Response element.
-        @param clone: the response to clone.
-        """
-        if not isinstance(clone, caldavxml.Response):
-            raise AssertionError("Incorrect element type: %r" % (clone,))
-
-        recipient = clone.childOfType(caldavxml.Recipient)
-        request_status = clone.childOfType(caldavxml.RequestStatus)
-        calendar_data = clone.childOfType(caldavxml.CalendarData)
-        error = clone.childOfType(davxml.Error)
-        desc = clone.childOfType(davxml.ResponseDescription)
-
-        children = []
-        children.append(recipient)
-        children.append(request_status)
-        if calendar_data is not None:
-            children.append(calendar_data)
-        if error is not None:
-            children.append(error)
-        if desc is not None:
-            children.append(desc)
-        self.responses.append(caldavxml.Response(*children))
-
-    def response(self):
-        """
-        Generate a L{ScheduleResponseResponse} with the responses contained in the
-        queue or, if no such responses, return the C{success_response} provided
-        to L{__init__}.
-        @return: the response.
-        """
-        if self.responses:
-            return ScheduleResponseResponse(self.responses, self.location)
-        else:
-            return self.success_response

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/scheduler.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,921 @@
+##
+# Copyright (c) 2005-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from twisted.python.failure import Failure
+
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import errorForFailure, messageForFailure, statusForFailure
+from twisted.web2.http import HTTPError, Response
+from twisted.web2.http_headers import MimeType
+
+from twistedcaldav import caldavxml
+from twistedcaldav.accounting import accountingEnabled, emitAccounting
+from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.extensions import ErrorResponse
+from twistedcaldav.ical import Component
+from twistedcaldav.log import Logger, LoggingMixIn
+from twistedcaldav.scheduling import addressmapping
+from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
+from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
+    LocalCalendarUser, RemoteCalendarUser, PartitionedCalendarUser
+from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
+from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+
+import itertools
+import re
+import socket
+import urlparse
+
+"""
+CalDAV/Server-to-Server scheduling behavior.
+"""
+
+__all__ = [
+    "Scheduler",
+    "CalDAVScheduler",
+    "IScheduleScheduler",
+    "IMIPScheduler",
+    "DirectScheduler",
+]
+
+
+log = Logger()
+
+class Scheduler(object):
+    
+    def __init__(self, request, resource):
+        self.request = request
+        self.resource = resource
+        self.originator = None
+        self.recipients = None
+        self.calendar = None
+        self.calendardata = None
+        self.organizer = None
+        self.attendee = None
+        self.isiTIPRequest = None
+        self.timeRange = None
+        self.excludeUID = None
+        self.fakeTheResult = False
+        self.method = "Unknown"
+        self.internal_request = False
+    
+    @inlineCallbacks
+    def doSchedulingViaPOST(self, use_request_headers=False):
+        """
+        The Scheduling POST operation on an Outbox.
+        """
+    
+        self.method = "POST"
+
+        # Load various useful bits doing some basic checks on those
+        yield self.loadCalendarFromRequest()
+        
+        if use_request_headers:
+            self.loadFromRequestHeaders()
+        else:
+            yield self.loadFromRequestData()
+
+        if not hasattr(self.request, "extendedLogItems"):
+            self.request.extendedLogItems = {}
+        self.request.extendedLogItems["recipients"] = len(self.recipients)
+        self.request.extendedLogItems["cl"] = str(len(self.calendardata))
+    
+        # Do some extra authorization checks
+        self.checkAuthorization()
+
+        result = (yield self.doScheduling())
+        returnValue(result)
+
+    def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
+        """
+        The implicit scheduling PUT operation.
+        """
+    
+        self.method = "PUT"
+
+        # Load various useful bits doing some basic checks on those
+        self.originator = originator
+        self.recipients = recipients
+        self.calendar = calendar
+        self.internal_request = internal_request
+
+        # Do some extra authorization checks
+        self.checkAuthorization()
+
+        return self.doScheduling()
+
+    @inlineCallbacks
+    def doScheduling(self):
+        # Check validity of Originator header.
+        yield self.checkOriginator()
+    
+        # Get recipient details.
+        yield self.checkRecipients()
+    
+        # Check calendar data.
+        self.checkCalendarData()
+    
+        # Check validity of ORGANIZER
+        yield self.checkOrganizer()
+    
+        # Do security checks (e.g. spoofing)
+        yield self.securityChecks()
+    
+        # Generate accounting information
+        self.doAccounting()
+
+        # Do some final checks after we have gathered all our information
+        self.finalChecks()
+
+        # Do scheduling tasks
+        result = (yield self.generateSchedulingResponse())
+
+        returnValue(result)
+
+    @inlineCallbacks
+    def loadFromRequestData(self):
+        yield self.loadOriginatorFromRequestDetails()
+        self.loadRecipientsFromCalendarData()
+        
+    @inlineCallbacks
+    def loadOriginatorFromRequestDetails(self):
+        # Get the originator who is the authenticated user
+        originatorPrincipal = None
+        originator = ""
+        authz_principal = self.resource.currentPrincipal(self.request).children[0]
+        if isinstance(authz_principal, davxml.HRef):
+            originatorPrincipalURL = str(authz_principal)
+            if originatorPrincipalURL:
+                originatorPrincipal = (yield self.request.locateResource(originatorPrincipalURL))
+                if originatorPrincipal:
+                    # Pick the first mailto cu address or the first other type
+                    for item in originatorPrincipal.calendarUserAddresses():
+                        if not originator:
+                            originator = item
+                        if item.startswith("mailto:"):
+                            originator = item
+                            break
+
+        if not originator:
+            log.err("%s request must have Originator" % (self.method,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
+        else:
+            self.originator = originator
+
+    def loadRecipientsFromCalendarData(self):
+
+        # Get the ATTENDEEs
+        attendees = set()
+        for attendee, _ignore in self.calendar.getAttendeesByInstance():
+            attendees.add(attendee)
+        
+        if not attendees:
+            log.err("%s request must have at least one Recipient" % (self.method,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+        else:
+            self.recipients = list(attendees)
+
+    def loadFromRequestHeaders(self):
+        """
+        Load Originator and Recipient from request headers.
+        """
+        self.loadOriginatorFromRequestHeaders()
+        self.loadRecipientsFromRequestHeaders()
+
+    def loadOriginatorFromRequestHeaders(self):
+        # Must have Originator header
+        originator = self.request.headers.getRawHeaders("originator")
+        if originator is None or (len(originator) != 1):
+            log.err("%s request must have Originator header" % (self.method,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
+        else:
+            self.originator = originator[0]
+    
+    def loadRecipientsFromRequestHeaders(self):
+        # Get list of Recipient headers
+        rawRecipients = self.request.headers.getRawHeaders("recipient")
+        if rawRecipients is None or (len(rawRecipients) == 0):
+            log.err("%s request must have at least one Recipient header" % (self.method,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+    
+        # Recipient header may be comma separated list
+        self.recipients = []
+        for rawRecipient in rawRecipients:
+            for r in rawRecipient.split(","):
+                r = r.strip()
+                if len(r):
+                    self.recipients.append(r)
+        
+    @inlineCallbacks
+    def loadCalendarFromRequest(self):
+        # Must be content-type text/calendar
+        contentType = self.request.headers.getHeader("content-type")
+        if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
+            log.err("MIME type %s not allowed in calendar collection" % (contentType,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
+    
+        # Parse the calendar object from the HTTP request stream
+        try:
+            self.calendar = (yield Component.fromIStream(self.request.stream))
+            self.calendardata = str(self.calendar)
+        except:
+            # FIXME: Bare except
+            log.err("Error while handling %s: %s" % (self.method, Failure(),))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Can't parse calendar data"))
+
+    def checkAuthorization(self):
+        raise NotImplementedError
+
+    def checkOriginator(self):
+        raise NotImplementedError
+
+    def checkRecipients(self):
+        raise NotImplementedError
+
+    def checkOrganizer(self):
+        raise NotImplementedError
+
+    def checkOrganizerAsOriginator(self):
+        raise NotImplementedError
+
+    def checkAttendeeAsOriginator(self):
+        raise NotImplementedError
+
+    def checkCalendarData(self):
+        # Must be a valid calendar
+        try:
+            self.calendar.validCalendarForCalDAV()
+        except ValueError, e:
+            log.err("%s request calendar component is not valid:%s %s" % (self.method, e, self.calendardata,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Calendar component is not valid"))
+    
+        # Must have a METHOD
+        if not self.calendar.isValidMethod():
+            log.err("%s request must have valid METHOD property in calendar component: %s" % (self.method, self.calendardata,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Must have valid METHOD property"))
+        
+        # Verify iTIP behavior
+        if not self.calendar.isValidITIP():
+            log.err("%s request must have a calendar component that satisfies iTIP requirements: %s" % (self.method, self.calendardata,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Must have a calendar component that satisfies iTIP requirements"))
+
+        # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
+        if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
+            log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component %s request: %s" % (self.method, self.calendardata,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
+
+        # Determine iTIP method mode
+        if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
+            self.isiTIPRequest = True
+
+        elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+            self.isiTIPRequest = False
+
+            # Verify that there is a single ATTENDEE property
+            attendees = self.calendar.getAttendees()
+        
+            # Must have only one
+            if len(attendees) != 1:
+                log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendardata,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+            self.attendee = attendees[0]
+
+        else:
+            msg = "Unknown iTIP METHOD: %s" % (self.calendar.propertyValue("METHOD"),)
+            log.err(msg)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description=msg))
+
+    def checkForFreeBusy(self):
+        if not hasattr(self, "isfreebusy"):
+            if (self.calendar.propertyValue("METHOD") == "REQUEST") and (self.calendar.mainType() == "VFREEBUSY"):
+                # Extract time range from VFREEBUSY object
+                vfreebusies = [v for v in self.calendar.subcomponents() if v.name() == "VFREEBUSY"]
+                if len(vfreebusies) != 1:
+                    log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (self.calendardata,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="iTIP data is not valid for a VFREEBUSY request"))
+                dtstart = vfreebusies[0].getStartDateUTC()
+                dtend = vfreebusies[0].getEndDateUTC()
+                if dtstart is None or dtend is None:
+                    log.err("VFREEBUSY start/end not valid: %s" % (self.calendardata,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="VFREEBUSY start/end not valid"))
+                self.timeRange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
+                self.timeRange.start = dtstart
+                self.timeRange.end = dtend
+        
+                # Look for masked UID
+                self.excludeUID = self.calendar.getMaskUID()
+        
+                # Do free busy operation
+                self.isfreebusy = True
+            else:
+                # Do regular invite (fan-out)
+                self.isfreebusy = False
+        
+        return self.isfreebusy
+    
+    def securityChecks(self):
+        raise NotImplementedError
+
+    def doAccounting(self):
+        #
+        # Accounting
+        #
+        # Note that we associate logging with the organizer, not the
+        # originator, which is good for looking for why something
+        # shows up in a given principal's calendars, rather than
+        # tracking the activities of a specific user.
+        #
+        if isinstance(self.organizer, LocalCalendarUser):
+            accountingType = "iTIP-VFREEBUSY" if self.calendar.mainType() == "VFREEBUSY" else "iTIP"
+            if accountingEnabled(accountingType, self.organizer.principal):
+                emitAccounting(
+                    accountingType, self.organizer.principal,
+                    "Originator: %s\nRecipients:\n%sMethod:%s\n\n%s"
+                    % (
+                        str(self.originator),
+                        str("".join(["    %s\n" % (recipient,) for recipient in self.recipients])),
+                        str(self.method),
+                        self.calendardata
+                    )
+                )
+
+    def finalChecks(self):
+        """
+        Final checks before doing the actual scheduling.
+        """
+        pass
+
+    @inlineCallbacks
+    def generateSchedulingResponse(self):
+
+        log.info("METHOD: %s, Component: %s" % (self.calendar.propertyValue("METHOD"), self.calendar.mainType(),))
+
+        # For free-busy do immediate determination of iTIP result rather than fan-out
+        freebusy = self.checkForFreeBusy()
+
+        # Prepare for multiple responses
+        responses = ScheduleResponseQueue(self.method, responsecode.OK)
+    
+        # Loop over each recipient and aggregate into lists by service types.
+        caldav_recipients = []
+        remote_recipients = []
+        for recipient in self.recipients:
+    
+            if self.fakeTheResult:
+                responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS if freebusy else iTIPRequestStatus.MESSAGE_DELIVERED)
+                
+            elif isinstance(recipient, LocalCalendarUser):
+                caldav_recipients.append(recipient)
+
+            elif isinstance(recipient, PartitionedCalendarUser):
+                remote_recipients.append(recipient)
+
+            elif isinstance(recipient, RemoteCalendarUser):
+                remote_recipients.append(recipient)
+
+            else:
+                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
+                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.INVALID_CALENDAR_USER)
+            
+        # Now process local recipients
+        if caldav_recipients:
+            yield self.generateLocalSchedulingResponses(caldav_recipients, responses, freebusy)
+
+        # To reduce chatter, we suppress certain messages
+        if not getattr(self.request, 'suppressRefresh', False):
+
+            # Now process remote recipients
+            if remote_recipients:
+                yield self.generateRemoteSchedulingResponses(remote_recipients, responses, freebusy)
+
+        # Return with final response if we are done
+        returnValue(responses)
+    
+    def generateLocalSchedulingResponses(self, recipients, responses, freebusy):
+        """
+        Generate scheduling responses for CalDAV recipients.
+        """
+
+        # Create the scheduler and run it.
+        requestor = ScheduleViaCalDAV(self, recipients, responses, freebusy)
+        return requestor.generateSchedulingResponses()
+
+    def generateRemoteSchedulingResponses(self, recipients, responses, freebusy):
+        """
+        Generate scheduling responses for remote recipients.
+        """
+
+        # Create the scheduler and run it.
+        requestor = ScheduleViaISchedule(self, recipients, responses, freebusy)
+        return requestor.generateSchedulingResponses()
+
+class CalDAVScheduler(Scheduler):
+
+    def __init__(self, request, resource):
+        super(CalDAVScheduler, self).__init__(request, resource)
+        self.doingPOST = False
+
+    def doSchedulingViaPOST(self, use_request_headers=False):
+        """
+        The Scheduling POST operation on an Outbox.
+        """
+        self.doingPOST = True
+        return super(CalDAVScheduler, self).doSchedulingViaPOST(use_request_headers)
+
+    def checkAuthorization(self):
+        # Must have an authenticated user
+        if not self.internal_request and self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
+            log.err("Unauthenticated originators not allowed: %s" % (self.originator,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+    def checkOriginator(self):
+        """
+        Check the validity of the Originator header. Extract the corresponding principal.
+        """
+    
+        # Verify that Originator is a valid calendar user
+        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+        if originatorPrincipal is None:
+            # Local requests MUST have a principal.
+            log.err("Could not find principal for originator: %s" % (self.originator,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        else:
+            # Must have a valid Inbox.
+            inboxURL = originatorPrincipal.scheduleInboxURL()
+            if inboxURL is None:
+                log.err("Could not find inbox for originator: %s" % (self.originator,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        
+            # Verify that Originator matches the authenticated user, but not if this is a server
+            # generated request
+            if not self.internal_request:
+                authn_principal = self.resource.currentPrincipal(self.request)
+                if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != authn_principal:
+                    log.err("Originator: %s does not match authorized user: %s" % (self.originator, authn_principal.children[0],))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+            self.originator = LocalCalendarUser(self.originator, originatorPrincipal)
+
+    @inlineCallbacks
+    def checkRecipients(self):
+        """
+        Check the validity of the Recipient header values. Map these into local or
+        remote CalendarUsers.
+        """
+        
+        results = []
+        for recipient in self.recipients:
+            # Get the principal resource for this recipient
+            principal = self.resource.principalForCalendarUserAddress(recipient)
+            
+            # If no principal we may have a remote recipient but we should check whether
+            # the address is one that ought to be on our server and treat that as a missing
+            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
+            if principal is None:
+                address = (yield addressmapping.mapper.getCalendarUser(recipient, principal))
+                if isinstance(address, InvalidCalendarUser):
+                    log.err("Unknown calendar user address: %s" % (recipient,))
+                results.append(address)
+            else:
+                # Map recipient to their inbox
+                inbox = None
+                inboxURL = principal.scheduleInboxURL()
+                if inboxURL:
+                    inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
+
+                if inbox:
+                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL) if principal.locallyHosted() else PartitionedCalendarUser(recipient, principal))
+                else:
+                    log.err("No schedule inbox for principal: %s" % (principal,))
+                    results.append(InvalidCalendarUser(recipient))
+        
+        self.recipients = results
+
+    @inlineCallbacks
+    def checkOrganizer(self):
+        """
+        Check the validity of the ORGANIZER value. ORGANIZER must be local.
+        """
+        
+        # Verify that the ORGANIZER's cu address maps to a valid user
+        organizer = self.calendar.getOrganizer()
+        if organizer:
+            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+            if organizerPrincipal:
+                outboxURL = organizerPrincipal.scheduleOutboxURL()
+                if outboxURL:
+                    self.organizer = LocalCalendarUser(organizer, organizerPrincipal)
+                else:
+                    log.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendardata,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+            else:
+                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
+                if localUser:
+                    log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendardata,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+                else:
+                    self.organizer = RemoteCalendarUser(organizer) 
+        else:
+            log.err("ORGANIZER missing in calendar data: %s" % (self.calendardata,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+    def checkOrganizerAsOriginator(self):
+
+        # Make sure that the ORGANIZER is local
+        if not isinstance(self.organizer, LocalCalendarUser):
+            log.err("ORGANIZER is not local to server in calendar data: %s" % (self.calendardata,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+        # Make sure that the ORGANIZER's Outbox is the request URI
+        if self.doingPOST and self.organizer.principal.scheduleOutboxURL() != self.request.uri:
+            log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendardata,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+    def checkAttendeeAsOriginator(self):
+        """
+        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+        Only local attendees are allowed for message originating from this server.
+        """
+        
+        # Attendee's Outbox MUST be the request URI
+        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
+        if attendeePrincipal:
+            if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != self.request.uri:
+                log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendardata,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+        else:
+            log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendardata,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+    
+    def securityChecks(self):
+        """
+        Check that the originator has the appropriate rights to send this type of iTIP message.
+        """
+    
+        # Prevent spoofing of ORGANIZER with specific METHODs when local
+        if self.isiTIPRequest:
+            self.checkOrganizerAsOriginator()
+    
+        # Prevent spoofing when doing reply-like METHODs
+        else:
+            self.checkAttendeeAsOriginator()
+
+    def finalChecks(self):
+        """
+        Final checks before doing the actual scheduling.
+        """
+        
+        # With implicit scheduling only certain types of iTIP operations are allowed for POST.
+        # This server does not do implicit so we let everything through
+        pass
+
+class RemoteScheduler(Scheduler):
+
+    def checkOrganizer(self):
+        """
+        Delay ORGANIZER check until we know what their role is.
+        """
+        pass
+
+    @inlineCallbacks
+    def checkRecipients(self):
+        """
+        Check the validity of the Recipient header values. These must all be local as there
+        is no concept of server-to-server relaying.
+        """
+        
+        results = []
+        for recipient in self.recipients:
+            # Get the principal resource for this recipient
+            principal = self.resource.principalForCalendarUserAddress(recipient)
+            
+            # If no principal we may have a remote recipient but we should check whether
+            # the address is one that ought to be on our server and treat that as a missing
+            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
+            if principal is None:
+                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(recipient))
+                if localUser:
+                    log.err("No principal for calendar user address: %s" % (recipient,))
+                else:
+                    log.err("Unknown calendar user address: %s" % (recipient,))
+                results.append(InvalidCalendarUser(recipient))
+            else:
+                # Map recipient to their inbox
+                inbox = None
+                inboxURL = principal.scheduleInboxURL()
+                if inboxURL:
+                    inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
+
+                if inbox:
+                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL) if principal.locallyHosted() else PartitionedCalendarUser(recipient, principal))
+                else:
+                    log.err("No schedule inbox for principal: %s" % (principal,))
+                    results.append(InvalidCalendarUser(recipient))
+        
+        self.recipients = results
+
+class IScheduleScheduler(RemoteScheduler):
+
+    def checkAuthorization(self):
+        # Must have an unauthenticated user
+        if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
+            log.err("Authenticated originators not allowed: %s" % (self.originator,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+    @inlineCallbacks
+    def checkOriginator(self):
+        """
+        Check the validity of the Originator header.
+        """
+    
+        # For remote requests we do not allow the originator to be a local user or one within our domain.
+        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
+        if originatorPrincipal or localUser:
+            if originatorPrincipal.locallyHosted():
+                log.err("Cannot use originator that is on this server: %s" % (self.originator,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+            else:
+                self.originator = PartitionedCalendarUser(self.originator, originatorPrincipal)
+                #self._validPartitionServer()
+        else:
+            self.originator = RemoteCalendarUser(self.originator)
+            self._validiScheduleServer()
+
+    def _validiScheduleServer(self):
+        """
+        Check the validity of the iSchedule host.
+        """
+    
+        # We will only accept originator in known domains.
+        servermgr = IScheduleServers()
+        server = servermgr.mapDomain(self.originator.domain)
+        if not server or not server.allow_from:
+            log.err("Originator not on recognized server: %s" % (self.originator,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        else:
+            # Get the request IP and map to hostname.
+            clientip = self.request.remoteAddr.host
+            
+            # First compare as dotted IP
+            matched = False
+            compare_with = (server.host,) + tuple(server.client_hosts)
+            if clientip in compare_with:
+                matched = True
+            else:
+                # Now do hostname lookup
+                try:
+                    host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
+                    for host in itertools.chain((host,), aliases):
+                        # Try simple match first
+                        if host in compare_with:
+                            matched = True
+                            break
+                        
+                        # Try pattern match next
+                        for pattern in compare_with:
+                            try:
+                                if re.match(pattern, host) is not None:
+                                    matched = True
+                                    break
+                            except re.error:
+                                log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
+                        else:
+                            continue
+                        break
+                except socket.herror, e:
+                    log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
+                        
+            if not matched:
+                log.err("Originator not on allowed server: %s" % (self.originator,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+    def _validPartitionServer(self, principal):
+        """
+        Check the validity of the partitioned host.
+        """
+
+        # Extract expected host/port
+        expected_uri = principal.hostedURL()
+        expected_uri = urlparse.urlparse(expected_uri)
+    
+        # Get the request IP and map to hostname.
+        clientip = self.request.remoteAddr.host
+        
+        # First compare as dotted IP
+        matched = False
+        if clientip == expected_uri.hostname:
+            matched = True
+        else:
+            # Now do hostname lookup
+            try:
+                host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
+                for hostname in itertools.chain((host,), aliases):
+                    # Try host match
+                    if hostname == expected_uri.hostname:
+                        matched = True
+                        break
+            except socket.herror, e:
+                log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
+        
+        if not matched:
+            log.err("Originator not on allowed server: %s" % (self.originator,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+    @inlineCallbacks
+    def checkOrganizerAsOriginator(self):
+        """
+        Check the validity of the ORGANIZER value. ORGANIZER must not be local.
+        """
+        
+        # Verify that the ORGANIZER's cu address does not map to a valid user
+        organizer = self.calendar.getOrganizer()
+        if organizer:
+            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+            if organizerPrincipal:
+                if organizerPrincipal.locallyHosted():
+                    log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendardata,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+                else:
+                    # Check that the origin server is the correct partition
+                    self.organizer = PartitionedCalendarUser(organizer, organizerPrincipal)
+                    self._validPartitionServer(self.organizer.principal)
+            else:
+                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
+                if localUser:
+                    log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendardata,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+                else:
+                    self.organizer = RemoteCalendarUser(organizer)
+        else:
+            log.err("ORGANIZER missing in calendar data: %s" % (self.calendardata,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+    @inlineCallbacks
+    def checkAttendeeAsOriginator(self):
+        """
+        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+        Only local attendees are allowed for message originating from this server.
+        """
+        
+        # Attendee cannot be local.
+        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
+        if attendeePrincipal:
+            if attendeePrincipal.locallyHosted():
+                log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendardata,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+            else:
+                self._validPartitionServer(attendeePrincipal)                
+        else:
+            localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
+            if localUser:
+                log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendardata,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+    
+        # TODO: in this case we should check that the ORGANIZER is the sole recipient.
+
+    @inlineCallbacks
+    def securityChecks(self):
+        """
+        Check that the originator has the appropriate rights to send this type of iTIP message.
+        """
+
+        # Prevent spoofing of ORGANIZER with specific METHODs when local
+        if self.isiTIPRequest:
+            yield self.checkOrganizerAsOriginator()
+    
+        # Prevent spoofing when doing reply-like METHODs
+        else:
+            yield self.checkAttendeeAsOriginator()
+
+class ScheduleResponseResponse (Response):
+    """
+    ScheduleResponse L{Response} object.
+    Renders itself as a CalDAV:schedule-response XML document.
+    """
+    def __init__(self, xml_responses, location=None):
+        """
+        @param xml_responses: an iterable of davxml.Response objects.
+        @param location:      the value of the location header to return in the response,
+            or None.
+        """
+
+        Response.__init__(self, code=responsecode.OK,
+                          stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+
+        self.headers.setHeader("content-type", MimeType("text", "xml"))
+    
+        if location is not None:
+            self.headers.setHeader("location", location)
+
+class ScheduleResponseQueue (LoggingMixIn):
+    """
+    Stores a list of (typically error) responses for use in a
+    L{ScheduleResponse}.
+    """
+    def __init__(self, method, success_response):
+        """
+        @param method: the name of the method generating the queue.
+        @param success_response: the response to return in lieu of a
+            L{ScheduleResponse} if no responses are added to this queue.
+        """
+        self.responses         = []
+        self.method            = method
+        self.success_response  = success_response
+        self.location          = None
+
+    def setLocation(self, location):
+        """
+        @param location:      the value of the location header to return in the response,
+            or None.
+        """
+        self.location = location
+
+    def add(self, recipient, what, reqstatus=None, calendar=None):
+        """
+        Add a response.
+        @param recipient: the recipient for this response.
+        @param what: a status code or a L{Failure} for the given recipient.
+        @param status: the iTIP request-status for the given recipient.
+        @param calendar: the calendar data for the given recipient response.
+        """
+        if type(what) is int:
+            code    = what
+            error   = None
+            message = responsecode.RESPONSES[code]
+        elif isinstance(what, Failure):
+            code    = statusForFailure(what)
+            error   = errorForFailure(what)
+            message = messageForFailure(what)
+        else:
+            raise AssertionError("Unknown data type: %r" % (what,))
+
+        if code > 400: # Error codes only
+            self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
+
+        children = []
+        children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
+        children.append(caldavxml.RequestStatus(reqstatus))
+        if calendar is not None:
+            children.append(caldavxml.CalendarData.fromCalendar(calendar))
+        if error is not None:
+            children.append(error)
+        if message is not None:
+            children.append(davxml.ResponseDescription(message))
+        self.responses.append(caldavxml.Response(*children))
+
+    def clone(self, clone):
+        """
+        Add a response cloned from an existing caldavxml.Response element.
+        @param clone: the response to clone.
+        """
+        if not isinstance(clone, caldavxml.Response):
+            raise AssertionError("Incorrect element type: %r" % (clone,))
+
+        recipient = clone.childOfType(caldavxml.Recipient)
+        request_status = clone.childOfType(caldavxml.RequestStatus)
+        calendar_data = clone.childOfType(caldavxml.CalendarData)
+        error = clone.childOfType(davxml.Error)
+        desc = clone.childOfType(davxml.ResponseDescription)
+
+        children = []
+        children.append(recipient)
+        children.append(request_status)
+        if calendar_data is not None:
+            children.append(calendar_data)
+        if error is not None:
+            children.append(error)
+        if desc is not None:
+            children.append(desc)
+        self.responses.append(caldavxml.Response(*children))
+
+    def response(self):
+        """
+        Generate a L{ScheduleResponseResponse} with the responses contained in the
+        queue or, if no such responses, return the C{success_response} provided
+        to L{__init__}.
+        @return: the response.
+        """
+        if self.responses:
+            return ScheduleResponseResponse(self.responses, self.location)
+        else:
+            return self.success_response

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/test/__init__.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/test/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Tests for the twistedcaldav.scheduling module.
-"""

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/test/__init__.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/test/__init__.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/test/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/test/__init__.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Tests for the twistedcaldav.scheduling module.
+"""

Deleted: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/utils.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/utils.py	2010-07-01 20:33:12 UTC (rev 5830)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/utils.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -1,58 +0,0 @@
-#
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks, succeed, returnValue
-from twistedcaldav.method import report_common
-
- at inlineCallbacks
-def getCalendarObjectForPrincipals(request, principal, uid):
-    """
-    Get a copy of the event for a principal.
-    """
-    
-    result = {
-        "resource": None,
-        "resource_name": None,
-        "calendar_collection": None,
-        "calendar_collection_uri": None,
-    }
-
-    if principal and principal.locallyHosted():
-        # Get principal's calendar-home
-        calendar_home = principal.calendarHome()
-        
-        # FIXME: because of the URL->resource request mapping thing, we have to force the request
-        # to recognize this resource
-        request._rememberResource(calendar_home, calendar_home.url())
-
-        # Run a UID query against the UID
-
-        def queryCalendarCollection(collection, uri):
-            rname = collection.index().resourceNameForUID(uid)
-            if rname:
-                result["resource"] = collection.getChild(rname)
-                result["resource_name"] = rname
-                result["calendar_collection"] = collection
-                result["calendar_collection_uri"] = uri
-                return succeed(False)
-            else:
-                return succeed(True)
-        
-        # NB We are by-passing privilege checking here. That should be OK as the data found is not
-        # exposed to the user.
-        yield report_common.applyToCalendarCollections(calendar_home, request, calendar_home.url(), "infinity", queryCalendarCollection, None)
-
-    returnValue((result["resource"], result["resource_name"], result["calendar_collection"], result["calendar_collection_uri"],))

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/utils.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/utils.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/utils.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/utils.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,58 @@
+#
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, succeed, returnValue
+from twistedcaldav.method import report_common
+
+ at inlineCallbacks
+def getCalendarObjectForPrincipals(request, principal, uid):
+    """
+    Get a copy of the event for a principal.
+    """
+    
+    result = {
+        "resource": None,
+        "resource_name": None,
+        "calendar_collection": None,
+        "calendar_collection_uri": None,
+    }
+
+    if principal and principal.locallyHosted():
+        # Get principal's calendar-home
+        calendar_home = principal.calendarHome()
+        
+        # FIXME: because of the URL->resource request mapping thing, we have to force the request
+        # to recognize this resource
+        request._rememberResource(calendar_home, calendar_home.url())
+
+        # Run a UID query against the UID
+
+        def queryCalendarCollection(collection, uri):
+            rname = collection.index().resourceNameForUID(uid)
+            if rname:
+                result["resource"] = collection.getChild(rname)
+                result["resource_name"] = rname
+                result["calendar_collection"] = collection
+                result["calendar_collection_uri"] = uri
+                return succeed(False)
+            else:
+                return succeed(True)
+        
+        # NB We are by-passing privilege checking here. That should be OK as the data found is not
+        # exposed to the user.
+        yield report_common.applyToCalendarCollections(calendar_home, request, calendar_home.url(), "infinity", queryCalendarCollection, None)
+
+    returnValue((result["resource"], result["resource_name"], result["calendar_collection"], result["calendar_collection_uri"],))

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/static.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/static.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -49,11 +49,12 @@
 from twisted.web2.dav.idav import IDAVResource
 from twisted.web2.dav.resource import AccessDeniedError
 from twisted.web2.dav.resource import davPrivilegeSet
-from twisted.web2.dav.util import parentForURL, bindMethods
+from twisted.web2.dav.util import parentForURL, bindMethods, joinURL
 
 from twistedcaldav import caldavxml
 from twistedcaldav import customxml
 from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.client.reverseproxy import ReverseProxyResource
 from twistedcaldav.config import config
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
 from twistedcaldav.extensions import DAVFile
@@ -63,7 +64,8 @@
 from twistedcaldav.ical import Property as iProperty
 from twistedcaldav.index import Index, IndexSchedule
 from twistedcaldav.resource import CalDAVResource, isCalendarCollectionResource, isPseudoCalendarCollectionResource
-from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
+from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource,\
+    IScheduleInboxResource
 from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource
 from twistedcaldav.directory.calendar import uidsResourceName
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
@@ -612,60 +614,77 @@
 
         assert len(name) > 4, "Directory record has an invalid GUID: %r" % (name,)
         
-        childPath = self.fp.child(name[0:2]).child(name[2:4]).child(name)
-        child = self.homeResourceClass(childPath.path, self, record)
-
-        if not child.exists():
-            self.provision()
-
-            if not childPath.parent().isdir():
-                childPath.parent().makedirs()
-
-            for oldPath in (
-                # Pre 2.0: All in one directory
-                self.fp.child(name),
-                # Pre 1.2: In types hierarchy instead of the GUID hierarchy
-                self.parent.getChild(record.recordType).fp.child(record.shortName),
-            ):
-                if oldPath.exists():
-                    # The child exists at an old location.  Move to new location.
-                    log.msg("Moving calendar home from old location %r to new location %r." % (oldPath, childPath))
-                    try:
-                        oldPath.moveTo(childPath)
-                    except (OSError, IOError), e:
-                        log.err("Error moving calendar home %r: %s" % (oldPath, e))
+        if record.locallyHosted():
+            childPath = self.fp.child(name[0:2]).child(name[2:4]).child(name)
+            child = self.homeResourceClass(childPath.path, self, record)
+    
+            if not child.exists():
+                self.provision()
+    
+                if not childPath.parent().isdir():
+                    childPath.parent().makedirs()
+    
+                for oldPath in (
+                    # Pre 2.0: All in one directory
+                    self.fp.child(name),
+                    # Pre 1.2: In types hierarchy instead of the GUID hierarchy
+                    self.parent.getChild(record.recordType).fp.child(record.shortName),
+                ):
+                    if oldPath.exists():
+                        # The child exists at an old location.  Move to new location.
+                        log.msg("Moving calendar home from old location %r to new location %r." % (oldPath, childPath))
+                        try:
+                            oldPath.moveTo(childPath)
+                        except (OSError, IOError), e:
+                            log.err("Error moving calendar home %r: %s" % (oldPath, e))
+                            raise HTTPError(StatusResponse(
+                                responsecode.INTERNAL_SERVER_ERROR,
+                                "Unable to move calendar home."
+                            ))
+                        child.fp.changed()
+                        break
+                else:
+                    #
+                    # NOTE: provisionDefaultCalendars() returns a deferred, which we are ignoring.
+                    # The result being that the default calendars will be present at some point
+                    # in the future, not necessarily right now, and we don't have a way to wait
+                    # on that to finish.
+                    #
+                    child.provisionDefaultCalendars()
+    
+                    #
+                    # Try to work around the above a little by telling the client that something
+                    # when wrong temporarily if the child isn't provisioned right away.
+                    #
+                    if not child.exists():
                         raise HTTPError(StatusResponse(
-                            responsecode.INTERNAL_SERVER_ERROR,
-                            "Unable to move calendar home."
+                            responsecode.SERVICE_UNAVAILABLE,
+                            "Provisioning calendar home."
                         ))
-                    child.fp.changed()
-                    break
-            else:
-                #
-                # NOTE: provisionDefaultCalendars() returns a deferred, which we are ignoring.
-                # The result being that the default calendars will be present at some point
-                # in the future, not necessarily right now, and we don't have a way to wait
-                # on that to finish.
-                #
-                child.provisionDefaultCalendars()
+    
+                assert child.exists()
+        
+        else:
+            childPath = self.fp.child(name[0:2]).child(name[2:4]).child(name)
+            child = CalendarHomeReverseProxyFile(childPath.path, self, record)
 
-                #
-                # Try to work around the above a little by telling the client that something
-                # when wrong temporarily if the child isn't provisioned right away.
-                #
-                if not child.exists():
-                    raise HTTPError(StatusResponse(
-                        responsecode.SERVICE_UNAVAILABLE,
-                        "Provisioning calendar home."
-                    ))
-
-            assert child.exists()
-
         return child
 
     def createSimilarFile(self, path):
         raise HTTPError(responsecode.NOT_FOUND)
 
+class CalendarHomeReverseProxyFile(ReverseProxyResource):
+    
+    def __init__(self, path, parent, record):
+        self.path = path
+        self.parent = parent
+        self.record = record
+        
+        super(CalendarHomeReverseProxyFile, self).__init__(self.record.hostedAt)
+    
+    def url(self):
+        return joinURL(self.parent.url(), self.record.guid)
+
 class CalendarHomeFile (PropfindCacheMixin, AutoProvisioningFileMixIn, DirectoryCalendarHomeResource, CalDAVFile):
     """
     Calendar home collection resource.
@@ -858,6 +877,51 @@
     def __repr__(self):
         return "<%s (calendar outbox collection): %s>" % (self.__class__.__name__, self.fp.path)
 
+class IScheduleInboxFile (IScheduleInboxResource, CalDAVFile):
+    """
+    Server-to-server scheduling inbox resource.
+    """
+    def __init__(self, path, parent):
+        CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+        IScheduleInboxResource.__init__(self, parent)
+
+    def __repr__(self):
+        return "<%s (server-to-server inbox resource): %s>" % (self.__class__.__name__, self.fp.path)
+
+    def isCollection(self):
+        return False
+
+    def createSimilarFile(self, path):
+        if path == self.fp.path:
+            return self
+        else:
+            return responsecode.NOT_FOUND
+
+    def http_PUT        (self, request): return responsecode.FORBIDDEN
+    def http_COPY       (self, request): return responsecode.FORBIDDEN
+    def http_MOVE       (self, request): return responsecode.FORBIDDEN
+    def http_DELETE     (self, request): return responsecode.FORBIDDEN
+    def http_MKCOL      (self, request): return responsecode.FORBIDDEN
+
+    def http_MKCALENDAR(self, request):
+        return ErrorResponse(
+            responsecode.FORBIDDEN,
+            (caldav_namespace, "calendar-collection-location-ok")
+        )
+
+    def etag(self):
+        return None
+
+    def checkPreconditions(self, request):
+        return None
+
+    ##
+    # ACL
+    ##
+
+    def supportedPrivileges(self, request):
+        return succeed(schedulePrivilegeSet)
+
 class DropBoxHomeFile (AutoProvisioningFileMixIn, DropBoxHomeResource, CalDAVFile):
     def __init__(self, path, parent):
         DropBoxHomeResource.__init__(self)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/tap.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/tap.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/tap.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -21,6 +21,7 @@
 
 from zope.interface import implements
 
+from twisted.internet import reactor
 from twisted.internet.address import IPv4Address
 from twisted.internet import tcp, ssl
 
@@ -47,6 +48,8 @@
 from twistedcaldav.config import config, parseConfig, defaultConfig, ConfigurationError
 from twistedcaldav.root import RootResource
 from twistedcaldav.resource import CalDAVResource
+from twistedcaldav.directory import augment, calendaruserproxy
+from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
 from twistedcaldav.directory.digest import QopDigestCredentialFactory
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
@@ -54,6 +57,7 @@
 from twistedcaldav.httpfactory import HTTP503LoggingFactory, LimitingHTTPFactory, LimitingSite
 from twistedcaldav.inspection import InspectionFactory
 from twistedcaldav.static import CalendarHomeProvisioningFile
+from twistedcaldav.static import IScheduleInboxFile
 from twistedcaldav.static import TimezoneServiceFile
 from twistedcaldav.timezones import TimezoneCache
 from twistedcaldav import pdmonster
@@ -504,6 +508,7 @@
     rootResourceClass            = RootResource
     principalResourceClass       = DirectoryPrincipalProvisioningResource
     calendarResourceClass        = CalendarHomeProvisioningFile
+    iScheduleResourceClass       = IScheduleInboxFile
     timezoneServiceResourceClass = TimezoneServiceFile
 
 
@@ -529,6 +534,32 @@
         setLogLevelForNamespace(None, "info")
 
         #
+        # Setup the Augment Service
+        #
+        augmentClass = namedClass(config.AugmentService.type)
+
+        log.info("Configuring augment service of type: %s" % (augmentClass,))
+
+        try:
+            augment.AugmentService = augmentClass(**config.AugmentService.params)
+        except IOError, e:
+            log.error("Could not start augment service")
+            raise
+
+        #
+        # Setup the ProxyDB Service
+        #
+        proxydbClass = namedClass(config.ProxyDBService.type)
+
+        log.info("Configuring proxydb service of type: %s" % (proxydbClass,))
+
+        try:
+            calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
+        except IOError, e:
+            log.error("Could not start proxydb service")
+            raise
+
+        #
         # Setup the Directory
         #
         directories = []
@@ -564,15 +595,22 @@
                 SudoDirectoryService.recordType_sudoers)
 
         #
+        # Make sure proxies get initialized
+        #
+        if config.ProxyLoadFromFile:
+            def _doProxyUpdate():
+                loader = XMLCalendarUserProxyLoader(config.ProxyLoadFromFile)
+                return loader.updateProxyDB()
+            
+            reactor.addSystemEventTrigger("after", "startup", _doProxyUpdate)
+
+        #
         # Configure Memcached Client Pool
         #
-        if config.Memcached["ClientEnabled"]:
-            memcachepool.installPool(
-                IPv4Address(
-                    'TCP',
-                    config.Memcached["BindAddress"],
-                    config.Memcached["Port"]),
-                config.Memcached["MaxClients"])
+        memcachepool.installPools(
+            config.Memcached.Pools,
+            config.Memcached.MaxClients,
+        )
 
         #
         # Configure NotificationClient
@@ -609,6 +647,17 @@
         root.putChild('principals', principalCollection)
         root.putChild('calendars', calendarCollection)
 
+        # iSchedule service is optional
+        if config.Scheduling.iSchedule.Enabled:
+            log.info("Setting up iSchedule inbox resource: %r"
+                          % (self.iScheduleResourceClass,))
+
+            ischedule = self.iScheduleResourceClass(
+                os.path.join(config.DocumentRoot, "ischedule"),
+                root,
+            )
+            root.putChild("ischedule", ischedule)
+
         # Timezone service is optional
         if config.EnableTimezoneService:
             timezoneService = self.timezoneServiceResourceClass(

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_cache.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_cache.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_cache.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -18,7 +18,6 @@
 import hashlib
 import cPickle
 
-from twisted.trial.unittest import TestCase
 from twisted.internet.defer import succeed, maybeDeferred
 
 from twisted.web2.dav import davxml
@@ -31,6 +30,7 @@
 from twistedcaldav.cache import PropfindCacheMixin
 
 from twistedcaldav.test.util import InMemoryMemcacheProtocol
+from twistedcaldav.test.util import TestCase
 
 
 def _newCacheToken(self):
@@ -87,6 +87,7 @@
 
 class MemCacheChangeNotifierTests(TestCase):
     def setUp(self):
+        TestCase.setUp(self)
         self.memcache = InMemoryMemcacheProtocol()
         self.ccn = MemcacheChangeNotifier(
             StubURLResource(':memory:'),
@@ -336,7 +337,7 @@
         self.tokens['/principals/__uids__/cdaboo/'] = 'principalToken0'
         self.tokens['/principals/__uids__/dreid/'] = 'principalTokenX'
 
-        def _getToken(uri):
+        def _getToken(uri, cachePoolHandle=None):
             return succeed(self.tokens.get(uri))
 
         self.rc._tokenForURI = _getToken
@@ -438,6 +439,7 @@
     Test the PropfindCacheMixin
     """
     def setUp(self):
+        TestCase.setUp(self)
         self.resource = TestCachingResource(StubResponse(200, {}, "foobar"))
         self.responseCache = StubResponseCacheResource()
 

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_config.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_config.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -158,7 +158,6 @@
         self.assertEquals(config.DirectoryService.type, "twistedcaldav.directory.appleopendirectory.OpenDirectoryService")
         self.assertNotIn("xmlFile", config.DirectoryService.params)
         self.assertEquals(config.DirectoryService.params.node, "/Search")
-        self.assertEquals(config.DirectoryService.params.requireComputerRecord, True)
         self.assertEquals(config.DirectoryService.params.cacheTimeout, 30)
 
     def testDirectoryService_newParam(self):
@@ -167,13 +166,11 @@
 
         config.update({"DirectoryService": {"type": "twistedcaldav.directory.appleopendirectory.OpenDirectoryService"}})
         config.update({"DirectoryService": {"params": {
-            "requireComputerRecord": False,
             "cacheTimeout": 12345,
         }}})
 
         self.assertEquals(config.DirectoryService.type, "twistedcaldav.directory.appleopendirectory.OpenDirectoryService")
         self.assertEquals(config.DirectoryService.params.node, "/Search")
-        self.assertEquals(config.DirectoryService.params.requireComputerRecord, False)
         self.assertEquals(config.DirectoryService.params.cacheTimeout, 12345)
 
     def testDirectoryService_unknownType(self):

Copied: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_database.py (from rev 5830, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/test/test_database.py)
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_database.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_database.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -0,0 +1,208 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin
+import twistedcaldav.test.util
+
+from twisted.internet.defer import inlineCallbacks
+
+import os
+import time
+
+class Database (twistedcaldav.test.util.TestCase):
+    """
+    Test abstract SQL DB class
+    """
+    
+    class TestDB(ADBAPISqliteMixin, AbstractADBAPIDatabase):
+        
+        def __init__(self, path, persistent=False, version="1"):
+            self.version = version
+            self.dbpath = path
+            super(Database.TestDB, self).__init__("sqlite", "sqlite3", (path,), persistent, cp_min=3, cp_max=3)
+
+        def _db_version(self):
+            """
+            @return: the schema version assigned to this index.
+            """
+            return self.version
+            
+        def _db_type(self):
+            """
+            @return: the collection type assigned to this index.
+            """
+            return "TESTTYPE"
+            
+        def _db_init_data_tables(self):
+            """
+            Initialise the underlying database tables.
+            @param q:           a database cursor to use.
+            """
+    
+            #
+            # TESTTYPE table
+            #
+            return self._db_execute(
+                """
+                create table TESTTYPE (
+                    KEY         text unique,
+                    VALUE       text
+                )
+                """
+            )
+
+        def _db_remove_data_tables(self):
+            return self._db_execute("drop table TESTTYPE")
+
+    class TestDBRecreateUpgrade(TestDB):
+        
+        class RecreateDBException(Exception):
+            pass
+        class UpgradeDBException(Exception):
+            pass
+
+        def __init__(self, path, persistent=False):
+            super(Database.TestDBRecreateUpgrade, self).__init__(path, persistent, version="2")
+
+        def _db_recreate(self):
+            raise self.RecreateDBException()
+
+    class TestDBCreateIndexOnUpgrade(TestDB):
+        
+        def __init__(self, path, persistent=False):
+            super(Database.TestDBCreateIndexOnUpgrade, self).__init__(path, persistent, version="2")
+
+        def _db_upgrade_data_tables(self, old_version):
+            return self._db_execute(
+                """
+                create index TESTING on TESTTYPE (VALUE)
+                """
+            )
+
+    class TestDBPauseInInit(TestDB):
+        
+        def _db_init(self):
+            
+            time.sleep(1)
+            super(Database.TestDBPauseInInit, self)._db_init()
+
+    @inlineCallbacks
+    def inlineCallbackRaises(self, exc, f, *args, **kwargs):
+        try:
+            yield f(*args, **kwargs)
+        except exc:
+            pass
+        except Exception, e:
+            self.fail("Wrong exception raised: %s" % (e,))
+        else:
+            self.fail("%s not raised" % (exc,))
+
+    @inlineCallbacks
+    def test_connect(self):
+        """
+        Connect to database and create table
+        """
+        db = Database.TestDB(self.mktemp())
+        self.assertFalse(db.initialized)
+        yield db.open()
+        self.assertTrue(db.initialized)
+
+    @inlineCallbacks
+    def test_readwrite(self):
+        """
+        Add a record, search for it
+        """
+        db = Database.TestDB(self.mktemp())
+        yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", ("FOO", "BAR",))
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR"),))
+        items = (yield db.queryList("SELECT * from TESTTYPE"))
+        self.assertEqual(items, ("FOO",))
+
+    @inlineCallbacks
+    def test_close(self):
+        """
+        Close database
+        """
+        db = Database.TestDB(self.mktemp())
+        self.assertFalse(db.initialized)
+        yield db.open()
+        db.close()
+        self.assertFalse(db.initialized)
+        db.close()
+        
+    @inlineCallbacks
+    def test_version_upgrade_nonpersistent(self):
+        """
+        Connect to database and create table
+        """
+        
+        db_file = self.mktemp()
+
+        db = Database.TestDB(db_file)
+        yield db.open()
+        yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", ("FOO", "BAR",))
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR"),))
+        db.close()
+        db = None
+
+        db = Database.TestDBRecreateUpgrade(db_file)
+        yield self.inlineCallbackRaises(Database.TestDBRecreateUpgrade.RecreateDBException, db.open)
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, ())
+
+    def test_version_upgrade_persistent(self):
+        """
+        Connect to database and create table
+        """
+        db_file = self.mktemp()
+        db = Database.TestDB(db_file, persistent=True)
+        yield db.open()
+        yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", "FOO", "BAR")
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR")))
+        db.close()
+        db = None
+
+        db = Database.TestDBRecreateUpgrade(db_file, persistent=True)
+        yield self.inlineCallbackRaises(NotImplementedError, db.open)
+        self.assertTrue(os.path.exists(db_file))
+        db.close()
+        db = None
+
+        db = Database.TestDB(db_file, persistent=True, autocommit=True)
+        yield db.open()
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR")))
+
+    def test_version_upgrade_persistent_add_index(self):
+        """
+        Connect to database and create table
+        """
+        db_file = self.mktemp()
+        db = Database.TestDB(db_file, persistent=True, autocommit=True)
+        yield db.open()
+        yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", "FOO", "BAR")
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR")))
+        db.close()
+        db = None
+
+        db = Database.TestDBCreateIndexOnUpgrade(db_file, persistent=True, autocommit=True)
+        yield db.open()
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR")))

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_log.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_log.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_log.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -17,7 +17,7 @@
 from twistedcaldav.log import *
 from twistedcaldav.log import logLevelsByNamespace
 
-import twisted.trial.unittest
+from twistedcaldav.test.util import TestCase
 
 defaultLogLevel = logLevelsByNamespace[None]
 
@@ -37,7 +37,7 @@
 class LoggingEnabledObject (LoggingMixIn):
     pass
 
-class Logging (twisted.trial.unittest.TestCase):
+class Logging (TestCase):
     def test_cmpLogLevels(self):
         self.assertEquals(cmpLogLevels("info" , "error"), -1)
         self.assertEquals(cmpLogLevels("debug", "debug"),  0)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcache.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcache.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcache.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -7,14 +7,12 @@
 
 from twistedcaldav.memcache import MemCacheProtocol, NoSuchCommand
 from twistedcaldav.memcache import ClientError, ServerError
+from twistedcaldav.test.util import TestCase
 
-from twisted.trial.unittest import TestCase
 from twisted.test.proto_helpers import StringTransportWithDisconnection
 from twisted.internet.task import Clock
 from twisted.internet.defer import Deferred, gatherResults
 
-
-
 class MemCacheTestCase(TestCase):
     """
     Test client protocol class L{MemCacheProtocol}.
@@ -25,6 +23,7 @@
         Create a memcache client, connect it to a string protocol, and make it
         use a deterministic clock.
         """
+        TestCase.setUp(self)
         self.proto = MemCacheProtocol()
         self.clock = Clock()
         self.proto.callLater = self.clock.callLater

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcachepool.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcachepool.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcachepool.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -16,8 +16,6 @@
 
 from zope.interface import implements
 
-from twisted.trial.unittest import TestCase
-
 from twisted.internet.interfaces import IConnector, IReactorTCP
 from twisted.internet.address import IPv4Address
 
@@ -25,8 +23,8 @@
 from twistedcaldav.memcachepool import PooledMemCacheProtocol
 from twistedcaldav.memcachepool import MemCacheClientFactory
 from twistedcaldav.memcachepool import MemCachePool
+from twistedcaldav.test.util import TestCase
 
-
 MC_ADDRESS = IPv4Address('TCP', '127.0.0.1', 11211)
 
 
@@ -137,6 +135,7 @@
         Create a L{MemCacheClientFactory} instance and and give it a
         L{StubConnectionPool} instance.
         """
+        TestCase.setUp(self)
         self.pool = StubConnectionPool()
         self.factory = MemCacheClientFactory()
         self.factory.connectionPool = self.pool
@@ -190,6 +189,7 @@
         """
         Create a L{MemCachePool}.
         """
+        TestCase.setUp(self)
         self.reactor = StubReactor()
         self.pool = MemCachePool(MC_ADDRESS,
                                  maxClients=5,

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcacher.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcacher.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_memcacher.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -6,10 +6,10 @@
 """
 
 from twisted.internet.defer import inlineCallbacks
-from twistedcaldav.test.util import TestCase
 
 from twistedcaldav.config import config
 from twistedcaldav.memcacher import Memcacher
+from twistedcaldav.test.util import TestCase
 
 class MemcacherTestCase(TestCase):
     """

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_resource.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_resource.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -14,13 +14,11 @@
 # limitations under the License.
 ##
 
-from twistedcaldav.test.util import TestCase
-
 from twistedcaldav.resource import CalDAVResource
 
 from twistedcaldav.test.util import InMemoryPropertyStore
+from twistedcaldav.test.util import TestCase
 
-
 class StubProperty(object):
     def qname(self):
         return "StubQname"

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_root.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_root.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_root.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -18,9 +18,10 @@
 
 from twistedcaldav.root import RootResource
 from twistedcaldav.test.util import TestCase
+from twistedcaldav.directory import augment
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
 
 from twisted.cred.portal import Portal
 
@@ -60,6 +61,7 @@
                 'calendar': ['dreid']})
 
         directory = XMLDirectoryService(xmlFile)
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
 
         principals = DirectoryPrincipalProvisioningResource('/principals/', directory)
 

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_static.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_static.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_static.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -14,11 +14,10 @@
 # limitations under the License.
 ##
 
-from twistedcaldav.test.util import TestCase
-
 from twistedcaldav.static import CalendarHomeFile, CalDAVFile
 from twistedcaldav.cache import DisabledCacheNotifier
 from twistedcaldav.test.util import StubCacheChangeNotifier
+from twistedcaldav.test.util import TestCase
 
 class StubParentResource(object):
     def principalCollections(self):

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_tap.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_tap.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_tap.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -20,8 +20,6 @@
 
 from zope.interface import implements
 
-from twistedcaldav.test.util import TestCase
-
 from twisted.python.usage import Options, UsageError
 from twisted.python.util import sibpath
 from twisted.python.reflect import namedAny
@@ -50,6 +48,8 @@
 from twistedcaldav.directory.sudo import SudoDirectoryService
 from twistedcaldav.directory.directory import UnknownRecordTypeError
 
+from twistedcaldav.test.util import TestCase
+
 class NotAProcessTransport(object):
     """
     Simple L{IProcessTransport} stub.
@@ -255,12 +255,19 @@
 
         accountsFile = sibpath(os.path.dirname(__file__),
                                'directory/test/accounts.xml')
+        augmentsFile = sibpath(os.path.dirname(__file__),
+                               'directory/test/augments.xml')
 
         self.config['DirectoryService'] = {
             'params': {'xmlFile': accountsFile},
             'type': 'twistedcaldav.directory.xmlfile.XMLDirectoryService'
             }
 
+        self.config["AugmentService"] = {
+            "params": {"xmlFiles": [augmentsFile]},
+            "type": "twistedcaldav.directory.augment.AugmentXMLDB"
+        }
+
         self.config['DocumentRoot'] = self.mktemp()
         self.config['DataRoot'] = self.mktemp()
         self.config['ProcessType'] = 'Slave'
@@ -269,6 +276,9 @@
 
         self.config['SudoersFile'] = ''
 
+        self.config['Memcached']['Pools']['Default']['ClientEnabled'] = False
+        self.config['Memcached']['Pools']['Default']['ServerEnabled'] = False
+
         if self.configOptions:
             config_mod._mergeData(self.config, self.configOptions)
 

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/util.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/util.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -41,8 +41,8 @@
         dataroot = self.mktemp()
         os.mkdir(dataroot)
         config.DataRoot = dataroot
-        config.Memcached.ClientEnabled = False
-        config.Memcached.ServerEnabled = False
+        config.Memcached.Pools.Default.ClientEnabled = False
+        config.Memcached.Pools.Default.ServerEnabled = False
         memcacheclient.ClientFactory.allowTestCache = True
 
     def createHierarchy(self, structure):

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/util.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/util.py	2010-07-01 22:05:26 UTC (rev 5832)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/util.py	2010-07-01 22:12:06 UTC (rev 5833)
@@ -108,3 +108,8 @@
         state = self._state
         self._state = not state
         return state
+
+def utf8String(s):
+    if isinstance(s, unicode):
+        s = s.encode("utf-8")
+    return s
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100701/32e94a05/attachment-0001.html>


More information about the calendarserver-changes mailing list