[CalendarServer-changes] [11029] CalendarServer/branches/users/cdaboo/store-scheduling

source_changes at macosforge.org source_changes at macosforge.org
Thu Apr 11 09:24:18 PDT 2013


Revision: 11029
          http://trac.calendarserver.org//changeset/11029
Author:   cdaboo at apple.com
Date:     2013-04-11 09:24:18 -0700 (Thu, 11 Apr 2013)
Log Message:
-----------
Merge from trunk.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/store-scheduling/README
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/accesslog.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/amppush.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/applepush.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/notifier.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_amppush.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_applepush.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_notifier.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/util.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/caldav.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/test/test_caldav.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/util.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/calverify.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/cmdline.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/config.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/gateway.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/principals.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/purge.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/push.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/gateway/caldavd.plist
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/principals/caldavd.plist
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_calverify.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_gateway.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_principals.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/upgrade.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/util.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/webadmin/resource.py
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/webadmin/test/test_resource.py
    CalendarServer/branches/users/cdaboo/store-scheduling/conf/auth/augments-test.xml
    CalendarServer/branches/users/cdaboo/store-scheduling/conf/auth/resources-test.xml
    CalendarServer/branches/users/cdaboo/store-scheduling/conf/caldavd-apple.plist
    CalendarServer/branches/users/cdaboo/store-scheduling/conf/caldavd-test.plist
    CalendarServer/branches/users/cdaboo/store-scheduling/contrib/performance/loadtest/ical.py
    CalendarServer/branches/users/cdaboo/store-scheduling/run
    CalendarServer/branches/users/cdaboo/store-scheduling/setup.py
    CalendarServer/branches/users/cdaboo/store-scheduling/support/build.sh
    CalendarServer/branches/users/cdaboo/store-scheduling/support/submit
    CalendarServer/branches/users/cdaboo/store-scheduling/twext/enterprise/queue.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twext/enterprise/test/test_queue.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twext/python/sendfd.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twisted/plugins/caldav.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/cache.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/config.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/ldapdirectory.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/accounts-modified.xml
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/accounts.xml
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/augments.xml
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/proxies.xml
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_directory.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_opendirectory.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_proxyprincipalmembers.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/ical.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/propfind.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/caldav/delivery.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/caldav/resource.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/imip/inbound.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/imip/test/test_inbound.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/implicit.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/ischedule/utils.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/storebridge.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_config.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_icalendar.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_validation.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/util.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/upgrade.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/util.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/base/datastore/dbapiclient.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/base/propertystore/xattr.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/delivery.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/resource.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/imip/inbound.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/implicit.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/ischedule/utils.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/common.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_file.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/file.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v14.sql
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v15.sql
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_12_to_13.sql
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/store-scheduling/.gitignore
    CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_config.py
    CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/RFC6764-srv-CalDAV.txt
    CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/RFC6868-Parameter Value Encoding.txt
    CalendarServer/branches/users/cdaboo/store-scheduling/doc/calendarserver_config.8
    CalendarServer/branches/users/cdaboo/store-scheduling/support/Apple.make
    CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/
    CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.pbxproj
    CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.xcworkspace/
    CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.xcworkspace/contents.xcworkspacedata
    CalendarServer/branches/users/cdaboo/store-scheduling/support/XCode.make
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v17.sql
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/postgres-dialect/v17.sql
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_17_to_18.sql
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_17_to_18.sql

Removed Paths:
-------------
    CalendarServer/branches/users/cdaboo/store-scheduling/contrib/certupdate/
    CalendarServer/branches/users/cdaboo/store-scheduling/contrib/migration/
    CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/draft-daboo-srv-caldav.txt
    CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.tmproj
    CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/
    CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.pbxproj
    CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.xcworkspace/
    CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.xcworkspace/contents.xcworkspacedata
    CalendarServer/branches/users/cdaboo/store-scheduling/support/Makefile.Apple

Property Changed:
----------------
    CalendarServer/branches/users/cdaboo/store-scheduling/
    CalendarServer/branches/users/cdaboo/store-scheduling/conf/auth/resources-test.xml


Property changes on: CalendarServer/branches/users/cdaboo/store-scheduling
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593
/CalendarServer/trunk:10876-11028

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/.gitignore (from rev 11028, CalendarServer/trunk/.gitignore)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/.gitignore	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/.gitignore	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,8 @@
+*.pyc
+*.so
+dropin.cache
+
+/build/
+/data/
+/calendarserver/version.py
+/conf/caldavd-dev.plist
\ No newline at end of file

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/README
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/README	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/README	2013-04-11 16:24:18 UTC (rev 11029)
@@ -110,4 +110,3 @@
 8443 for HTTPS.  You should then be able to connect to the server
 using your web browser (eg. Safari, Firefox) or with a CalDAV client
 (eg. iCal).
-

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/accesslog.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/accesslog.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/accesslog.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -33,6 +33,7 @@
     import psutil
 except ImportError:
     psutil = None
+from sys import platform
 import time
 
 from calendarserver.logAnalysis import getAdjustedMethodName, \
@@ -627,7 +628,7 @@
             self.previous_cpu = cpu_now
 
         # Memory usage
-        if psutil is not None:
+        if psutil is not None and 'freebsd' not in platform:
             mem = psutil.virtual_memory()
             self.items["memory used"] = mem.used
             self.items["memory percent"] = mem.percent

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/amppush.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/amppush.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/amppush.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -17,7 +17,7 @@
 from calendarserver.push.util import PushScheduler
 from twext.python.log import Logger, LoggingMixIn
 from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.internet.endpoints import TCP4ClientEndpoint 
+from twisted.internet.endpoints import TCP4ClientEndpoint
 from twisted.internet.protocol import Factory, ServerFactory
 from twisted.protocols import amp
 import time
@@ -38,11 +38,13 @@
     response = [('status', amp.String())]
 
 
+
 class UnsubscribeFromID(amp.Command):
     arguments = [('token', amp.String()), ('id', amp.String())]
     response = [('status', amp.String())]
 
 
+
 # AMP Commands sent to client (and forwarded to Master)
 
 class NotificationForID(amp.Command):
@@ -50,6 +52,7 @@
     response = [('status', amp.String())]
 
 
+
 # Server classes
 
 class AMPPushForwardingFactory(Factory, LoggingMixIn):
@@ -57,11 +60,14 @@
     def __init__(self, forwarder):
         self.forwarder = forwarder
 
+
     def buildProtocol(self, addr):
         protocol = amp.AMP()
         self.forwarder.protocols.append(protocol)
         return protocol
 
+
+
 class AMPPushForwarder(LoggingMixIn):
     """
     Runs in the slaves, forwards notifications to the master via AMP
@@ -70,8 +76,9 @@
         self.protocols = []
         controlSocket.addFactory(PUSH_ROUTE, AMPPushForwardingFactory(self))
 
+
     @inlineCallbacks
-    def enqueue(self, id, dataChangedTimestamp=None):
+    def enqueue(self, transaction, id, dataChangedTimestamp=None):
         if dataChangedTimestamp is None:
             dataChangedTimestamp = int(time.time())
         for protocol in self.protocols:
@@ -88,24 +95,28 @@
         super(AMPPushMasterListeningProtocol, self).__init__()
         self.master = master
 
+
     @NotificationForID.responder
     def enqueueFromWorker(self, id, dataChangedTimestamp=None):
         if dataChangedTimestamp is None:
             dataChangedTimestamp = int(time.time())
-        self.master.enqueue(id, dataChangedTimestamp=dataChangedTimestamp)
+        self.master.enqueue(None, id, dataChangedTimestamp=dataChangedTimestamp)
         return {"status" : "OK"}
- 
 
+
+
 class AMPPushMasterListenerFactory(Factory, LoggingMixIn):
 
     def __init__(self, master):
         self.master = master
 
+
     def buildProtocol(self, addr):
         protocol = AMPPushMasterListeningProtocol(self.master)
         return protocol
 
 
+
 class AMPPushMaster(LoggingMixIn):
     """
     AMPPushNotifierService allows clients to use AMP to subscribe to,
@@ -137,15 +148,18 @@
         else:
             self.scheduler = None
 
+
     def addSubscriber(self, p):
         self.log_debug("Added subscriber")
         self.subscribers.append(p)
 
+
     def removeSubscriber(self, p):
         self.log_debug("Removed subscriber")
         self.subscribers.remove(p)
 
-    def enqueue(self, pushKey, dataChangedTimestamp=None):
+
+    def enqueue(self, transaction, pushKey, dataChangedTimestamp=None):
         """
         Sends an AMP push notification to any clients subscribing to this pushKey.
 
@@ -189,6 +203,7 @@
                 yield self.sendNotification(token, id, dataChangedTimestamp)
 
 
+
 class AMPPushNotifierProtocol(amp.AMP, LoggingMixIn):
 
     def __init__(self, service):
@@ -197,6 +212,7 @@
         self.subscriptions = {}
         self.any = None
 
+
     def subscribe(self, token, id):
         if id == "any":
             self.any = token
@@ -219,15 +235,18 @@
             return self.callRemote(NotificationForID, id=id,
                 dataChangedTimestamp=dataChangedTimestamp)
 
+
     def subscribedToID(self, id):
         if self.any is not None:
             return self.any
         return self.subscriptions.get(id, None)
 
+
     def connectionLost(self, reason=None):
         self.service.removeSubscriber(self)
 
 
+
 class AMPPushNotifierFactory(ServerFactory, LoggingMixIn):
 
     protocol = AMPPushNotifierProtocol
@@ -235,6 +254,7 @@
     def __init__(self, service):
         self.service = service
 
+
     def buildProtocol(self, addr):
         p = self.protocol(self.service)
         self.service.addSubscriber(p)
@@ -242,6 +262,7 @@
         return p
 
 
+
 # Client classes
 
 class AMPPushClientProtocol(amp.AMP):
@@ -255,14 +276,16 @@
         super(AMPPushClientProtocol, self).__init__()
         self.callback = callback
 
+
     @inlineCallbacks
     def notificationForID(self, id, dataChangedTimestamp):
         yield self.callback(id, dataChangedTimestamp)
-        returnValue( {"status" : "OK"} )
+        returnValue({"status" : "OK"})
 
     NotificationForID.responder(notificationForID)
 
 
+
 class AMPPushClientFactory(Factory, LoggingMixIn):
 
     protocol = AMPPushClientProtocol
@@ -270,11 +293,13 @@
     def __init__(self, callback):
         self.callback = callback
 
+
     def buildProtocol(self, addr):
         p = self.protocol(self.callback)
         return p
 
 
+
 # Client helper methods
 
 @inlineCallbacks

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/applepush.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/applepush.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/applepush.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -176,7 +176,7 @@
 
 
     @inlineCallbacks
-    def enqueue(self, pushKey, dataChangedTimestamp=None):
+    def enqueue(self, transaction, pushKey, dataChangedTimestamp=None):
         """
         Sends an Apple Push Notification to any device token subscribed to
         this pushKey.
@@ -207,9 +207,7 @@
         if provider is not None:
 
             # Look up subscriptions for this key
-            txn = self.store.newTransaction()
-            subscriptions = (yield txn.apnSubscriptionsByKey(pushKey))
-            yield txn.commit()
+            subscriptions = (yield transaction.apnSubscriptionsByKey(pushKey))
 
             numSubscriptions = len(subscriptions)
             if numSubscriptions > 0:
@@ -230,11 +228,11 @@
     """
 
     # Sent by provider
-    COMMAND_SIMPLE   = 0
+    COMMAND_SIMPLE = 0
     COMMAND_ENHANCED = 1
 
     # Received by provider
-    COMMAND_ERROR    = 8
+    COMMAND_ERROR = 8
 
     # Returned only for an error.  Successful notifications get no response.
     STATUS_CODES = {
@@ -261,6 +259,7 @@
         self.log_debug("ProviderProtocol makeConnection")
         protocol.Protocol.makeConnection(self, transport)
 
+
     def connectionMade(self):
         self.log_debug("ProviderProtocol connectionMade")
         self.buffer = ""
@@ -269,11 +268,13 @@
         self.factory.connection = self
         self.factory.clientConnectionMade()
 
+
     def connectionLost(self, reason=None):
         # self.log_debug("ProviderProtocol connectionLost: %s" % (reason,))
         # Clear the reference to us from the factory
         self.factory.connection = None
 
+
     @inlineCallbacks
     def dataReceived(self, data, fn=None):
         """
@@ -323,7 +324,7 @@
                     (token,))
                 txn = self.factory.store.newTransaction()
                 subscriptions = (yield txn.apnSubscriptionsByToken(token))
-                for key, modified, uid in subscriptions:
+                for key, _ignore_modified, _ignore_uid in subscriptions:
                     self.log_debug("Removing subscription: %s %s" %
                         (token, key))
                     yield txn.removeAPNSubscription(token, key)
@@ -378,6 +379,7 @@
         )
 
 
+
 class APNProviderFactory(ReconnectingClientFactory, LoggingMixIn):
 
     protocol = APNProviderProtocol
@@ -389,26 +391,31 @@
         self.maxDelay = 30 # max seconds between connection attempts
         self.shuttingDown = False
 
+
     def clientConnectionMade(self):
         self.log_info("Connection to APN server made")
         self.service.clientConnectionMade()
         self.delay = 1.0
 
+
     def clientConnectionLost(self, connector, reason):
         if not self.shuttingDown:
             self.log_info("Connection to APN server lost: %s" % (reason,))
         ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
 
+
     def clientConnectionFailed(self, connector, reason):
         self.log_error("Unable to connect to APN server: %s" % (reason,))
         self.connected = False
         ReconnectingClientFactory.clientConnectionFailed(self, connector,
             reason)
 
+
     def retry(self, connector=None):
         self.log_info("Reconnecting to APN server")
         ReconnectingClientFactory.retry(self, connector)
 
+
     def stopTrying(self):
         self.shuttingDown = True
         ReconnectingClientFactory.stopTrying(self)
@@ -434,6 +441,7 @@
             from twisted.internet import reactor
         self.reactor = reactor
 
+
     def connect(self, factory):
         if self.testConnector is not None:
             # For testing purposes
@@ -475,11 +483,13 @@
         else:
             self.scheduler = None
 
+
     def startService(self):
         self.log_debug("APNProviderService startService")
         self.factory = APNProviderFactory(self, self.store)
         self.connect(self.factory)
 
+
     def stopService(self):
         self.log_debug("APNProviderService stopService")
         if self.factory is not None:
@@ -487,6 +497,7 @@
         if self.scheduler is not None:
             self.scheduler.stop()
 
+
     def clientConnectionMade(self):
         # Service the queue
         if self.queue:
@@ -551,7 +562,6 @@
                 self.queue.append(((token, key), dataChangedTimestamp))
 
 
-
     def sendNotification(self, token, key, dataChangedTimestamp):
         """
         If there is a connection the notification is sent right away, otherwise
@@ -588,6 +598,7 @@
         self.log_debug("FeedbackProtocol connectionMade")
         self.buffer = ""
 
+
     @inlineCallbacks
     def dataReceived(self, data, fn=None):
         """
@@ -606,7 +617,7 @@
             self.buffer = self.buffer[self.MESSAGE_LENGTH:]
 
             try:
-                timestamp, tokenLength, binaryToken = struct.unpack("!IH32s",
+                timestamp, _ignore_tokenLength, binaryToken = struct.unpack("!IH32s",
                     message)
                 token = binaryToken.encode("hex").lower()
                 yield fn(timestamp, token)
@@ -614,6 +625,7 @@
                 self.log_warn("FeedbackProtocol could not process message: %s (%s)" %
                     (message.encode("hex"), e))
 
+
     @inlineCallbacks
     def processFeedback(self, timestamp, token):
         """
@@ -634,7 +646,7 @@
         txn = self.factory.store.newTransaction()
         subscriptions = (yield txn.apnSubscriptionsByToken(token))
 
-        for key, modified, uid in subscriptions:
+        for key, modified, _ignore_uid in subscriptions:
             if timestamp > modified:
                 self.log_debug("FeedbackProtocol removing subscription: %s %s" %
                     (token, key))
@@ -650,6 +662,7 @@
     def __init__(self, store):
         self.store = store
 
+
     def clientConnectionFailed(self, connector, reason):
         self.log_error("Unable to connect to APN feedback server: %s" %
             (reason,))
@@ -657,6 +670,7 @@
         ClientFactory.clientConnectionFailed(self, connector, reason)
 
 
+
 class APNFeedbackService(APNConnectionService):
 
     def __init__(self, store, updateSeconds, host, port,
@@ -670,16 +684,19 @@
         self.store = store
         self.updateSeconds = updateSeconds
 
+
     def startService(self):
         self.log_debug("APNFeedbackService startService")
         self.factory = APNFeedbackFactory(self.store)
         self.checkForFeedback()
 
+
     def stopService(self):
         self.log_debug("APNFeedbackService stopService")
         if self.nextCheck is not None:
             self.nextCheck.cancel()
 
+
     def checkForFeedback(self):
         self.nextCheck = None
         self.log_debug("APNFeedbackService checkForFeedback")
@@ -707,17 +724,21 @@
         self.parent = parent
         self.store = store
 
+
     def deadProperties(self):
         if not hasattr(self, "_dead_properties"):
             self._dead_properties = NonePropertyStore(self)
         return self._dead_properties
 
+
     def etag(self):
         return succeed(None)
 
+
     def checkPreconditions(self, request):
         return None
 
+
     def defaultAccessControlList(self):
         return davxml.ACL(
             # DAV:Read for authenticated principals
@@ -738,21 +759,27 @@
             ),
         )
 
+
     def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8");
+        return MimeType.fromString("text/html; charset=utf-8")
 
+
     def resourceType(self):
         return None
 
+
     def isCollection(self):
         return False
 
+
     def isCalendarCollection(self):
         return False
 
+
     def isPseudoCalendarCollection(self):
         return False
 
+
     @inlineCallbacks
     def http_POST(self, request):
         yield self.authorize(request, (davxml.Write(),))
@@ -774,6 +801,7 @@
             if principal is not None:
                 return principal
 
+
     @inlineCallbacks
     def processSubscription(self, request):
         """
@@ -814,6 +842,7 @@
 
         returnValue((code, msg))
 
+
     @inlineCallbacks
     def addSubscription(self, token, key, uid, userAgent, host):
         """
@@ -839,6 +868,7 @@
         yield txn.addAPNSubscription(token, key, now, uid, userAgent, host)
         yield txn.commit()
 
+
     def renderResponse(self, code, body=None):
         response = Response(code, {}, body)
         response.headers.setHeader("content-type", MimeType("text", "html"))

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/notifier.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/notifier.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/notifier.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -32,19 +32,19 @@
 
 class PushNotificationWork(WorkItem, fromTable(schema.PUSH_NOTIFICATION_WORK)):
 
-    group = "PUSH_ID"
+    group = property(lambda self: self.pushID)
 
     @inlineCallbacks
     def doWork(self):
 
         # Delete all other work items with the same pushID
         yield Delete(From=self.table,
-                     Where=self.table.PUSH_ID == self.pushID 
+                     Where=self.table.PUSH_ID == self.pushID
                     ).on(self.transaction)
 
         pushDistributor = self.transaction._pushDistributor
         if pushDistributor is not None:
-            yield pushDistributor.enqueue(self.pushID)
+            yield pushDistributor.enqueue(self.transaction, self.pushID)
 
 
 
@@ -60,10 +60,11 @@
 
     def __init__(self, notifierFactory, label="default", id=None, prefix=None):
         self._notifierFactory = notifierFactory
-        self._ids = { label : self.normalizeID(id) }
+        self._ids = {label : self.normalizeID(id) }
         self._notify = True
         self._prefix = prefix
 
+
     def normalizeID(self, id):
         urn = "urn:uuid:"
         try:
@@ -73,14 +74,17 @@
             pass
         return id
 
+
     def enableNotify(self, arg):
         self.log_debug("enableNotify: %s" % (self._ids['default'][1],))
         self._notify = True
 
+
     def disableNotify(self):
         self.log_debug("disableNotify: %s" % (self._ids['default'][1],))
         self._notify = False
 
+
     @inlineCallbacks
     def notify(self):
         for label in self._ids.iterkeys():
@@ -93,6 +97,7 @@
                 else:
                     self.log_debug("Skipping notification for: %s" % (id,))
 
+
     def clone(self, label="default", id=None):
         newNotifier = self.__class__(self._notifierFactory)
         newNotifier._ids = self._ids.copy()
@@ -100,9 +105,11 @@
         newNotifier._prefix = self._prefix
         return newNotifier
 
+
     def addID(self, label="default", id=None):
         self._ids[label] = self.normalizeID(id)
 
+
     def getID(self, label="default"):
         id = self._ids.get(label, None)
         if self._prefix is None:
@@ -110,11 +117,13 @@
         else:
             return "%s|%s" % (self._prefix, id)
 
+
     def nodeName(self, label="default"):
         id = self.getID(label=label)
         return succeed(self._notifierFactory.pushKeyForId(id))
 
 
+
 class NotifierFactory(LoggingMixIn):
     """
     Notifier Factory
@@ -132,6 +141,7 @@
             from twisted.internet import reactor
         self.reactor = reactor
 
+
     @inlineCallbacks
     def send(self, id):
         txn = self.store.newTransaction()
@@ -141,9 +151,11 @@
             notBefore=notBefore)
         yield txn.commit()
 
+
     def newNotifier(self, label="default", id=None, prefix=None):
         return Notifier(self, label=label, id=id, prefix=prefix)
 
+
     def pushKeyForId(self, id):
         path = "/"
 
@@ -190,6 +202,7 @@
     return None
 
 
+
 class PushDistributor(object):
     """
     Distributes notifications to the protocol-specific subservices
@@ -198,18 +211,22 @@
     def __init__(self, observers):
         """
         @param observers: the list of observers to distribute pushKeys to
-        @type observers: C{list} 
+        @type observers: C{list}
         """
         # TODO: add an IPushObservers interface?
-        self.observers = observers 
+        self.observers = observers
 
+
     @inlineCallbacks
-    def enqueue(self, pushKey):
+    def enqueue(self, transaction, pushKey):
         """
         Pass along enqueued pushKey to any observers
 
+        @param transaction: a transaction to use, if needed
+        @type transaction: L{CommonStoreTransaction}
+
         @param pushKey: the push key to distribute to the observers
         @type pushKey: C{str}
         """
         for observer in self.observers:
-            yield observer.enqueue(pushKey)
+            yield observer.enqueue(transaction, pushKey)

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_amppush.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_amppush.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_amppush.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -57,7 +57,7 @@
         self.assertTrue(client3.subscribedToID("/CalDAV/localhost/user03/"))
 
         dataChangedTimestamp = 1354815999
-        service.enqueue("/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
+        service.enqueue(None, "/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
         self.assertEquals(len(client1.history), 0)
         self.assertEquals(len(client2.history), 0)
         self.assertEquals(len(client3.history), 0)
@@ -74,7 +74,7 @@
         client1.reset()
         client2.reset()
         client2.unsubscribe("token2", "/CalDAV/localhost/user01/")
-        service.enqueue("/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
+        service.enqueue(None, "/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
         self.assertEquals(len(client1.history), 0)
         clock.advance(1)
         self.assertEquals(client1.history, [(NotificationForID, {'id': '/CalDAV/localhost/user01/', 'dataChangedTimestamp' : 1354815999})])
@@ -87,19 +87,22 @@
         client1.reset()
         client2.reset()
         client2.subscribe("token2", "/CalDAV/localhost/user01/")
-        service.enqueue("/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
+        service.enqueue(None, "/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
         self.assertEquals(client1.history, [(NotificationForID, {'id': '/CalDAV/localhost/user01/', 'dataChangedTimestamp' : 1354815999})])
         self.assertEquals(client2.history, [(NotificationForID, {'id': '/CalDAV/localhost/user01/', 'dataChangedTimestamp' : 1354815999})])
 
 
+
 class TestProtocol(AMPPushNotifierProtocol):
 
     def __init__(self, service):
         super(TestProtocol, self).__init__(service)
         self.reset()
 
+
     def callRemote(self, cls, **kwds):
         self.history.append((cls, kwds))
 
+
     def reset(self):
         self.history = []

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_applepush.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_applepush.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_applepush.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -34,6 +34,7 @@
         yield super(ApplePushNotifierServiceTests, self).setUp()
         self.store = yield buildStore(self, None)
 
+
     @inlineCallbacks
     def test_ApplePushNotifierService(self):
 
@@ -65,7 +66,6 @@
             },
         }
 
-
         # Add subscriptions
         txn = self.store.newTransaction()
 
@@ -79,7 +79,7 @@
         except InvalidSubscriptionValues:
             pass
 
-        token  = "2d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219df"
+        token = "2d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219df"
         token2 = "3d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219df"
         key1 = "/CalDAV/calendars.example.com/user01/calendar/"
         timestamp1 = 1000
@@ -117,16 +117,18 @@
         clock = Clock()
         service = (yield ApplePushNotifierService.makeService(settings,
             self.store, testConnectorClass=TestConnector, reactor=clock))
-        self.assertEquals(set(service.providers.keys()), set(["CalDAV","CardDAV"]))
-        self.assertEquals(set(service.feedbacks.keys()), set(["CalDAV","CardDAV"]))
+        self.assertEquals(set(service.providers.keys()), set(["CalDAV", "CardDAV"]))
+        self.assertEquals(set(service.feedbacks.keys()), set(["CalDAV", "CardDAV"]))
 
         # First, enqueue a notification while we have no connection, in this
         # case by doing it prior to startService()
 
         # Notification arrives from calendar server
         dataChangedTimestamp = 1354815999
-        yield service.enqueue("/CalDAV/calendars.example.com/user01/calendar/",
+        txn = self.store.newTransaction()
+        yield service.enqueue(txn, "/CalDAV/calendars.example.com/user01/calendar/",
             dataChangedTimestamp=dataChangedTimestamp)
+        yield txn.commit()
 
         # The notifications should be in the queue
         self.assertTrue(((token, key1), dataChangedTimestamp) in service.providers["CalDAV"].queue)
@@ -152,12 +154,11 @@
         payload = json.loads(payload[0])
         self.assertEquals(payload["key"], u"/CalDAV/calendars.example.com/user01/calendar/")
         self.assertEquals(payload["dataChangedTimestamp"], dataChangedTimestamp)
-        self.assertTrue(payload.has_key("pushRequestSubmittedTimestamp"))
+        self.assertTrue("pushRequestSubmittedTimestamp" in payload)
         # Verify token history is updated
-        self.assertTrue(token in [t for (i, t) in providerConnector.service.protocol.history.history])
-        self.assertTrue(token2 in [t for (i, t) in providerConnector.service.protocol.history.history])
+        self.assertTrue(token in [t for (_ignore_i, t) in providerConnector.service.protocol.history.history])
+        self.assertTrue(token2 in [t for (_ignore_i, t) in providerConnector.service.protocol.history.history])
 
-
         #
         # Verify staggering behavior
         #
@@ -165,7 +166,9 @@
         # Reset sent data
         providerConnector.transport.data = None
         # Send notification while service is connected
-        yield service.enqueue("/CalDAV/calendars.example.com/user01/calendar/")
+        txn = self.store.newTransaction()
+        yield service.enqueue(txn, "/CalDAV/calendars.example.com/user01/calendar/")
+        yield txn.commit()
         clock.advance(1) # so that first push is sent
         self.assertEquals(len(providerConnector.transport.data), 183)
         # Reset sent data
@@ -173,13 +176,13 @@
         clock.advance(3) # so that second push is sent
         self.assertEquals(len(providerConnector.transport.data), 183)
 
+        history = []
 
         def errorTestFunction(status, identifier):
             history.append((status, identifier))
             return succeed(None)
 
         # Simulate an error
-        history = []
         errorData = struct.pack("!BBI", APNProviderProtocol.COMMAND_ERROR, 1, 2)
         yield providerConnector.receiveData(errorData, fn=errorTestFunction)
         clock.advance(301)
@@ -204,14 +207,12 @@
         # Buffer has 1 byte remaining
         self.assertEquals(len(providerConnector.service.protocol.buffer), 1)
 
-
         # Prior to feedback, there are 2 subscriptions
         txn = self.store.newTransaction()
         subscriptions = (yield txn.apnSubscriptionsByToken(token))
         yield txn.commit()
         self.assertEquals(len(subscriptions), 2)
 
-
         # Simulate feedback with a single token
         feedbackConnector = service.feedbacks["CalDAV"].testConnector
         timestamp = 2000
@@ -294,6 +295,7 @@
 
         service.stopService()
 
+
     def test_validToken(self):
         self.assertTrue(validToken("2d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219df"))
         self.assertFalse(validToken("d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219df"))
@@ -359,6 +361,7 @@
         )
 
 
+
 class TestConnector(object):
 
     def connect(self, service, factory):
@@ -368,14 +371,17 @@
         self.transport = StubTransport()
         service.protocol.makeConnection(self.transport)
 
+
     def receiveData(self, data, fn=None):
         return self.service.protocol.dataReceived(data, fn=fn)
 
 
+
 class StubTransport(object):
 
     def __init__(self):
         self.data = None
 
+
     def write(self, data):
         self.data = data

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_notifier.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_notifier.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/test/test_notifier.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -27,22 +27,27 @@
     def __init__(self):
         self.reset()
 
+
     def reset(self):
         self.history = []
 
-    def enqueue(self, id):
+
+    def enqueue(self, transaction, id):
         self.history.append(id)
         return(succeed(None))
 
+
+
 class PushDistributorTests(TestCase):
 
     @inlineCallbacks
     def test_enqueue(self):
         stub = StubService()
         dist = PushDistributor([stub])
-        yield dist.enqueue("testing")
+        yield dist.enqueue(None, "testing")
         self.assertEquals(stub.history, ["testing"])
 
+
     def test_getPubSubAPSConfiguration(self):
         config = ConfigDict({
             "EnableSSL" : True,
@@ -67,24 +72,29 @@
         self.assertEquals(
             result,
             {
-                "SubscriptionRefreshIntervalSeconds": 42, 
-                "SubscriptionURL": "https://calendars.example.com:8443/apns", 
-                "APSBundleID": "test topic", 
+                "SubscriptionRefreshIntervalSeconds": 42,
+                "SubscriptionURL": "https://calendars.example.com:8443/apns",
+                "APSBundleID": "test topic",
                 "APSEnvironment": "prod"
             }
         )
 
 
+
 class StubDistributor(object):
     def __init__(self):
         self.reset()
 
+
     def reset(self):
         self.history = []
 
-    def enqueue(self, pushID):
+
+    def enqueue(self, transaction, pushID):
         self.history.append(pushID)
 
+
+
 class PushNotificationWorkTests(TestCase):
 
     @inlineCallbacks
@@ -117,6 +127,13 @@
         wp = (yield txn.enqueue(PushNotificationWork,
             pushID="/CalDAV/localhost/bar/",
         ))
+        # Enqueue a different pushID to ensure those are not grouped with
+        # the others:
+        wp = (yield txn.enqueue(PushNotificationWork,
+            pushID="/CalDAV/localhost/baz/",
+        ))
+
         yield txn.commit()
         yield wp.whenExecuted()
-        self.assertEquals(pushDistributor.history, ["/CalDAV/localhost/bar/"])
+        self.assertEquals(pushDistributor.history,
+            ["/CalDAV/localhost/bar/", "/CalDAV/localhost/baz/"])

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/util.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/push/util.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -36,6 +36,8 @@
             return value
     return ""
 
+
+
 def validToken(token):
     """
     Return True if token is in hex and is 64 characters long, False
@@ -52,6 +54,7 @@
     return True
 
 
+
 class TokenHistory(object):
     """
     Manages a queue of tokens and corresponding identifiers.  Queue is always
@@ -68,6 +71,7 @@
         self.identifier = 0
         self.history = []
 
+
     def add(self, token):
         """
         Add a token to the history, and return the new identifier associated
@@ -84,6 +88,7 @@
         del self.history[:-self.maxSize]
         return self.identifier
 
+
     def extractIdentifier(self, identifier):
         """
         Look for the token associated with the identifier.  Remove the
@@ -121,6 +126,7 @@
         self.callback = callback
         self.staggerSeconds = staggerSeconds
 
+
     def schedule(self, tokens, key, dataChangedTimestamp):
         """
         Schedules a batch of notifications for the given tokens, staggered
@@ -139,7 +145,7 @@
         scheduleTime = 0.0
         for token in tokens:
             internalKey = (token, key)
-            if self.outstanding.has_key(internalKey):
+            if internalKey in self.outstanding:
                 self.log_debug("PushScheduler already has this scheduled: %s" %
                     (internalKey,))
             else:
@@ -149,6 +155,7 @@
                     (internalKey, scheduleTime))
                 scheduleTime += self.staggerSeconds
 
+
     def send(self, token, key, dataChangedTimestamp):
         """
         This method is what actually gets scheduled.  Its job is to remove
@@ -167,6 +174,7 @@
         del self.outstanding[(token, key)]
         return self.callback(token, key, dataChangedTimestamp)
 
+
     def stop(self):
         """
         Cancel all outstanding delayed calls

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/caldav.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/caldav.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -70,12 +70,15 @@
 
 from twistedcaldav.config import ConfigurationError
 from twistedcaldav.config import config
+from twistedcaldav.directory import calendaruserproxy
+from twistedcaldav.directory.directory import GroupMembershipCacheUpdater
 from twistedcaldav.localization import processLocalizationFiles
 from twistedcaldav import memcachepool
 from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
 from twistedcaldav.upgrade import UpgradeFileSystemFormatService, PostDBImportService
 
 from calendarserver.tap.util import pgServiceFromConfig, getDBPool, MemoryLimitService
+from calendarserver.tap.util import directoryFromConfig, checkDirectories
 
 from twext.enterprise.ienterprise import POSTGRES_DIALECT
 from twext.enterprise.ienterprise import ORACLE_DIALECT
@@ -105,7 +108,6 @@
 from calendarserver.tap.util import pgConnectorFromConfig
 from calendarserver.tap.util import oracleConnectorFromConfig
 from calendarserver.tap.cfgchild import ConfiguredChildSpawner
-from calendarserver.tools.util import checkDirectory
 from calendarserver.push.notifier import PushDistributor
 from calendarserver.push.amppush import AMPPushMaster, AMPPushForwarder
 from calendarserver.push.applepush import ApplePushNotifierService
@@ -384,8 +386,8 @@
         config.updateDefaults(self.overrides)
 
 
-    def checkDirectory(self, dirpath, description, access=None, create=None, wait=False):
-        checkDirectory(dirpath, description, access=access, create=create, wait=wait)
+    def checkDirectories(self, config):
+        checkDirectories(config)
 
 
     def checkConfiguration(self):
@@ -415,60 +417,9 @@
 
         self.parent["pidfile"] = config.PIDFile
 
-        #
-        # Verify that server root actually exists
-        #
-        self.checkDirectory(
-            config.ServerRoot,
-            "Server root",
-            # Require write access because one might not allow editing on /
-            access=os.W_OK,
-            wait=True # Wait in a loop until ServerRoot exists
-        )
+        self.checkDirectories(config)
 
         #
-        # Verify that other root paths are OK
-        #
-        if config.DataRoot.startswith(config.ServerRoot + os.sep):
-            self.checkDirectory(
-                config.DataRoot,
-                "Data root",
-                access=os.W_OK,
-                create=(0750, config.UserName, config.GroupName),
-            )
-        if config.DocumentRoot.startswith(config.DataRoot + os.sep):
-            self.checkDirectory(
-                config.DocumentRoot,
-                "Document root",
-                # Don't require write access because one might not allow editing on /
-                access=os.R_OK,
-                create=(0750, config.UserName, config.GroupName),
-            )
-        if config.ConfigRoot.startswith(config.ServerRoot + os.sep):
-            self.checkDirectory(
-                config.ConfigRoot,
-                "Config root",
-                access=os.W_OK,
-                create=(0750, config.UserName, config.GroupName),
-            )
-
-        if config.LogRoot.startswith(config.ServerRoot + os.sep):
-            self.checkDirectory(
-                config.LogRoot,
-                "Log root",
-                access=os.W_OK,
-                create=(0750, config.UserName, config.GroupName),
-            )
-
-        # Always create RunRoot (for pid files, socket files) if it does not exist
-        self.checkDirectory(
-            config.RunRoot,
-            "Run root",
-            access=os.W_OK,
-            create=(0770, config.UserName, config.GroupName),
-        )
-
-        #
         # Nuke the file log observer's time format.
         #
 
@@ -542,29 +493,8 @@
             )
             self.monitor.addProcessObject(process, PARENT_ENVIRONMENT)
 
-        if config.GroupCaching.Enabled and config.GroupCaching.EnableUpdater:
-            self.maker.log_info("Adding group caching service")
 
-            groupMembershipCacherArgv = [
-                sys.executable,
-                sys.argv[0],
-            ]
-            if config.UserName:
-                groupMembershipCacherArgv.extend(("-u", config.UserName))
-            if config.GroupName:
-                groupMembershipCacherArgv.extend(("-g", config.GroupName))
-            groupMembershipCacherArgv.extend((
-                "--reactor=%s" % (config.Twisted.reactor,),
-                "-n", self.maker.groupMembershipCacherTapName,
-                "-f", self.configPath,
-                "-o", "PIDFile=groupcacher.pid",
-            ))
 
-            self.monitor.addProcess("groupcacher", groupMembershipCacherArgv,
-                               env=PARENT_ENVIRONMENT)
-
-
-
 class ReExecService(MultiService, LoggingMixIn):
     """
     A MultiService which catches SIGHUP and re-exec's the process.
@@ -621,12 +551,7 @@
     description = "Calendar and Contacts Server"
     options = CalDAVOptions
 
-    #
-    # Default tap names
-    #
-    groupMembershipCacherTapName = "caldav_groupcacher"
 
-
     def makeService(self, options):
         """
         Create the top-level service.
@@ -634,15 +559,18 @@
         self.log_info("%s %s starting %s process..." % (self.description, version, config.ProcessType))
 
         try:
-            from setproctitle import setproctitle
+            from setproctitle import setproctitle, getproctitle
         except ImportError:
             pass
         else:
+            origTitle = getproctitle()
             if config.LogID:
                 logID = " #%s" % (config.LogID,)
             else:
                 logID = ""
-            setproctitle("CalendarServer %s [%s%s]" % (version, config.ProcessType, logID))
+            if config.ProcessType is not "Utility":
+                origTitle = ""
+            setproctitle("CalendarServer %s [%s%s] %s" % (version, config.ProcessType, logID, origTitle))
 
         serviceMethod = getattr(self, "makeService_%s" % (config.ProcessType,), None)
 
@@ -771,10 +699,24 @@
         else:
             mailRetriever = None
 
+        # Optionally set up group cacher
+        if config.GroupCaching.Enabled:
+            groupCacher = GroupMembershipCacheUpdater(
+                calendaruserproxy.ProxyDBService,
+                directory,
+                config.GroupCaching.UpdateSeconds,
+                config.GroupCaching.ExpireSeconds,
+                namespace=config.GroupCaching.MemcachedPool,
+                useExternalProxies=config.GroupCaching.UseExternalProxies
+                )
+        else:
+            groupCacher = None
+
         def decorateTransaction(txn):
             txn._pushDistributor = pushDistributor
             txn._rootResource = result.rootResource
             txn._mailRetriever = mailRetriever
+            txn._groupCacher = groupCacher
 
         store.callWithNewTransactions(decorateTransaction)
 
@@ -1013,11 +955,35 @@
     def makeService_Single(self, options):
         """
         Create a service to be used in a single-process, stand-alone
-        configuration.
+        configuration.  Memcached will be spawned automatically.
         """
         def slaveSvcCreator(pool, store, logObserver):
             result = self.requestProcessingService(options, store, logObserver)
 
+            # Optionally launch memcached.  Note, this is not going through a
+            # ProcessMonitor because there is code elsewhere that needs to
+            # access memcached before startService() gets called, so we're just
+            # directly using Popen to spawn memcached.
+            for name, pool in config.Memcached.Pools.items():
+                if pool.ServerEnabled:
+                    self.log_info(
+                        "Adding memcached service for pool: %s" % (name,)
+                    )
+                    memcachedArgv = [
+                        config.Memcached.memcached,
+                        "-p", str(pool.Port),
+                        "-l", pool.BindAddress,
+                        "-U", "0",
+                    ]
+                    if config.Memcached.MaxMemory is not 0:
+                        memcachedArgv.extend(
+                            ["-m", str(config.Memcached.MaxMemory)]
+                        )
+                    if config.UserName:
+                        memcachedArgv.extend(["-u", config.UserName])
+                    memcachedArgv.extend(config.Memcached.Options)
+                    Popen(memcachedArgv)
+
             # Optionally set up push notifications
             pushDistributor = None
             if config.Notifications.Enabled:
@@ -1037,23 +1003,38 @@
                 if observers:
                     pushDistributor = PushDistributor(observers)
 
+            directory = result.rootResource.getDirectory()
+
             # Optionally set up mail retrieval
             if config.Scheduling.iMIP.Enabled:
-                directory = result.rootResource.getDirectory()
                 mailRetriever = MailRetriever(store, directory,
                     config.Scheduling.iMIP.Receiving)
                 mailRetriever.setServiceParent(result)
             else:
                 mailRetriever = None
 
+            # Optionally set up group cacher
+            if config.GroupCaching.Enabled:
+                groupCacher = GroupMembershipCacheUpdater(
+                    calendaruserproxy.ProxyDBService,
+                    directory,
+                    config.GroupCaching.UpdateSeconds,
+                    config.GroupCaching.ExpireSeconds,
+                    namespace=config.GroupCaching.MemcachedPool,
+                    useExternalProxies=config.GroupCaching.UseExternalProxies
+                    )
+            else:
+                groupCacher = None
+
             def decorateTransaction(txn):
                 txn._pushDistributor = pushDistributor
                 txn._rootResource = result.rootResource
                 txn._mailRetriever = mailRetriever
+                txn._groupCacher = groupCacher
 
             store.callWithNewTransactions(decorateTransaction)
 
-            return result 
+            return result
 
         uid, gid = getSystemIDs(config.UserName, config.GroupName)
 
@@ -1394,6 +1375,7 @@
             from twisted.internet import reactor
             pool = PeerConnectionPool(reactor, store.newTransaction,
                                       7654, schema)
+            store.queuer = store.queuer.transferProposalCallbacks(pool)
             controlSocket.addFactory(_QUEUE_ROUTE,
                                      pool.workerListenerFactory())
             # TODO: now that we have the shared control socket, we should get
@@ -1414,7 +1396,41 @@
             spawner.setServiceParent(multi)
             if config.UseMetaFD:
                 cl.setServiceParent(multi)
+
+            directory = directoryFromConfig(config)
+            rootResource = getRootResource(config, store, [])
+
+            # Optionally set up mail retrieval
+            if config.Scheduling.iMIP.Enabled:
+                mailRetriever = MailRetriever(store, directory,
+                    config.Scheduling.iMIP.Receiving)
+                mailRetriever.setServiceParent(multi)
+            else:
+                mailRetriever = None
+
+            # Optionally set up group cacher
+            if config.GroupCaching.Enabled:
+                groupCacher = GroupMembershipCacheUpdater(
+                    calendaruserproxy.ProxyDBService,
+                    directory,
+                    config.GroupCaching.UpdateSeconds,
+                    config.GroupCaching.ExpireSeconds,
+                    namespace=config.GroupCaching.MemcachedPool,
+                    useExternalProxies=config.GroupCaching.UseExternalProxies
+                    )
+            else:
+                groupCacher = None
+
+            def decorateTransaction(txn):
+                txn._pushDistributor = None
+                txn._rootResource = rootResource
+                txn._mailRetriever = mailRetriever
+                txn._groupCacher = groupCacher
+
+            store.callWithNewTransactions(decorateTransaction)
+
             return multi
+
         ssvc = self.storageService(spawnerSvcCreator, uid, gid)
         ssvc.setServiceParent(s)
         return s

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/test/test_caldav.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/test/test_caldav.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/test/test_caldav.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -33,11 +33,12 @@
 
 from twisted.internet.interfaces import IProcessTransport, IReactorProcess
 from twisted.internet.protocol import ServerFactory
-from twisted.internet.defer import Deferred, inlineCallbacks
+from twisted.internet.defer import Deferred, inlineCallbacks, passthru
 from twisted.internet.task import Clock
 from twisted.internet import reactor
 
-from twisted.application.service import IService, IServiceCollection
+from twisted.application.service import (IService, IServiceCollection,
+                                         MultiService)
 from twisted.application import internet
 
 from twext.web2.dav import auth
@@ -62,6 +63,7 @@
     _CONTROL_SERVICE_NAME, getSystemIDs
 )
 from calendarserver.provision.root import RootResource
+from twext.enterprise.queue import PeerConnectionPool, LocalQueuer
 from StringIO import StringIO
 
 
@@ -150,6 +152,8 @@
     def checkFile(self, *args, **kwargs):
         pass
 
+    def checkDirectories(self, *args, **kwargs):
+        pass
 
     def loadConfiguration(self):
         """
@@ -358,14 +362,16 @@
         writePlist(self.config, self.configFile)
 
 
-    def makeService(self):
+    def makeService(self, patcher=passthru):
         """
         Create a service by calling into CalDAVServiceMaker with
         self.configFile
         """
         self.options.parseOptions(["-f", self.configFile])
 
-        return CalDAVServiceMaker().makeService(self.options)
+        maker = CalDAVServiceMaker()
+        maker = patcher(maker)
+        return maker.makeService(self.options)
 
 
     def getSite(self):
@@ -507,8 +513,41 @@
         )
 
 
+    def test_storeQueuerSetInMaster(self):
+        """
+        In the master, the store's queuer should be set to a
+        L{PeerConnectionPool}, so that work can be distributed to other
+        processes.
+        """
+        self.config["ProcessType"] = "Combined"
+        self.writeConfig()
+        class NotAStore(object):
+            queuer = LocalQueuer(None)
+            def newTransaction(self):
+                return None
+            def callWithNewTransactions(self, x):
+                pass
+        store = NotAStore()
+        def something(proposal):
+            pass
+        store.queuer.callWithNewProposals(something)
+        def patch(maker):
+            def storageServiceStandIn(createMainService, logObserver,
+                                      uid=None, gid=None):
+                pool = None
+                logObserver = None
+                svc = createMainService(pool, store, logObserver)
+                multi = MultiService()
+                svc.setServiceParent(multi)
+                return multi
+            self.patch(maker, "storageService", storageServiceStandIn)
+            return maker
+        self.makeService(patch)
+        self.assertIsInstance(store.queuer, PeerConnectionPool)
+        self.assertIn(something, store.queuer.proposalCallbacks)
 
 
+
 class SlaveServiceTest(BaseServiceMakerTests):
     """
     Test various configurations of the Slave service

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/util.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tap/util.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -57,7 +57,7 @@
 from twistedcaldav.directory.internal import InternalDirectoryService
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
 from twistedcaldav.directory.wiki import WikiDirectoryService
-from calendarserver.push.notifier import NotifierFactory 
+from calendarserver.push.notifier import NotifierFactory
 from calendarserver.push.applepush import APNSubscriptionResource
 from twistedcaldav.directorybackedaddressbook import DirectoryBackedAddressBookResource
 from twistedcaldav.resource import AuthenticationWrapper
@@ -86,6 +86,7 @@
 
 from calendarserver.accesslog import DirectoryLogWrapperResource
 from calendarserver.provision.root import RootResource
+from calendarserver.tools.util import checkDirectory
 from calendarserver.webadmin.resource import WebAdminResource
 from calendarserver.webcal.resource import WebCalendarResource
 
@@ -650,6 +651,7 @@
             config.WebCalendarRoot,
             root,
             directory,
+            newStore,
             principalCollections=(principalCollection,),
         )
         root.putChild("admin", webAdmin)
@@ -959,3 +961,61 @@
                         self._processMonitor.stopProcess(name)
         finally:
             self._delayedCall = self._reactor.callLater(self._seconds, self.checkMemory)
+
+
+
+def checkDirectories(config):
+    """
+    Make sure that various key directories exist (and create if needed)
+    """
+
+    #
+    # Verify that server root actually exists
+    #
+    checkDirectory(
+        config.ServerRoot,
+        "Server root",
+        # Require write access because one might not allow editing on /
+        access=os.W_OK,
+        wait=True # Wait in a loop until ServerRoot exists
+    )
+
+    #
+    # Verify that other root paths are OK
+    #
+    if config.DataRoot.startswith(config.ServerRoot + os.sep):
+        checkDirectory(
+            config.DataRoot,
+            "Data root",
+            access=os.W_OK,
+            create=(0750, config.UserName, config.GroupName),
+        )
+    if config.DocumentRoot.startswith(config.DataRoot + os.sep):
+        checkDirectory(
+            config.DocumentRoot,
+            "Document root",
+            # Don't require write access because one might not allow editing on /
+            access=os.R_OK,
+            create=(0750, config.UserName, config.GroupName),
+        )
+    if config.ConfigRoot.startswith(config.ServerRoot + os.sep):
+        checkDirectory(
+            config.ConfigRoot,
+            "Config root",
+            access=os.W_OK,
+            create=(0750, config.UserName, config.GroupName),
+        )
+    if config.LogRoot.startswith(config.ServerRoot + os.sep):
+        checkDirectory(
+            config.LogRoot,
+            "Log root",
+            access=os.W_OK,
+            create=(0750, config.UserName, config.GroupName),
+        )
+    if config.RunRoot.startswith(config.ServerRoot + os.sep):
+        checkDirectory(
+            config.RunRoot,
+            "Run root",
+            access=os.W_OK,
+            create=(0770, config.UserName, config.GroupName),
+        )

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/calverify.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/calverify.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/calverify.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -17,8 +17,6 @@
 ##
 from __future__ import print_function
 
-from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 
 """
 This tool scans the calendar store to analyze organizer/attendee event
@@ -42,30 +40,39 @@
 
 """
 
+from calendarserver.tools.cmdline import utilityMain
+
 from calendarserver.tools import tables
-from calendarserver.tools.cmdline import utilityMain
 from calendarserver.tools.util import getDirectory
+
 from pycalendar import definitions
 from pycalendar.calendar import PyCalendar
 from pycalendar.datetime import PyCalendarDateTime
 from pycalendar.exceptions import PyCalendarError
 from pycalendar.period import PyCalendarPeriod
 from pycalendar.timezone import PyCalendarTimezone
+
 from twext.enterprise.dal.syntax import Select, Parameter, Count
+
 from twisted.application.service import Service
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python import log, usage
 from twisted.python.usage import Options
+
 from twistedcaldav import caldavxml
+from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 from twistedcaldav.dateops import pyCalendarTodatetime
+from twistedcaldav.directory.directory import DirectoryService
 from twistedcaldav.ical import Component, ignoredComponents, \
     InvalidICalendarDataError, Property
 from twistedcaldav.scheduling.itip import iTipGenerator
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 from twistedcaldav.util import normalizationLookup
+
 from txdav.base.propertystore.base import PropertyName
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_OWN
 from txdav.common.icommondatastore import InternalDataStoreError
+
 import base64
 import collections
 import sys
@@ -195,7 +202,7 @@
 if not hasattr(Component, "maxAlarmCounts"):
     Component.hasDuplicateAlarms = new_hasDuplicateAlarms
 
-VERSION = "9"
+VERSION = "10"
 
 def printusage(e=None):
     if e:
@@ -228,6 +235,7 @@
 --missing           : display orphaned calendar homes - can be used.
                       with either --ical or --mismatch.
 --double            : detect double-bookings.
+--dark-purge        : purge room/resource events with invalid organizer
 
 --nuke PATH|RID     : remove specific calendar resources - can
                       only be used by itself. PATH is the full
@@ -263,6 +271,19 @@
 --summary  : report only which GUIDs have double-bookings - no details.
 --days     : number of days ahead to scan [DEFAULT: 365]
 
+Options for --dark-purge:
+
+--uuid     : only scan specified calendar homes. Can be a partial GUID
+             to scan all GUIDs with that as a prefix or "*" for all GUIDS
+             (that are marked as resources or locations in the directory).
+--summary  : report only which GUIDs have double-bookings - no details.
+--no-organizer       : only detect events without an organizer
+--invalid-organizer  : only detect events with an organizer not in the directory
+--disabled-organizer : only detect events with an organizer disabled for calendaring
+
+If none of (--no-organizer, --invalid-organizer, --disabled-organizer) is present, it
+will default to (--invalid-organizer, --disabled-organizer).
+
 CHANGES
 v8: Detects ORGANIZER or ATTENDEE properties with mailto: calendar user
     addresses for users that have valid directory records. Fix is to
@@ -293,13 +314,18 @@
         ['mismatch', 's', "Detect organizer/attendee mismatches."],
         ['missing', 'm', "Show 'orphaned' homes."],
         ['double', 'd', "Detect double-bookings."],
+        ['dark-purge', 'p', "Purge room/resource events with invalid organizer."],
         ['fix', 'x', "Fix problems."],
         ['verbose', 'v', "Verbose logging."],
         ['details', 'V', "Detailed logging."],
         ['summary', 'S', "Summary of double-bookings."],
         ['tzid', 't', "Timezone to adjust displayed times to."],
-    ]
 
+        ['no-organizer', '', "Detect dark events without an organizer"],
+        ['invalid-organizer', '', "Detect dark events with an organizer not in the directory"],
+        ['disabled-organizer', '', "Detect dark events with a disabled organizer"],
+]
+
     optParameters = [
         ['config', 'f', DEFAULT_CONFIG_FILE, "Specify caldavd.plist configuration path."],
         ['uuid', 'u', "", "Only check this user."],
@@ -564,7 +590,32 @@
         cb = schema.CALENDAR_BIND
         ch = schema.CALENDAR_HOME
         tr = schema.TIME_RANGE
+        kwds = {
+            "Start" : pyCalendarTodatetime(start),
+            "Max"   : pyCalendarTodatetime(PyCalendarDateTime(1900, 1, 1, 0, 0, 0)),
+            "UUID" : uuid,
+        }
+        rows = (yield Select(
+            [ch.OWNER_UID, co.RESOURCE_ID, co.ICALENDAR_UID, cb.CALENDAR_RESOURCE_NAME, co.MD5, co.ORGANIZER, co.CREATED, co.MODIFIED],
+            From=ch.join(
+                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID)).join(
+                co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID).And(
+                    cb.BIND_MODE == _BIND_MODE_OWN).And(
+                    cb.CALENDAR_RESOURCE_NAME != "inbox")).join(
+                tr, type="left", on=(co.RESOURCE_ID == tr.CALENDAR_OBJECT_RESOURCE_ID)),
+            Where=(ch.OWNER_UID == Parameter("UUID")).And((tr.START_DATE >= Parameter("Start")).Or(co.RECURRANCE_MAX <= Parameter("Start"))),
+            GroupBy=(ch.OWNER_UID, co.RESOURCE_ID, co.ICALENDAR_UID, cb.CALENDAR_RESOURCE_NAME, co.MD5, co.ORGANIZER, co.CREATED, co.MODIFIED,),
+        ).on(self.txn, **kwds))
+        returnValue(tuple(rows))
 
+
+    @inlineCallbacks
+    def getAllResourceInfoTimeRangeWithUUIDForAllUID(self, start, uuid):
+        co = schema.CALENDAR_OBJECT
+        cb = schema.CALENDAR_BIND
+        ch = schema.CALENDAR_HOME
+        tr = schema.TIME_RANGE
+
         cojoin = (cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID).And(
                 cb.BIND_MODE == _BIND_MODE_OWN).And(
                 cb.CALENDAR_RESOURCE_NAME != "inbox")
@@ -1370,8 +1421,8 @@
             rows = yield self.getAllResourceInfoWithUID(self.options["uid"])
             descriptor = "getAllResourceInfoWithUID"
         elif self.options["uuid"]:
-            rows = yield self.getAllResourceInfoTimeRangeWithUUID(self.start, self.options["uuid"])
-            descriptor = "getAllResourceInfoTimeRangeWithUUID"
+            rows = yield self.getAllResourceInfoTimeRangeWithUUIDForAllUID(self.start, self.options["uuid"])
+            descriptor = "getAllResourceInfoTimeRangeWithUUIDForAllUID"
             self.options["uuid"] = None
         else:
             rows = yield self.getAllResourceInfoTimeRange(self.start)
@@ -2357,6 +2408,244 @@
 
 
 
+class DarkPurgeService(CalVerifyService):
+    """
+    Service which detects room/resource events that have an invalid organizer.
+    """
+
+    def title(self):
+        return "Dark Purge Service"
+
+
+    @inlineCallbacks
+    def doAction(self):
+
+        if not self.options["no-organizer"] and not self.options["invalid-organizer"] and not self.options["disabled-organizer"]:
+            self.options["invalid-organizer"] = self.options["disabled-organizer"] = True
+
+        self.output.write("\n---- Scanning calendar data ----\n")
+
+        self.tzid = PyCalendarTimezone(tzid=self.options["tzid"] if self.options["tzid"] else "America/Los_Angeles")
+        self.now = PyCalendarDateTime.getNowUTC()
+        self.start = self.options["start"] if "start" in self.options else PyCalendarDateTime.getToday()
+        self.start.setDateOnly(False)
+        self.start.setTimezone(self.tzid)
+        self.fix = self.options["fix"]
+
+        if self.options["verbose"] and self.options["summary"]:
+            ot = time.time()
+
+        # Check loop over uuid
+        UUIDDetails = collections.namedtuple("UUIDDetails", ("uuid", "rname", "purged",))
+        self.uuid_details = []
+        if len(self.options["uuid"]) != 36:
+            self.txn = self.store.newTransaction()
+            if self.options["uuid"]:
+                homes = yield self.getMatchingHomeUIDs(self.options["uuid"])
+            else:
+                homes = yield self.getAllHomeUIDs()
+            yield self.txn.commit()
+            self.txn = None
+            uuids = []
+            if self.options["verbose"]:
+                self.output.write("%d uuids to check\n" % (len(homes,)))
+            for uuid in sorted(homes):
+                record = self.directoryService().recordWithGUID(uuid)
+                if record is not None and record.recordType in (DirectoryService.recordType_locations, DirectoryService.recordType_resources):
+                    uuids.append(uuid)
+        else:
+            uuids = [self.options["uuid"], ]
+        if self.options["verbose"]:
+            self.output.write("%d uuids to scan\n" % (len(uuids,)))
+
+        count = 0
+        for uuid in uuids:
+            self.results = {}
+            self.summary = []
+            self.total = 0
+            count += 1
+
+            record = self.directoryService().recordWithGUID(uuid)
+            if record is None:
+                continue
+            if not record.thisServer() or not record.enabledForCalendaring:
+                continue
+
+            rname = record.fullName
+
+            if len(uuids) > 1 and not self.options["summary"]:
+                self.output.write("\n\n-----------------------------\n")
+
+            self.txn = self.store.newTransaction()
+
+            if self.options["verbose"]:
+                t = time.time()
+            rows = yield self.getAllResourceInfoTimeRangeWithUUID(self.start, uuid)
+            descriptor = "getAllResourceInfoTimeRangeWithUUID"
+
+            yield self.txn.commit()
+            self.txn = None
+
+            if self.options["verbose"]:
+                if not self.options["summary"]:
+                    self.output.write("%s time: %.1fs\n" % (descriptor, time.time() - t,))
+                else:
+                    self.output.write("%s (%d/%d)" % (uuid, count, len(uuids),))
+                    self.output.flush()
+
+            self.total = len(rows)
+            if not self.options["summary"]:
+                self.logResult("UUID to process", uuid)
+                self.logResult("Record name", rname)
+                self.addSummaryBreak()
+                self.logResult("Number of events to process", self.total)
+
+            if rows:
+                if not self.options["summary"]:
+                    self.addSummaryBreak()
+                purged = yield self.darkPurge(rows, uuid)
+            else:
+                purged = False
+
+            self.uuid_details.append(UUIDDetails(uuid, rname, purged))
+
+            if not self.options["summary"]:
+                self.printSummary()
+            else:
+                self.output.write(" - %s\n" % ("Dark Events" if purged else "OK",))
+                self.output.flush()
+
+        if count == 0:
+            self.output.write("Nothing to scan\n")
+
+        if self.options["summary"]:
+            table = tables.Table()
+            table.addHeader(("GUID", "Name", "RID", "UID", "Organizer",))
+            purged = 0
+            for item in sorted(self.uuid_details):
+                if not item.purged:
+                    continue
+                uuid = item.uuid
+                rname = item.rname
+                for detail in item.purged:
+                    table.addRow((
+                        uuid,
+                        rname,
+                        detail.resid,
+                        detail.uid,
+                        detail.organizer,
+                    ))
+                    uuid = ""
+                    rname = ""
+                    purged += 1
+            table.addFooter(("Total", "%d" % (purged,), "", "", "",))
+            self.output.write("\n")
+            table.printTable(os=self.output)
+
+            if self.options["verbose"]:
+                self.output.write("%s time: %.1fs\n" % ("Summary", time.time() - ot,))
+
+
+    @inlineCallbacks
+    def darkPurge(self, rows, uuid):
+        """
+        Check each calendar resource by looking at any ORGANIER property value and verifying it is valid.
+        """
+
+        if not self.options["summary"]:
+            self.output.write("\n---- Checking for dark events ----\n")
+        self.txn = self.store.newTransaction()
+
+        if self.options["verbose"]:
+            t = time.time()
+
+        Details = collections.namedtuple("Details", ("resid", "uid", "organizer",))
+
+        count = 0
+        total = len(rows)
+        details = []
+        fixed = 0
+        rjust = 10
+        for resid in rows:
+            resid = resid[1]
+            caldata = yield self.getCalendar(resid, self.fix)
+            if caldata is None:
+                if self.parseError:
+                    returnValue((False, self.parseError))
+                else:
+                    returnValue((True, "Nothing to scan"))
+
+            cal = Component(None, pycalendar=caldata)
+            uid = cal.resourceUID()
+
+            fail = False
+            organizer = cal.getOrganizer()
+            if organizer is None:
+                if self.options["no-organizer"]:
+                    fail = True
+            else:
+                principal = self.directoryService().principalForCalendarUserAddress(organizer)
+                if principal is None and organizer.startswith("urn:uuid:"):
+                    principal = self.directoryService().principalCollection.principalForUID(organizer[9:])
+                if principal is None:
+                    if self.options["invalid-organizer"]:
+                        fail = True
+                elif not principal.calendarsEnabled():
+                    if self.options["disabled-organizer"]:
+                        fail = True
+
+            if fail:
+                details.append(Details(resid, uid, organizer,))
+                if self.fix:
+                    yield self.removeEvent(resid)
+                    fixed += 1
+
+            if self.options["verbose"] and not self.options["summary"]:
+                if count == 1:
+                    self.output.write("Current".rjust(rjust) + "Total".rjust(rjust) + "Complete".rjust(rjust) + "\n")
+                if divmod(count, 100)[1] == 0:
+                    self.output.write((
+                        "\r" +
+                        ("%s" % count).rjust(rjust) +
+                        ("%s" % total).rjust(rjust) +
+                        ("%d%%" % safePercent(count, total)).rjust(rjust)
+                    ).ljust(80))
+                    self.output.flush()
+
+            # To avoid holding locks on all the rows scanned, commit every 100 resources
+            if divmod(count, 100)[1] == 0:
+                yield self.txn.commit()
+                self.txn = self.store.newTransaction()
+
+        yield self.txn.commit()
+        self.txn = None
+        if self.options["verbose"] and not self.options["summary"]:
+            self.output.write((
+                "\r" +
+                ("%s" % count).rjust(rjust) +
+                ("%s" % total).rjust(rjust) +
+                ("%d%%" % safePercent(count, total)).rjust(rjust)
+            ).ljust(80) + "\n")
+
+        # Print table of results
+        if not self.options["summary"]:
+            self.logResult("Number of dark events", len(details))
+
+        self.results["Dark Events"] = details
+        if self.fix:
+            self.results["Fix dark events"] = fixed
+
+        if self.options["verbose"] and not self.options["summary"]:
+            diff_time = time.time() - t
+            self.output.write("Time: %.2f s  Average: %.1f ms/resource\n" % (
+                diff_time,
+                safePercent(diff_time, total, 1000.0),
+            ))
+
+        returnValue(details)
+
+
+
 def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
 
     if reactor is None:
@@ -2387,6 +2676,11 @@
             return SchedulingMismatchService(store, options, output, reactor, config)
         elif options["double"]:
             return DoubleBookingService(store, options, output, reactor, config)
+        elif options["dark-purge"]:
+            return DarkPurgeService(store, options, output, reactor, config)
+        else:
+            printusage("Invalid operation")
+            sys.exit(1)
 
     utilityMain(options['config'], makeService, reactor)
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/cmdline.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/cmdline.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/cmdline.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -19,14 +19,19 @@
 """
 
 from calendarserver.tap.caldav import CalDAVServiceMaker, CalDAVOptions
+from calendarserver.tap.util import checkDirectories
 from calendarserver.tools.util import loadConfig, autoDisableMemcached
 
 from twext.python.log import StandardIOObserver
 
 from twistedcaldav.config import ConfigurationError
+from twisted.internet.defer import inlineCallbacks
 
-import os
 import sys
+from calendarserver.tap.util import getRootResource
+from twisted.application.service import Service
+from errno import ENOENT, EACCES
+from twext.enterprise.queue import NonPerformingQueuer
 
 # TODO: direct unit tests for these functions.
 
@@ -74,10 +79,7 @@
         if patchConfig is not None:
             patchConfig(config)
 
-        # If we don't have permission to access the DataRoot directory, we
-        # can't proceed.  If this fails it should raise OSError which we
-        # catch below.
-        os.listdir(config.DataRoot)
+        checkDirectories(config)
 
         config.ProcessType = "Utility"
         config.UtilityServiceClass = serviceClass
@@ -85,6 +87,10 @@
         autoDisableMemcached(config)
 
         maker = serviceMaker()
+
+        # Only perform post-import duties if someone has explicitly said to
+        maker.doPostImport = getattr(maker, "doPostImport", False)
+
         options = CalDAVOptions
         service = maker.makeService(options)
 
@@ -98,3 +104,49 @@
         return
 
     reactor.run()
+
+
+
+class WorkerService(Service):
+
+    def __init__(self, store):
+        self._store = store
+        # Work can be queued but will not be performed by the command line tool
+        store.queuer = NonPerformingQueuer()
+
+
+    def rootResource(self):
+        try:
+            from twistedcaldav.config import config
+            rootResource = getRootResource(config, self._store)
+        except OSError, e:
+            if e.errno == ENOENT:
+                # Trying to re-write resources.xml but its parent directory does
+                # not exist.  The server's never been started, so we're missing
+                # state required to do any work.
+                raise ConfigurationError(
+                    "It appears that the server has never been started.\n"
+                    "Please start it at least once before running this tool.")
+            elif e.errno == EACCES:
+                # Trying to re-write resources.xml but it is not writable by the
+                # current user.  This most likely means we're in a system
+                # configuration and the user doesn't have sufficient privileges
+                # to do the other things the tool might need to do either.
+                raise ConfigurationError("You must run this tool as root.")
+            else:
+                raise
+        return rootResource
+
+
+    @inlineCallbacks
+    def startService(self):
+        from twisted.internet import reactor
+        try:
+            yield self.doWork()
+        except ConfigurationError, ce:
+            sys.stderr.write("Error: %s\n" % (str(ce),))
+        except Exception, e:
+            sys.stderr.write("Error: %s\n" % (e,))
+            raise
+        finally:
+            reactor.stop()

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/config.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/config.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -18,18 +18,59 @@
 from __future__ import print_function
 
 """
-This tool reads the Calendar Server configuration file and emits the
-requested value.
+This tool gets and sets Calendar Server configuration keys
 """
 
-import os, sys
 from getopt import getopt, GetoptError
+import os
+import plistlib
+import signal
+import sys
+import xml
 
-from twistedcaldav.config import ConfigurationError
+from twext.python.plistlib import readPlistFromString, writePlistToString
+from twistedcaldav.config import config, ConfigDict, ConfigurationError, mergeData
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+WRITABLE_CONFIG_KEYS = [
+    "ServerHostName",
+    "HTTPPort",
+    "SSLPort",
+    "EnableSSL",
+    "RedirectHTTPToHTTPS",
+    "EnableCalDAV",
+    "EnableCardDAV",
+    "DataRoot",
+    "SSLCertificate",
+    "SSLPrivateKey",
+    "SSLAuthorityChain",
+    "EnableSearchAddressBook",
+    "Authentication.Basic.Enabled",
+    "Authentication.Basic.AllowedOverWireUnencrypted",
+    "Authentication.Digest.Enabled",
+    "Authentication.Digest.AllowedOverWireUnencrypted",
+    "Authentication.Kerberos.Enabled",
+    "Authentication.Kerberos.AllowedOverWireUnencrypted",
+    "Authentication.Wiki.Enabled",
+    "Scheduling.iMIP.Enabled",
+    "Scheduling.iMIP.Receiving.Username",
+    "Scheduling.iMIP.Receiving.Server",
+    "Scheduling.iMIP.Receiving.Port",
+    "Scheduling.iMIP.Receiving.Type",
+    "Scheduling.iMIP.Receiving.UseSSL",
+    "Scheduling.iMIP.Sending.Username",
+    "Scheduling.iMIP.Sending.Server",
+    "Scheduling.iMIP.Sending.Port",
+    "Scheduling.iMIP.Sending.UseSSL",
+    "Scheduling.iMIP.Sending.Address",
+    "Notifications.Services.APNS.Enabled",
+    "Notifications.Services.APNS.CalDAV.CertificatePath",
+    "Notifications.Services.APNS.CalDAV.AuthorityChainPath",
+    "Notifications.Services.APNS.CalDAV.PrivateKeyPath",
+    "Notifications.Services.APNS.CardDAV.CertificatePath",
+    "Notifications.Services.APNS.CardDAV.AuthorityChainPath",
+    "Notifications.Services.APNS.CardDAV.PrivateKeyPath",
+]
 
-from calendarserver.tools.util import loadConfig
-
 def usage(e=None):
     if e:
         print(e)
@@ -42,6 +83,7 @@
     print("options:")
     print("  -h --help: print this help and exit")
     print("  -f --config: Specify caldavd.plist configuration path")
+    print("  -w --writeconfig: Specify caldavd.plist configuration path for writing")
 
     if e:
         sys.exit(64)
@@ -51,15 +93,17 @@
 def main():
     try:
         (optargs, args) = getopt(
-            sys.argv[1:], "hf:", [
+            sys.argv[1:], "hf:w:", [
                 "help",
                 "config=",
+                "writeconfig=",
             ],
         )
     except GetoptError, e:
         usage(e)
 
     configFileName = DEFAULT_CONFIG_FILE
+    writeConfigFileName = ""
 
     for opt, arg in optargs:
         if opt in ("-h", "--help"):
@@ -68,18 +112,338 @@
         elif opt in ("-f", "--config"):
             configFileName = arg
 
+        elif opt in ("-w", "--writeconfig"):
+            writeConfigFileName = arg
+
     try:
-        config = loadConfig(configFileName)
+        config.load(configFileName)
     except ConfigurationError, e:
         sys.stdout.write("%s\n" % (e,))
         sys.exit(1)
 
-    for configKey in args:
-        c = config
-        for subKey in configKey.split("."):
-            c = c.get(subKey, None)
-            if c is None:
-                sys.stderr.write("No such config key: %s\n" % configKey)
-                break
+    if not writeConfigFileName:
+        # If --writeconfig was not passed, use WritableConfigFile from
+        # main plist.  If that's an empty string, writes will happen to
+        # the main file.
+        writeConfigFileName = config.WritableConfigFile
+
+    if not writeConfigFileName:
+        writeConfigFileName = configFileName
+
+    writable = WritableConfig(config, writeConfigFileName)
+    writable.read()
+
+    if args:
+        for configKey in args:
+
+            if "=" in configKey:
+                # This is an assignment
+                configKey, stringValue = configKey.split("=")
+                value = writable.convertToValue(stringValue)
+                writable.set({configKey:value})
+            else:
+                # This is a read
+                c = config
+                for subKey in configKey.split("."):
+                    c = c.get(subKey, None)
+                    if c is None:
+                        sys.stderr.write("No such config key: %s\n" % configKey)
+                        break
+                sys.stdout.write("%s=%s\n" % (configKey, c))
+
+        writable.save(restart=True)
+
+    else:
+        # Read plist commands from stdin
+        rawInput = sys.stdin.read()
+        try:
+            plist = readPlistFromString(rawInput)
+            # Note: values in plist will already be unicode
+        except xml.parsers.expat.ExpatError, e:
+            respondWithError(str(e))
+            return
+
+        # If the plist is an array, each element of the array is a separate
+        # command dictionary.
+        if isinstance(plist, list):
+            commands = plist
         else:
-            sys.stdout.write("%s\n" % c)
+            commands = [plist]
+
+        runner = Runner(commands)
+        runner.run()
+
+
+
+class Runner(object):
+
+    """
+    A class which carries out commands, which are plist strings containing
+    dictionaries with a "command" key, plus command-specific data.
+    """
+
+    def __init__(self, commands):
+        """
+        @param commands: the commands to run
+        @type commands: list of plist strings
+        """
+        self.commands = commands
+
+    def validate(self):
+        """
+        Validate all the commands by making sure this class implements
+        all the command keys.
+        @return: True if all commands are valid, False otherwise
+        """
+        # Make sure commands are valid
+        for command in self.commands:
+            if 'command' not in command:
+                respondWithError("'command' missing from plist")
+                return False
+            commandName = command['command']
+            methodName = "command_%s" % (commandName,)
+            if not hasattr(self, methodName):
+                respondWithError("Unknown command '%s'" % (commandName,))
+                return False
+        return True
+
+    def run(self):
+        """
+        Find the appropriate method for each command and call them.
+        """
+        try:
+            for command in self.commands:
+                commandName = command['command']
+                methodName = "command_%s" % (commandName,)
+                if hasattr(self, methodName):
+                    getattr(self, methodName)(command)
+                else:
+                    respondWithError("Unknown command '%s'" % (commandName,))
+
+        except Exception, e:
+            respondWithError("Command failed: '%s'" % (str(e),))
+            raise
+
+    def command_readConfig(self, command):
+        """
+        Return current configuration
+
+        @param command: the dictionary parsed from the plist read from stdin
+        @type command: C{dict}
+        """
+        result = {}
+        for keyPath in WRITABLE_CONFIG_KEYS:
+            value = getKeyPath(config, keyPath)
+            if value is not None:
+                # Note: config contains utf-8 encoded strings, but plistlib
+                # wants unicode, so decode here:
+                if isinstance(value, str):
+                    value = value.decode("utf-8")
+                setKeyPath(result, keyPath, value)
+        respond(command, result)
+
+    def command_writeConfig(self, command):
+        """
+        Write config to secondary, writable plist
+
+        @param command: the dictionary parsed from the plist read from stdin
+        @type command: C{dict}
+        """
+        writable = WritableConfig(config, config.WritableConfigFile)
+        writable.read()
+        valuesToWrite = command.get("Values", {})
+        # Note: values are unicode if they contain non-ascii
+        for keyPath, value in flattenDictionary(valuesToWrite):
+            if keyPath in WRITABLE_CONFIG_KEYS:
+                writable.set(setKeyPath(ConfigDict(), keyPath, value))
+        try:
+            writable.save(restart=False)
+        except Exception, e:
+            respond(command, {"error": str(e)})
+        else:
+            config.reload()
+            self.command_readConfig(command)
+
+
+def setKeyPath(parent, keyPath, value):
+    """
+    Allows the setting of arbitrary nested dictionary keys via a single
+    dot-separated string.  For example, setKeyPath(parent, "foo.bar.baz",
+    "xyzzy") would create any intermediate missing directories (or whatever
+    class parent is, such as ConfigDict) so that the following structure
+    results:  parent = { "foo" : { "bar" : { "baz" : "xyzzy } } }
+
+    @param parent: the object to modify
+    @type parent: any dict-like object
+    @param keyPath: a dot-delimited string specifying the path of keys to
+        traverse
+    @type keyPath: C{str}
+    @param value: the value to set
+    @type value: c{object}
+    @return: parent
+    """
+    original = parent
+    parts = keyPath.split(".")
+    for part in parts[:-1]:
+        child = parent.get(part, None)
+        if child is None:
+            parent[part] = child = parent.__class__()
+        parent = child
+    parent[parts[-1]] = value
+    return original
+
+def getKeyPath(parent, keyPath):
+    """
+    Allows the getting of arbitrary nested dictionary keys via a single
+    dot-separated string.  For example, getKeyPath(parent, "foo.bar.baz")
+    would fetch parent["foo"]["bar"]["baz"].  If any of the keys don't
+    exist, None is returned instead.
+
+    @param parent: the object to traverse
+    @type parent: any dict-like object
+    @param keyPath: a dot-delimited string specifying the path of keys to
+        traverse
+    @type keyPath: C{str}
+    @return: the value at keyPath
+    """
+    parts = keyPath.split(".")
+    for part in parts[:-1]:
+        child = parent.get(part, None)
+        if child is None:
+            return None
+        parent = child
+    return parent.get(parts[-1], None)
+
+def flattenDictionary(dictionary, current=""):
+    """
+    Returns a generator of (keyPath, value) tuples for the given dictionary,
+    where each keyPath is a dot-separated string representing the complete
+    path to a nested key.
+
+    @param dictionary: the dict object to traverse
+    @type dictionary: C{dict}
+    @param current: do not use; used internally for recursion
+    @type current: C{str}
+    @return: generator of (keyPath, value) tuples
+    """
+    for key, value in dictionary.iteritems():
+        if isinstance(value, dict):
+            for result in flattenDictionary(value, current + key + "."):
+                yield result
+        else:
+            yield (current + key, value)
+
+
+def restartService(pidFilename):
+    """
+    Given the path to a PID file, sends a HUP signal to the contained pid
+    in order to cause calendar server to restart.
+
+    @param pidFilename: an absolute path to a PID file
+    @type pidFilename: C{str}
+    """
+    if os.path.exists(pidFilename):
+        pidFile = open(pidFilename, "r")
+        pid = pidFile.read().strip()
+        pidFile.close()
+        try:
+            pid = int(pid)
+        except ValueError:
+            return
+        try:
+            os.kill(pid, signal.SIGHUP)
+        except OSError:
+            pass
+
+
+class WritableConfig(object):
+    """
+    A wrapper around a Config object which allows writing of values.  The idea
+    is a deployment could have a master plist which doesn't change, and have
+    it include a plist file which does.  This class facilitates writing to that
+    included plist.
+    """
+
+    def __init__(self, wrappedConfig, fileName):
+        """
+        @param wrappedConfig: the Config object to read from
+        @type wrappedConfig: C{Config}
+        @param fileName: the full path to the modifiable plist
+        @type fileName: C{str}
+        """
+        self.config = wrappedConfig
+        self.fileName = fileName
+        self.changes = None
+        self.currentConfigSubset = ConfigDict()
+        self.dirty = False
+
+    def set(self, data):
+        """
+        Merges data into a ConfigDict of changes intended to be saved to disk
+        when save( ) is called.
+
+        @param data: a dict containing new values
+        @type data: C{dict}
+        """
+        if not isinstance(data, ConfigDict):
+            data = ConfigDict(mapping=data)
+        mergeData(self.currentConfigSubset, data)
+        self.dirty = True
+
+    def read(self):
+        """
+        Reads in the data contained in the writable plist file.
+
+        @return: C{ConfigDict}
+        """
+        if os.path.exists(self.fileName):
+            self.currentConfigSubset = ConfigDict(mapping=plistlib.readPlist(self.fileName))
+        else:
+            self.currentConfigSubset = ConfigDict()
+
+    def toString(self):
+        return plistlib.writePlistToString(self.currentConfigSubset)
+
+    def save(self, restart=False):
+        """
+        Writes any outstanding changes to the writable plist file.  Optionally
+        restart calendar server.
+
+        @param restart: whether to restart the calendar server.
+        @type restart: C{bool}
+        """
+        if self.dirty:
+            plistlib.writePlist(self.currentConfigSubset, self.fileName)
+            self.dirty = False
+            if restart:
+                restartService(self.config.PIDFile)
+
+    @classmethod
+    def convertToValue(cls, string):
+        """
+        Inspect string and convert the value into an appropriate Python data type
+        TODO: change this to look at actual types definied within stdconfig
+        """
+        if "." in string:
+            try:
+                value = float(string)
+            except ValueError:
+                value = string
+        else:
+            try:
+                value = int(string)
+            except ValueError:
+                if string == "True":
+                    value = True
+                elif string == "False":
+                    value = False
+                else:
+                    value = string
+        return value
+
+
+def respond(command, result):
+    sys.stdout.write(writePlistToString({'command' : command['command'], 'result' : result}))
+
+def respondWithError(msg, status=1):
+    sys.stdout.write(writePlistToString({'error' : msg, }))

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/gateway.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/gateway.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/gateway.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -28,10 +28,11 @@
 from twistedcaldav.directory.directory import DirectoryError
 from txdav.xml import element as davxml
 
-from calendarserver.tools.principals import (
+from calendarserver.tools.util import (
     principalForPrincipalID, proxySubprincipal, addProxy, removeProxy,
-    getProxies, setProxies, ProxyError, ProxyWarning, updateRecord
+    ProxyError, ProxyWarning
 )
+from calendarserver.tools.principals import getProxies, setProxies, updateRecord
 from calendarserver.tools.purge import WorkerService, PurgeOldEventsService, DEFAULT_BATCH_SIZE, DEFAULT_RETAIN_DAYS
 from calendarserver.tools.cmdline import utilityMain
 
@@ -212,7 +213,7 @@
         readProxies = command.get("ReadProxies", None)
         writeProxies = command.get("WriteProxies", None)
         principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(principal, readProxies, writeProxies, directory=self.dir))
+        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
 
         respondWithRecordsOfType(self.dir, command, "locations")
 
@@ -260,7 +261,7 @@
         readProxies = command.get("ReadProxies", None)
         writeProxies = command.get("WriteProxies", None)
         principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(principal, readProxies, writeProxies, directory=self.dir))
+        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
 
         yield self.command_getLocationAttributes(command)
 
@@ -300,7 +301,7 @@
         readProxies = command.get("ReadProxies", None)
         writeProxies = command.get("WriteProxies", None)
         principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(principal, readProxies, writeProxies, directory=self.dir))
+        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
 
         respondWithRecordsOfType(self.dir, command, "resources")
 
@@ -328,7 +329,7 @@
         readProxies = command.get("ReadProxies", None)
         writeProxies = command.get("WriteProxies", None)
         principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(principal, readProxies, writeProxies, directory=self.dir))
+        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
 
         yield self.command_getResourceAttributes(command)
 
@@ -370,7 +371,7 @@
             respondWithError("Proxy not found: %s" % (command['Proxy'],))
             return
         try:
-            (yield addProxy(principal, "write", proxy))
+            (yield addProxy(self.root, self.dir, self.store, principal, "write", proxy))
         except ProxyError, e:
             respondWithError(str(e))
             return
@@ -390,7 +391,7 @@
             respondWithError("Proxy not found: %s" % (command['Proxy'],))
             return
         try:
-            (yield removeProxy(principal, proxy, proxyTypes=("write",)))
+            (yield removeProxy(self.root, self.dir, self.store, principal, proxy, proxyTypes=("write",)))
         except ProxyError, e:
             respondWithError(str(e))
             return
@@ -419,7 +420,7 @@
             respondWithError("Proxy not found: %s" % (command['Proxy'],))
             return
         try:
-            (yield addProxy(principal, "read", proxy))
+            (yield addProxy(self.root, self.dir, self.store, principal, "read", proxy))
         except ProxyError, e:
             respondWithError(str(e))
             return
@@ -439,7 +440,7 @@
             respondWithError("Proxy not found: %s" % (command['Proxy'],))
             return
         try:
-            (yield removeProxy(principal, proxy, proxyTypes=("read",)))
+            (yield removeProxy(self.root, self.dir, self.store, principal, proxy, proxyTypes=("read",)))
         except ProxyError, e:
             respondWithError(str(e))
             return
@@ -516,13 +517,7 @@
 
 def respondWithError(msg, status=1):
     sys.stdout.write(writePlistToString({'error' : msg, }))
-    """
-    try:
-        reactor.stop()
-    except RuntimeError:
-        pass
-    sys.exit(status)
-    """
 
+
 if __name__ == "__main__":
     main()

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/principals.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/principals.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -20,33 +20,29 @@
 import sys
 import os
 import operator
-import signal
 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, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from txdav.xml import element as davxml
 
-from twext.python.log import clearLogLevels
-from twext.python.log import StandardIOObserver
-
 from txdav.xml.base import decodeXMLName, encodeXMLName
 
-from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.config import config
 from twistedcaldav.directory.directory import UnknownRecordTypeError, DirectoryError
+from twistedcaldav.directory.directory import scheduleNextGroupCachingUpdate
 
-from calendarserver.tools.util import loadConfig, getDirectory, setupMemcached,  booleanArgument, checkDirectory
+from calendarserver.tools.util import (
+    booleanArgument, proxySubprincipal, action_addProxyPrincipal,
+    principalForPrincipalID, prettyPrincipal, ProxyError,
+    action_removeProxyPrincipal
+)
 from twistedcaldav.directory.augment import allowedAutoScheduleModes
 
-__all__ = [
-    "principalForPrincipalID", "proxySubprincipal", "addProxy", "removeProxy",
-    "ProxyError", "ProxyWarning", "updateRecord"
-]
+from calendarserver.tools.cmdline import utilityMain, WorkerService
 
+
 def usage(e=None):
     if e:
         if isinstance(e, UnknownRecordTypeError):
@@ -99,6 +95,29 @@
     else:
         sys.exit(0)
 
+
+
+class PrincipalService(WorkerService):
+    """
+    Executes principals-related functions in a context which has access to the store
+    """
+
+    function = None
+    params = []
+
+    @inlineCallbacks
+    def doWork(self):
+        """
+        Calls the function that's been assigned to "function" and passes the root
+        resource, directory, store, and whatever has been assigned to "params".
+        """
+        if self.function is not None:
+            rootResource = self.rootResource()
+            directory = rootResource.getDirectory()
+            yield self.function(rootResource, directory, self._store, *self.params)
+
+
+
 def main():
     try:
         (optargs, args) = getopt(
@@ -239,70 +258,19 @@
         elif opt in ("", "--get-auto-accept-group"):
             principalActions.append((action_getAutoAcceptGroup,))
 
-
         else:
             raise NotImplementedError(opt)
 
     #
-    # Get configuration
-    #
-    try:
-        loadConfig(configFileName)
-
-        # Do this first, because modifying the config object will cause
-        # some logging activity at whatever log level the plist says
-        clearLogLevels()
-
-
-        config.DefaultLogLevel = "debug" if verbose else "error"
-
-        #
-        # Send logging output to stdout
-        #
-        observer = StandardIOObserver()
-        observer.start()
-
-        # Create the DataRoot directory before shedding privileges
-        if config.DataRoot.startswith(config.ServerRoot + os.sep):
-            checkDirectory(
-                config.DataRoot,
-                "Data root",
-                access=os.W_OK,
-                create=(0750, config.UserName, config.GroupName),
-            )
-
-        # 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)
-
-        # Configure memcached client settings prior to setting up resource
-        # hierarchy (in getDirectory)
-        setupMemcached(config)
-
-        try:
-            config.directory = getDirectory()
-        except DirectoryError, e:
-            abort(e)
-
-    except ConfigurationError, e:
-        abort(e)
-
-    #
     # List principals
     #
     if listPrincipalTypes:
         if args:
             usage("Too many arguments")
 
-        for recordType in config.directory.recordTypes():
-            print(recordType)
+        function = runListPrincipalTypes
+        params = ()
 
-        return
-
     elif addType:
 
         try:
@@ -322,9 +290,9 @@
         else:
             shortNames = ()
 
-        params = (runAddPrincipal, addType, guid, shortNames, fullName)
+        function = runAddPrincipal
+        params = (addType, guid, shortNames, fullName)
 
-
     elif listPrincipals:
         try:
             listPrincipals = matchStrings(listPrincipals, ["users", "groups",
@@ -336,19 +304,12 @@
         if args:
             usage("Too many arguments")
 
-        try:
-            records = list(config.directory.listRecords(listPrincipals))
-            if records:
-                printRecordList(records)
-            else:
-                print("No records of type %s" % (listPrincipals,))
-        except UnknownRecordTypeError, e:
-            usage(e)
+        function = runListPrincipals
+        params = (listPrincipals,)
 
-        return
-
     elif searchPrincipals:
-        params = (runSearch, searchPrincipals)
+        function = runSearch
+        params = (searchPrincipals,)
 
     else:
         #
@@ -364,178 +325,123 @@
             except ValueError, e:
                 abort(e)
 
-        params = (runPrincipalActions, args, principalActions)
+        function = runPrincipalActions
+        params = (args, principalActions)
 
-    #
-    # Start the reactor
-    #
-    reactor.callLater(0, *params)
-    reactor.run()
+    PrincipalService.function = function
+    PrincipalService.params = params
+    utilityMain(configFileName, PrincipalService, verbose=verbose)
 
 
 
- at inlineCallbacks
-def runPrincipalActions(principalIDs, actions):
-    try:
-        for principalID in principalIDs:
-            # Resolve the given principal IDs to principals
-            try:
-                principal = principalForPrincipalID(principalID)
-            except ValueError:
-                principal = None
+def runListPrincipalTypes(service, rootResource, directory, store):
+    for recordType in directory.recordTypes():
+        print(recordType)
+    return succeed(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()
-
- at inlineCallbacks
-def runSearch(searchTerm):
-
+def runListPrincipals(service, rootResource, directory, store, listPrincipals):
     try:
-        fields = []
-        for fieldName in ("fullName", "firstName", "lastName", "emailAddresses"):
-            fields.append((fieldName, searchTerm, True, "contains"))
-
-        records = list((yield config.directory.recordsMatchingTokens(searchTerm.strip().split())))
+        records = list(directory.listRecords(listPrincipals))
         if records:
-            records.sort(key=operator.attrgetter('fullName'))
-            print("%d matches found:" % (len(records),))
-            for record in records:
-                print("\n%s (%s)" % (record.fullName,
-                    { "users"     : "User",
-                      "groups"    : "Group",
-                      "locations" : "Place",
-                      "resources" : "Resource",
-                    }.get(record.recordType),
-                ))
-                print("   GUID: %s" % (record.guid,))
-                print("   Record name(s): %s" % (", ".join(record.shortNames),))
-                if record.authIDs:
-                    print("   Auth ID(s): %s" % (", ".join(record.authIDs),))
-                if record.emailAddresses:
-                    print("   Email(s): %s" % (", ".join(record.emailAddresses),))
+            printRecordList(records)
         else:
-            print("No matches found")
+            print("No records of type %s" % (listPrincipals,))
+    except UnknownRecordTypeError, e:
+        usage(e)
+    return succeed(None)
 
-        print("")
 
-    finally:
-        #
-        # Stop the reactor
-        #
-        reactor.stop()
 
 @inlineCallbacks
-def runAddPrincipal(addType, guid, shortNames, fullName):
-    try:
+def runPrincipalActions(service, rootResource, directory, store, principalIDs,
+    actions):
+    for principalID in principalIDs:
+        # Resolve the given principal IDs to principals
         try:
-            yield updateRecord(True, config.directory, addType, guid=guid,
-                shortNames=shortNames, fullName=fullName)
-            print("Added '%s'" % (fullName,))
-        except DirectoryError, e:
-            print(e)
+            principal = principalForPrincipalID(principalID, directory=directory)
+        except ValueError:
+            principal = None
 
-    finally:
-        #
-        # Stop the reactor
-        #
-        reactor.stop()
+        if principal is None:
+            sys.stderr.write("Invalid principal ID: %s\n" % (principalID,))
+            continue
 
+        # Performs requested actions
+        for action in actions:
+            (yield action[0](rootResource, directory, store, principal,
+                *action[1:]))
+            print("")
 
-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("/"):
-        segments = principalID.strip("/").split("/")
-        if (len(segments) == 3 and
-            segments[0] == "principals" and segments[1] == "__uids__"):
-            uid = segments[2]
-        else:
-            raise ValueError("Can't resolve all paths yet")
 
-        if checkOnly:
-            return None
+ at inlineCallbacks
+def runSearch(service, rootResource, directory, store, searchTerm):
 
-        return directory.principalCollection.principalForUID(uid)
+    fields = []
+    for fieldName in ("fullName", "firstName", "lastName", "emailAddresses"):
+        fields.append((fieldName, searchTerm, True, "contains"))
 
+    records = list((yield directory.recordsMatchingTokens(searchTerm.strip().split())))
+    if records:
+        records.sort(key=operator.attrgetter('fullName'))
+        print("%d matches found:" % (len(records),))
+        for record in records:
+            print("\n%s (%s)" % (record.fullName,
+                {"users" : "User",
+                 "groups" : "Group",
+                 "locations" : "Place",
+                 "resources" : "Resource",
+                }.get(record.recordType),
+            ))
+            print("   GUID: %s" % (record.guid,))
+            print("   Record name(s): %s" % (", ".join(record.shortNames),))
+            if record.authIDs:
+                print("   Auth ID(s): %s" % (", ".join(record.authIDs),))
+            if record.emailAddresses:
+                print("   Email(s): %s" % (", ".join(record.emailAddresses),))
+    else:
+        print("No matches found")
 
-    if principalID.startswith("("):
-        try:
-            i = principalID.index(")")
+    print("")
 
-            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)
-
+ at inlineCallbacks
+def runAddPrincipal(service, rootResource, directory, store, addType, guid,
+    shortNames, fullName):
     try:
-        UUID(principalID)
+        yield updateRecord(True, directory, addType, guid=guid,
+            shortNames=shortNames, fullName=fullName)
+        print("Added '%s'" % (fullName,))
+    except DirectoryError, e:
+        print(e)
 
-        if checkOnly:
-            return None
 
-        x = directory.principalCollection.principalForUID(principalID)
-        return x
-    except ValueError:
-        pass
 
-    raise ValueError("Invalid principal identifier: %s" % (principalID,))
-
-def proxySubprincipal(principal, proxyType):
-    return principal.getChild("calendar-proxy-" + proxyType)
-
-def action_removePrincipal(principal):
+def action_removePrincipal(rootResource, directory, store, principal):
     record = principal.record
     fullName = record.fullName
     shortName = record.shortNames[0]
     guid = record.guid
 
-    config.directory.destroyRecord(record.recordType, guid=guid)
+    directory.destroyRecord(record.recordType, guid=guid)
     print("Removed '%s' %s %s" % (fullName, shortName, guid))
 
 
+
 @inlineCallbacks
-def action_readProperty(resource, qname):
+def action_readProperty(rootResource, directory, store, resource, qname):
     property = (yield resource.readProperty(qname, None))
     print("%r on %s:" % (encodeXMLName(*qname), resource))
     print("")
     print(property.toxml())
 
+
+
 @inlineCallbacks
-def action_listProxies(principal, *proxyTypes):
+def action_listProxies(rootResource, directory, store, principal, *proxyTypes):
     for proxyType in proxyTypes:
         subPrincipal = proxySubprincipal(principal, proxyType)
         if subPrincipal is None:
@@ -553,7 +459,7 @@
             records = []
             for member in membersProperty.children:
                 proxyPrincipal = principalForPrincipalID(str(member),
-                    directory=config.directory)
+                    directory=directory)
                 records.append(proxyPrincipal.record)
 
             printRecordList(records)
@@ -562,60 +468,22 @@
             print("No %s proxies for %s" % (proxyType,
                 prettyPrincipal(principal)))
 
+
+
 @inlineCallbacks
-def action_addProxy(principal, proxyType, *proxyIDs):
+def action_addProxy(rootResource, directory, store, principal, proxyType, *proxyIDs):
     for proxyID in proxyIDs:
-        proxyPrincipal = principalForPrincipalID(proxyID)
+        proxyPrincipal = principalForPrincipalID(proxyID, directory=directory)
         if proxyPrincipal is None:
             print("Invalid principal ID: %s" % (proxyID,))
         else:
-            (yield action_addProxyPrincipal(principal, proxyType, proxyPrincipal))
+            (yield action_addProxyPrincipal(rootResource, directory, store,
+                principal, proxyType, proxyPrincipal))
 
- at inlineCallbacks
-def action_addProxyPrincipal(principal, proxyType, proxyPrincipal):
-    try:
-        (yield addProxy(principal, proxyType, proxyPrincipal))
-        print("Added %s as a %s proxy for %s" % (
-            prettyPrincipal(proxyPrincipal), proxyType,
-            prettyPrincipal(principal)))
-    except ProxyError, e:
-        print("Error:", e)
-    except ProxyWarning, e:
-        print(e)
 
- at inlineCallbacks
-def addProxy(principal, proxyType, proxyPrincipal):
-    proxyURL = proxyPrincipal.url()
 
-    subPrincipal = proxySubprincipal(principal, proxyType)
-    if subPrincipal is None:
-        raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
-            prettyPrincipal(principal)))
-
-    membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-
-    for memberURL in membersProperty.children:
-        if str(memberURL) == proxyURL:
-            raise ProxyWarning("%s is already a %s proxy for %s" % (
-                prettyPrincipal(proxyPrincipal), proxyType,
-                prettyPrincipal(principal)))
-
-    else:
-        memberURLs = list(membersProperty.children)
-        memberURLs.append(davxml.HRef(proxyURL))
-        membersProperty = davxml.GroupMemberSet(*memberURLs)
-        (yield subPrincipal.writeProperty(membersProperty, None))
-
-    proxyTypes = ["read", "write"]
-    proxyTypes.remove(proxyType)
-
-    (yield action_removeProxyPrincipal(principal, proxyPrincipal, proxyTypes=proxyTypes))
-
-    triggerGroupCacherUpdate(config)
-
-
 @inlineCallbacks
-def setProxies(principal, readProxyPrincipals, writeProxyPrincipals, directory=None):
+def setProxies(store, principal, readProxyPrincipals, writeProxyPrincipals, directory=None):
     """
     Set read/write proxies en masse for a principal
     @param principal: DirectoryPrincipalResource
@@ -640,10 +508,12 @@
             proxyURL = proxyPrincipal.url()
             memberURLs.append(davxml.HRef(proxyURL))
         membersProperty = davxml.GroupMemberSet(*memberURLs)
-        (yield subPrincipal.writeProperty(membersProperty, None))
-        triggerGroupCacherUpdate(config)
+        yield subPrincipal.writeProperty(membersProperty, None)
+        if store is not None:
+            yield scheduleNextGroupCachingUpdate(store, 0)
 
 
+
 @inlineCallbacks
 def getProxies(principal, directory=None):
     """
@@ -667,64 +537,21 @@
     returnValue((proxies['read'], proxies['write']))
 
 
+
 @inlineCallbacks
-def action_removeProxy(principal, *proxyIDs, **kwargs):
+def action_removeProxy(rootResource, directory, store, principal, *proxyIDs, **kwargs):
     for proxyID in proxyIDs:
-        proxyPrincipal = principalForPrincipalID(proxyID)
+        proxyPrincipal = principalForPrincipalID(proxyID, directory=directory)
         if proxyPrincipal is None:
             print("Invalid principal ID: %s" % (proxyID,))
         else:
-            (yield action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs))
+            (yield action_removeProxyPrincipal(rootResource, directory, store,
+                principal, proxyPrincipal, **kwargs))
 
- at inlineCallbacks
-def action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs):
-    try:
-        removed = (yield removeProxy(principal, proxyPrincipal, **kwargs))
-        if removed:
-            print("Removed %s as a proxy for %s" % (
-                prettyPrincipal(proxyPrincipal),
-                prettyPrincipal(principal)))
-    except ProxyError, e:
-        print("Error:", e)
-    except ProxyWarning, e:
-        print(e)
 
 
 @inlineCallbacks
-def removeProxy(principal, proxyPrincipal, **kwargs):
-    removed = False
-    proxyTypes = kwargs.get("proxyTypes", ("read", "write"))
-    for proxyType in proxyTypes:
-        proxyURL = proxyPrincipal.url()
-
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is None:
-            raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
-                prettyPrincipal(principal)))
-
-        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
-        else:
-            removed = True
-
-        membersProperty = davxml.GroupMemberSet(*memberURLs)
-        (yield subPrincipal.writeProperty(membersProperty, None))
-
-    if removed:
-        triggerGroupCacherUpdate(config)
-    returnValue(removed)
-
-
- at inlineCallbacks
-def action_setAutoSchedule(principal, autoSchedule):
+def action_setAutoSchedule(rootResource, directory, store, principal, autoSchedule):
     if principal.record.recordType == "groups":
         print("Enabling auto-schedule for %s is not allowed." % (principal,))
 
@@ -733,11 +560,11 @@
 
     else:
         print("Setting auto-schedule to %s for %s" % (
-            { True: "true", False: "false" }[autoSchedule],
+            {True: "true", False: "false"}[autoSchedule],
             prettyPrincipal(principal),
         ))
 
-        (yield updateRecord(False, config.directory,
+        (yield updateRecord(False, directory,
             principal.record.recordType,
             guid=principal.record.guid,
             shortNames=principal.record.shortNames,
@@ -746,15 +573,19 @@
             **principal.record.extras
         ))
 
-def action_getAutoSchedule(principal):
+
+
+def action_getAutoSchedule(rootResource, directory, store, principal):
     autoSchedule = principal.getAutoSchedule()
     print("Auto-schedule for %s is %s" % (
         prettyPrincipal(principal),
-        { True: "true", False: "false" }[autoSchedule],
+        {True: "true", False: "false"}[autoSchedule],
     ))
 
+
+
 @inlineCallbacks
-def action_setAutoScheduleMode(principal, autoScheduleMode):
+def action_setAutoScheduleMode(rootResource, directory, store, principal, autoScheduleMode):
     if principal.record.recordType == "groups":
         print("Setting auto-schedule mode for %s is not allowed." % (principal,))
 
@@ -767,7 +598,7 @@
             prettyPrincipal(principal),
         ))
 
-        (yield updateRecord(False, config.directory,
+        (yield updateRecord(False, directory,
             principal.record.recordType,
             guid=principal.record.guid,
             shortNames=principal.record.shortNames,
@@ -776,7 +607,9 @@
             **principal.record.extras
         ))
 
-def action_getAutoScheduleMode(principal):
+
+
+def action_getAutoScheduleMode(rootResource, directory, store, principal):
     autoScheduleMode = principal.getAutoScheduleMode()
     if not autoScheduleMode:
         autoScheduleMode = "automatic"
@@ -785,8 +618,10 @@
         autoScheduleMode,
     ))
 
+
+
 @inlineCallbacks
-def action_setAutoAcceptGroup(principal, autoAcceptGroup):
+def action_setAutoAcceptGroup(rootResource, directory, store, principal, autoAcceptGroup):
     if principal.record.recordType == "groups":
         print("Setting auto-accept-group for %s is not allowed." % (principal,))
 
@@ -794,7 +629,7 @@
         print("Setting auto-accept-group for %s is not allowed." % (principal,))
 
     else:
-        groupPrincipal = principalForPrincipalID(autoAcceptGroup)
+        groupPrincipal = principalForPrincipalID(autoAcceptGroup, directory=directory)
         if groupPrincipal is None or groupPrincipal.record.recordType != "groups":
             print("Invalid principal ID: %s" % (autoAcceptGroup,))
         else:
@@ -803,7 +638,7 @@
                 prettyPrincipal(principal),
             ))
 
-            (yield updateRecord(False, config.directory,
+            (yield updateRecord(False, directory,
                 principal.record.recordType,
                 guid=principal.record.guid,
                 shortNames=principal.record.shortNames,
@@ -812,12 +647,14 @@
                 **principal.record.extras
             ))
 
-def action_getAutoAcceptGroup(principal):
+
+
+def action_getAutoAcceptGroup(rootResource, directory, store, principal):
     autoAcceptGroup = principal.getAutoAcceptGroup()
     if autoAcceptGroup:
-        record = config.directory.recordWithGUID(autoAcceptGroup)
+        record = directory.recordWithGUID(autoAcceptGroup)
         if record is not None:
-            groupPrincipal = config.directory.principalCollection.principalForUID(record.uid)
+            groupPrincipal = directory.principalCollection.principalForUID(record.uid)
             if groupPrincipal is not None:
                 print("Auto-accept-group for %s is %s" % (
                     prettyPrincipal(principal),
@@ -829,6 +666,7 @@
         print("No auto-accept-group assigned to %s" % (prettyPrincipal(principal),))
 
 
+
 def abort(msg, status=1):
     sys.stdout.write("%s\n" % (msg,))
     try:
@@ -837,16 +675,7 @@
         pass
     sys.exit(status)
 
-class ProxyError(Exception):
-    """
-    Raised when proxy assignments cannot be performed
-    """
 
-class ProxyWarning(Exception):
-    """
-    Raised for harmless proxy assignment failures such as trying to add a
-    duplicate or remove a non-existent assignment.
-    """
 
 def parseCreationArgs(args):
     """
@@ -875,6 +704,7 @@
     return fullName, shortName, guid
 
 
+
 def isUUID(value):
     try:
         UUID(value)
@@ -882,6 +712,8 @@
     except:
         return False
 
+
+
 def matchStrings(value, validValues):
     for validValue in validValues:
         if validValue.startswith(value):
@@ -890,6 +722,7 @@
     raise ValueError("'%s' is not a recognized value" % (value,))
 
 
+
 def printRecordList(records):
     results = [(record.fullName, record.shortNames[0], record.guid)
         for record in records]
@@ -900,10 +733,6 @@
     for fullName, shortName, guid in results:
         print(format % (fullName, shortName, guid))
 
-def prettyPrincipal(principal):
-    record = principal.record
-    return "\"%s\" (%s:%s)" % (record.fullName, record.recordType,
-        record.shortNames[0])
 
 
 @inlineCallbacks
@@ -917,7 +746,7 @@
     """
 
     assignAutoSchedule = False
-    if kwargs.has_key("autoSchedule"):
+    if "autoSchedule" in kwargs:
         assignAutoSchedule = True
         autoSchedule = kwargs["autoSchedule"]
         del kwargs["autoSchedule"]
@@ -926,7 +755,7 @@
         autoSchedule = recordType in ("locations", "resources")
 
     assignAutoScheduleMode = False
-    if kwargs.has_key("autoScheduleMode"):
+    if "autoScheduleMode" in kwargs:
         assignAutoScheduleMode = True
         autoScheduleMode = kwargs["autoScheduleMode"]
         del kwargs["autoScheduleMode"]
@@ -935,7 +764,7 @@
         autoScheduleMode = None
 
     assignAutoAcceptGroup = False
-    if kwargs.has_key("autoAcceptGroup"):
+    if "autoAcceptGroup" in kwargs:
         assignAutoAcceptGroup = True
         autoAcceptGroup = kwargs["autoAcceptGroup"]
         del kwargs["autoAcceptGroup"]
@@ -984,28 +813,6 @@
     returnValue(record)
 
 
-def triggerGroupCacherUpdate(config, killMethod=None):
-    """
-    Look up the pid of the group cacher sidecar and HUP it to trigger an update
-    """
-    if killMethod is None:
-        killMethod = os.kill
 
-    pidFilename = os.path.join(config.RunRoot, "groupcacher.pid")
-    if os.path.exists(pidFilename):
-        pidFile = open(pidFilename, "r")
-        pid = pidFile.read().strip()
-        pidFile.close()
-        try:
-            pid = int(pid)
-        except ValueError:
-            return
-        try:
-            killMethod(pid, signal.SIGHUP)
-        except OSError:
-            pass
-
-
-
 if __name__ == "__main__":
     main()

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/purge.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/purge.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -18,12 +18,10 @@
 from __future__ import print_function
 
 from calendarserver.tap.util import FakeRequest
-from calendarserver.tap.util import getRootResource
 from calendarserver.tools import tables
-from calendarserver.tools.cmdline import utilityMain
-from calendarserver.tools.principals import removeProxy
+from calendarserver.tools.cmdline import utilityMain, WorkerService
+from calendarserver.tools.util import removeProxy
 
-from errno import ENOENT, EACCES
 from getopt import getopt, GetoptError
 
 from pycalendar.datetime import PyCalendarDateTime
@@ -31,13 +29,10 @@
 from twext.python.log import Logger
 from twext.web2.responsecode import NO_CONTENT
 
-from twisted.application.service import Service
-from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 from twistedcaldav import caldavxml
 from twistedcaldav.caldavxml import TimeRange
-from twistedcaldav.config import config, ConfigurationError
 from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 from twistedcaldav.directory.directory import DirectoryRecord
 from twistedcaldav.method.put_common import StoreCalendarObjectResource
@@ -45,6 +40,7 @@
 
 from txdav.xml import element as davxml
 
+
 import collections
 import os
 import sys
@@ -54,49 +50,8 @@
 DEFAULT_BATCH_SIZE = 100
 DEFAULT_RETAIN_DAYS = 365
 
-class WorkerService(Service):
 
-    def __init__(self, store):
-        self._store = store
 
-
-    def rootResource(self):
-        try:
-            rootResource = getRootResource(config, self._store)
-        except OSError, e:
-            if e.errno == ENOENT:
-                # Trying to re-write resources.xml but its parent directory does
-                # not exist.  The server's never been started, so we're missing
-                # state required to do any work.  (Plus, what would be the point
-                # of purging stuff from a server that's completely empty?)
-                raise ConfigurationError(
-                    "It appears that the server has never been started.\n"
-                    "Please start it at least once before purging anything.")
-            elif e.errno == EACCES:
-                # Trying to re-write resources.xml but it is not writable by the
-                # current user.  This most likely means we're in a system
-                # configuration and the user doesn't have sufficient privileges
-                # to do the other things the tool might need to do either.
-                raise ConfigurationError("You must run this tool as root.")
-            else:
-                raise
-        return rootResource
-
-
-    @inlineCallbacks
-    def startService(self):
-        try:
-            yield self.doWork()
-        except ConfigurationError, ce:
-            sys.stderr.write("Error: %s\n" % (str(ce),))
-        except Exception, e:
-            sys.stderr.write("Error: %s\n" % (e,))
-            raise
-        finally:
-            reactor.stop()
-
-
-
 class PurgeOldEventsService(WorkerService):
 
     cutoff = None
@@ -792,10 +747,14 @@
             # FIXME: probably want a more elegant way to accomplish this,
             # since it requires the aggregate directory to examine these first:
             record = DirectoryRecord(self.directory, "users", uid, shortNames=(uid,), enabledForCalendaring=True)
-            record.enabled = True
             self.directory._tmpRecords["shortNames"][uid] = record
             self.directory._tmpRecords["uids"][uid] = record
 
+        # Override augments settings for this record
+        record.enabled = True
+        record.enabledForCalendaring = True
+        record.enabledForAddressBooks = True
+
         cua = "urn:uuid:%s" % (uid,)
 
         principalCollection = self.directory.principalCollection
@@ -872,76 +831,97 @@
 
                         for childName in childNames:
 
-                            childResource = (yield collection.getChild(childName))
-                            # Allways delete inbox items
-                            if self.completely or collName == "inbox":
-                                action = self.CANCELEVENT_SHOULD_DELETE
-                            else:
-                                event = (yield childResource.iCalendar())
-                                event = perUserFilter.filter(event)
-                                action = self._cancelEvent(event, self.when, cua)
-
-                            uri = "/calendars/__uids__/%s/%s/%s" % (uid, collName, childName)
-                            request.path = uri
-                            if action == self.CANCELEVENT_MODIFIED:
-                                count += 1
-                                request._rememberResource(childResource, uri)
-                                storer = StoreCalendarObjectResource(
-                                    request=request,
-                                    destination=childResource,
-                                    destination_uri=uri,
-                                    destinationcal=True,
-                                    destinationparent=collection,
-                                    calendar=str(event),
+                            try:
+                                perresource_request = FakeRequest(self.root, None, None)
+                                perresource_request.checkedSACL = True
+                                perresource_request.authnUser = perresource_request.authzUser = davxml.Principal(
+                                        davxml.HRef.fromString("/principals/__uids__/%s/" % (uid,))
                                 )
-                                if self.verbose:
-                                    if self.dryrun:
-                                        print("Would modify: %s" % (uri,))
-                                    else:
-                                        print("Modifying: %s" % (uri,))
-                                if not self.dryrun:
-                                    result = (yield storer.run())
+                                perresource_request._rememberResource(collection,
+                                    "/calendars/__uids__/%s/%s/" % (uid, collName))
 
-                            elif action == self.CANCELEVENT_SHOULD_DELETE:
-                                incrementCount = self.dryrun
-                                request._rememberResource(childResource, uri)
-                                if self.verbose:
-                                    if self.dryrun:
-                                        print("Would delete: %s" % (uri,))
-                                    else:
-                                        print("Deleting: %s" % (uri,))
-                                if not self.dryrun:
-                                    retry = False
-                                    try:
-                                        result = (yield childResource.storeRemove(request, self.doimplicit, uri))
-                                        if result != NO_CONTENT:
-                                            print("Error deleting %s/%s/%s: %s" % (uid,
-                                                collName, childName, result))
-                                            retry = True
-                                        else:
-                                            incrementCount = True
+                                childResource = (yield collection.getChild(childName))
 
-                                    except Exception, e:
-                                        print("Exception deleting %s/%s/%s: %s" % (uid,
-                                            collName, childName, str(e)))
-                                        retry = True
+                                # Allways delete inbox items
+                                if self.completely or collName == "inbox":
+                                    action = self.CANCELEVENT_SHOULD_DELETE
+                                else:
+                                    event = (yield childResource.iCalendar())
+                                    event = perUserFilter.filter(event)
+                                    action = self._cancelEvent(event, self.when, cua)
 
-                                    if retry and self.doimplicit:
-                                        # Try again with implicit scheduling off
-                                        print("Retrying deletion of %s/%s/%s with implicit scheduling turned off" % (uid, collName, childName))
+                                uri = "/calendars/__uids__/%s/%s/%s" % (uid, collName, childName)
+                                perresource_request.path = uri
+                                if action == self.CANCELEVENT_MODIFIED:
+                                    perresource_request._rememberResource(childResource, uri)
+                                    storer = StoreCalendarObjectResource(
+                                        request=perresource_request,
+                                        destination=childResource,
+                                        destination_uri=uri,
+                                        destinationcal=True,
+                                        destinationparent=collection,
+                                        calendar=str(event),
+                                    )
+                                    if self.verbose:
+                                        if self.dryrun:
+                                            print("Would modify: %s" % (uri,))
+                                        else:
+                                            print("Modifying: %s" % (uri,))
+                                    if not self.dryrun:
+                                        result = (yield storer.run())
+                                    count += 1
+
+                                elif action == self.CANCELEVENT_SHOULD_DELETE:
+                                    incrementCount = self.dryrun
+                                    perresource_request._rememberResource(childResource, uri)
+                                    if self.verbose:
+                                        if self.dryrun:
+                                            print("Would delete: %s" % (uri,))
+                                        else:
+                                            print("Deleting: %s" % (uri,))
+                                    if not self.dryrun:
+                                        retry = False
                                         try:
-                                            result = (yield childResource.storeRemove(request, False, uri))
+                                            result = (yield childResource.storeRemove(perresource_request, self.doimplicit, uri))
                                             if result != NO_CONTENT:
                                                 print("Error deleting %s/%s/%s: %s" % (uid,
                                                     collName, childName, result))
+                                                retry = True
                                             else:
                                                 incrementCount = True
+
                                         except Exception, e:
-                                            print("Still couldn't delete %s/%s/%s even with implicit scheduling turned off: %s" % (uid, collName, childName, str(e)))
+                                            print("Exception deleting %s/%s/%s: %s" % (uid,
+                                                collName, childName, str(e)))
+                                            retry = True
 
-                                if incrementCount:
-                                    count += 1
+                                        if retry and self.doimplicit:
+                                            # Try again with implicit scheduling off
+                                            print("Retrying deletion of %s/%s/%s with implicit scheduling turned off" % (uid, collName, childName))
+                                            try:
+                                                result = (yield childResource.storeRemove(perresource_request, False, uri))
+                                                if result != NO_CONTENT:
+                                                    print("Error deleting %s/%s/%s: %s" % (uid,
+                                                        collName, childName, result))
+                                                else:
+                                                    incrementCount = True
+                                            except Exception, e:
+                                                print("Still couldn't delete %s/%s/%s even with implicit scheduling turned off: %s" % (uid, collName, childName, str(e)))
 
+                                    if incrementCount:
+                                        count += 1
+                                txn = getattr(perresource_request, "_newStoreTransaction", None)
+                                # Commit
+                                if txn is not None:
+                                    (yield txn.commit())
+
+                            except Exception, e:
+                                # Abort
+                                txn = getattr(perresource_request, "_newStoreTransaction", None)
+                                if txn is not None:
+                                    (yield txn.abort())
+                                raise e
+
             txn = getattr(request, "_newStoreTransaction", None)
             # Commit
             if txn is not None:
@@ -1137,9 +1117,8 @@
             return cls.CANCELEVENT_NOT_MODIFIED
 
 
-    @classmethod
     @inlineCallbacks
-    def _purgeProxyAssignments(cls, principal):
+    def _purgeProxyAssignments(self, principal):
 
         assignments = []
 
@@ -1148,7 +1127,7 @@
             proxyFor = (yield principal.proxyFor(proxyType == "write"))
             for other in proxyFor:
                 assignments.append((principal.record.uid, proxyType, other.record.uid))
-                (yield removeProxy(other, principal))
+                (yield removeProxy(self.root, self.directory, self._store, other, principal))
 
             subPrincipal = principal.getChild("calendar-proxy-" + proxyType)
             proxies = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/push.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/push.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/push.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -19,39 +19,18 @@
 from calendarserver.tap.util import getRootResource
 from calendarserver.tools.cmdline import utilityMain
 from errno import ENOENT, EACCES
-from getopt import getopt, GetoptError
+from argparse import ArgumentParser
 from twext.python.log import Logger
 from twisted.application.service import Service
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks
 from twistedcaldav.config import config, ConfigurationError
-import os
 import sys
 import time
 
 log = Logger()
 
-def usage(e=None):
 
-    name = os.path.basename(sys.argv[0])
-    print("usage: %s [options] [user ...]" % (name,))
-    print("")
-    print("  Display Apple Push Notification subscriptions")
-    print("")
-    print("options:")
-    print("  -h --help: print this help and exit")
-    print("  -f --config <path>: Specify caldavd.plist configuration path")
-    print("  -D --debug: debug logging")
-    print("")
-
-    if e:
-        sys.stderr.write("%s\n" % (e,))
-        sys.exit(64)
-    else:
-        sys.exit(0)
-
-
-
 class WorkerService(Service):
 
     def __init__(self, store):
@@ -109,45 +88,18 @@
 
 def main():
 
-    try:
-        (optargs, args) = getopt(
-            sys.argv[1:], "Df:h", [
-                "config=",
-                "help",
-                "debug",
-            ],
-        )
-    except GetoptError, e:
-        usage(e)
+    parser = ArgumentParser(description='Display Apple Push Notification subscriptions')
+    parser.add_argument('-f', '--config', dest='configFileName', metavar='CONFIGFILE', help='caldavd.plist configuration file path')
+    parser.add_argument('-d', '--debug', action='store_true', help='show debug logging')
+    parser.add_argument('user', help='one or more users to display', nargs='+') # Required
+    args = parser.parse_args()
 
-    #
-    # Get configuration
-    #
-    configFileName = None
-    debug = False
+    DisplayAPNSubscriptions.users = args.user
 
-    for opt, arg in optargs:
-        if opt in ("-h", "--help"):
-            usage()
-
-        elif opt in ("-f", "--config"):
-            configFileName = arg
-
-        if opt in ("-d", "--debug"):
-            debug = True
-
-        else:
-            raise NotImplementedError(opt)
-
-    if not args:
-        usage("Not enough arguments")
-
-    DisplayAPNSubscriptions.users = args
-
     utilityMain(
-        configFileName,
+        args.configFileName,
         DisplayAPNSubscriptions,
-        verbose=debug,
+        verbose=args.debug,
     )
 
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/gateway/caldavd.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/gateway/caldavd.plist	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/gateway/caldavd.plist	2013-04-11 16:24:18 UTC (rev 11029)
@@ -34,6 +34,14 @@
     <key>ServerHostName</key>
     <string></string> <!-- The hostname clients use when connecting -->
 
+    <!-- Enable Calendars -->
+    <key>EnableCalDAV</key>
+    <true/>
+
+    <!-- Enable AddressBooks -->
+    <key>EnableCardDAV</key>
+    <true/>
+
     <!-- HTTP port [0 = disable HTTP] -->
     <key>HTTPPort</key>
     <integer>8008</integer>
@@ -87,7 +95,7 @@
 
     <!-- Configuration root -->
     <key>ConfigRoot</key>
-    <string>/etc/caldavd</string>
+    <string>config</string>
 
     <!-- Log root -->
     <key>LogRoot</key>
@@ -496,65 +504,31 @@
 
       <key>Services</key>
       <dict>
-        <key>SimpleLineNotifier</key>
-        <dict>
-          <!-- Simple line notification service (for testing) -->
-          <key>Service</key>
-          <string>twistedcaldav.notify.SimpleLineNotifierService</string>
-          <key>Enabled</key>
-          <false/>
-          <key>Port</key>
-          <integer>62308</integer>
-        </dict>
 
-        <key>XMPPNotifier</key>
+        <key>APNS</key>
         <dict>
-          <!-- XMPP notification service -->
-          <key>Service</key>
-          <string>twistedcaldav.notify.XMPPNotifierService</string>
           <key>Enabled</key>
           <false/>
-
-          <!-- XMPP host and port to contact -->
-          <key>Host</key>
-          <string>xmpp.host.name</string>
-          <key>Port</key>
-          <integer>5222</integer>
-
-          <!-- Jabber ID and password for the server -->
-          <key>JID</key>
-          <string>jid at xmpp.host.name/resource</string>
-          <key>Password</key>
-          <string>password_goes_here</string>
-
-          <!-- PubSub service address -->
-          <key>ServiceAddress</key>
-          <string>pubsub.xmpp.host.name</string>
-
-          <key>NodeConfiguration</key>
+          <key>EnableStaggering</key>
+          <true/>
+          <key>StaggerSeconds</key>
+          <integer>5</integer>
+          <key>CalDAV</key>
           <dict>
-            <key>pubsub#deliver_payloads</key>
-            <string>1</string>
-            <key>pubsub#persist_items</key>
-            <string>1</string>
+            <key>CertificatePath</key>
+            <string>/example/calendar.cer</string>
+            <key>PrivateKeyPath</key>
+            <string>/example/calendar.pem</string>
           </dict>
-
-          <!-- Sends a presence notification to XMPP server at this interval (prevents disconnect) -->
-          <key>KeepAliveSeconds</key>
-          <integer>120</integer>
-
-          <!-- Sends a pubsub publish to a particular heartbeat node at this interval -->
-          <key>HeartbeatMinutes</key>
-          <integer>30</integer>
-
-          <!-- List of glob-like expressions defining which XMPP JIDs can converse with the server (for debugging) -->
-          <key>AllowedJIDs</key>
-          <array>
-            <!--
-            <string>*.example.com</string>
-             -->
-          </array>
+          <key>CardDAV</key>
+          <dict>
+            <key>CertificatePath</key>
+            <string>/example/contacts.cer</string>
+            <key>PrivateKeyPath</key>
+            <string>/example/contacts.pem</string>
+          </dict>
         </dict>
+
       </dict>
     </dict>
 
@@ -764,5 +738,12 @@
     </dict>
 
 
+    <key>Includes</key>
+    <array>
+        <string>%(WritablePlist)s</string>
+    </array>
+    <key>WritableConfigFile</key>
+    <string>%(WritablePlist)s</string>
+
   </dict>
 </plist>

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/principals/caldavd.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/principals/caldavd.plist	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/principals/caldavd.plist	2013-04-11 16:24:18 UTC (rev 11029)
@@ -79,23 +79,23 @@
 
     <!-- Data root -->
     <key>DataRoot</key>
-    <string>Data</string>
+    <string>%(DataRoot)s</string>
 
     <!-- Document root -->
     <key>DocumentRoot</key>
-    <string>Documents</string>
+    <string>%(DocumentRoot)s</string>
 
     <!-- Configuration root -->
     <key>ConfigRoot</key>
-    <string>/etc/caldavd</string>
+    <string>Config</string>
 
     <!-- Log root -->
     <key>LogRoot</key>
-    <string>/var/log/caldavd</string>
+    <string>%(LogRoot)s</string>
 
     <!-- Run root -->
     <key>RunRoot</key>
-    <string>/var/run</string>
+    <string>%(LogRoot)s</string>
 
     <!-- Child aliases -->
     <key>Aliases</key>
@@ -279,7 +279,7 @@
      -->
 
 	<key>ProxyLoadFromFile</key>
-    <string>conf/auth/proxies-test.xml</string>
+    <string></string>
 
     <!--
         Special principals

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_calverify.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_calverify.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_calverify.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -19,10 +19,11 @@
 Tests for calendarserver.tools.calverify
 """
 
+from calendarserver.tools.calverify import BadDataService, \
+    SchedulingMismatchService, DoubleBookingService, DarkPurgeService
+
 from StringIO import StringIO
 from calendarserver.tap.util import getRootResource
-from calendarserver.tools.calverify import BadDataService, \
-    SchedulingMismatchService, DoubleBookingService
 from pycalendar.datetime import PyCalendarDateTime
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
@@ -2525,67 +2526,338 @@
         self.assertEqual(sync_token_oldl1, sync_token_newl1)
 
 
-    def test_instance(self):
-        """
-        CalVerifyService.doScan without fix for mismatches. Make sure it detects
-        as much as it can. Make sure sync-token is not changed.
-        """
 
-        s = """BEGIN:VCALENDAR
+class CalVerifyDarkPurge(CalVerifyMismatchTestsBase):
+    """
+    Tests calverify for events.
+    """
+
+    # No organizer
+    INVITE_NO_ORGANIZER_ICS = """BEGIN:VCALENDAR
 VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
 CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VTIMEZONE
-TZID:America/Los_Angeles
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
 BEGIN:VEVENT
-UID:4760FF93-C7F8-4EB0-B3E8-0B22A96DB1BC
-DTSTART;TZID=America/Los_Angeles:20130221T170000
-DTEND;TZID=America/Los_Angeles:20130221T180000
-ATTENDEE;CN=Casa Blanca APPLE EMP ONLY (12) DA03 4th;CUTYPE=ROOM;PARTSTAT=
- ACCEPTED;ROLE=REQ-PARTICIPANT:urn:uuid:366CC7BE-FEF7-4FFF-B713-6B883538A24
- 9
-ATTENDEE;CN=Mark Chu;CUTYPE=INDIVIDUAL;EMAIL=markchu at apple.com;PARTSTAT=AC
- CEPTED;ROLE=REQ-PARTICIPANT:urn:uuid:46F9D5D9-08E8-4987-9636-CC796F4093C6
-ATTENDEE;CN=Kristie Phan;CUTYPE=INDIVIDUAL;EMAIL=kristie_phan at apple.com;PA
- RTSTAT=ACCEPTED:urn:uuid:97E8720F-4364-DBEC-6721-123E9A92B980
-CREATED:20130220T200530Z
-DTSTAMP:20130222T002246Z
-EXDATE:20130228T010000Z
-EXDATE:20130314T000000Z
-EXDATE:20130321T000000Z
-EXDATE:20130327T000000Z
-EXDATE:20130328T000000Z
-EXDATE:20130403T000000Z
-LOCATION:Casa Blanca APPLE EMP ONLY (12) DA03 4th
-ORGANIZER;CN=Kristie Phan;EMAIL=kristie_phan at apple.com;SCHEDULE-STATUS=1.2
- :urn:uuid:97E8720F-4364-DBEC-6721-123E9A92B980
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;WKST=SU
-SEQUENCE:13
-SUMMARY:ESD Daily Meeting
+CREATED:20100303T181216Z
+UID:INVITE_NO_ORGANIZER_ICS
+TRANSP:OPAQUE
+SUMMARY:INVITE_NO_ORGANIZER_ICS
+DTSTART:%(year)s%(month)02d07T100000Z
+DURATION:PT1H
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
 END:VEVENT
 END:VCALENDAR
-"""
-        from twistedcaldav.ical import Component
-        c = Component.fromString(s)
-        start = PyCalendarDateTime.getToday()
-        start.setDateOnly(False)
-        end = start.duplicate()
-        end.offsetDay(30)
-        config.MaxAllowedInstances = 3000
-        i = c.expandTimeRanges(end, start, ignoreInvalidInstances=True)
-        print(i)
+""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}
+
+    # Valid organizer
+    INVITE_VALID_ORGANIZER_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:INVITE_VALID_ORGANIZER_ICS
+TRANSP:OPAQUE
+SUMMARY:INVITE_VALID_ORGANIZER_ICS
+DTSTART:%(year)s%(month)02d08T100000Z
+DURATION:PT1H
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE:urn:uuid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}
+
+    # Invalid organizer #1
+    INVITE_INVALID_ORGANIZER_1_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:INVITE_INVALID_ORGANIZER_1_ICS
+TRANSP:OPAQUE
+SUMMARY:INVITE_INVALID_ORGANIZER_1_ICS
+DTSTART:%(year)s%(month)02d09T100000Z
+DURATION:PT1H
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0-1
+ATTENDEE:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0-1
+ATTENDEE:urn:uuid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}
+
+    # Invalid organizer #2
+    INVITE_INVALID_ORGANIZER_2_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:INVITE_INVALID_ORGANIZER_2_ICS
+TRANSP:OPAQUE
+SUMMARY:INVITE_INVALID_ORGANIZER_2_ICS
+DTSTART:%(year)s%(month)02d10T100000Z
+DURATION:PT1H
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:mailto:foobar at example.com
+ATTENDEE:mailto:foobar at example.com
+ATTENDEE:urn:uuid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}
+
+    allEvents = {
+        "invite1.ics"      : (INVITE_NO_ORGANIZER_ICS, CalVerifyMismatchTestsBase.metadata,),
+        "invite2.ics"      : (INVITE_VALID_ORGANIZER_ICS, CalVerifyMismatchTestsBase.metadata,),
+        "invite3.ics"      : (INVITE_INVALID_ORGANIZER_1_ICS, CalVerifyMismatchTestsBase.metadata,),
+        "invite4.ics"      : (INVITE_INVALID_ORGANIZER_2_ICS, CalVerifyMismatchTestsBase.metadata,),
+    }
+
+    requirements = {
+        CalVerifyMismatchTestsBase.uuid1 : {
+            "calendar" : {},
+            "inbox" : {},
+        },
+        CalVerifyMismatchTestsBase.uuid2 : {
+            "calendar" : {},
+            "inbox" : {},
+        },
+        CalVerifyMismatchTestsBase.uuid3 : {
+            "calendar" : {},
+            "inbox" : {},
+        },
+        CalVerifyMismatchTestsBase.uuidl1 : {
+            "calendar" : allEvents,
+            "inbox" : {},
+        },
+    }
+
+    @inlineCallbacks
+    def test_scanDarkEvents(self):
+        """
+        CalVerifyService.doScan without fix for dark events. Make sure it detects
+        as much as it can. Make sure sync-token is not changed.
+        """
+
+        sync_token_oldl1 = (yield (yield self.calendarUnderTest(self.uuidl1)).syncToken())
+        self.commit()
+
+        options = {
+            "ical": False,
+            "badcua": False,
+            "mismatch": False,
+            "nobase64": False,
+            "double": True,
+            "dark-purge": False,
+            "fix": False,
+            "verbose": False,
+            "details": False,
+            "summary": False,
+            "days": 365,
+            "uid": "",
+            "uuid": self.uuidl1,
+            "tzid": "utc",
+            "start": PyCalendarDateTime(nowYear, 1, 1, 0, 0, 0),
+            "no-organizer": False,
+            "invalid-organizer": False,
+            "disabled-organizer": False,
+        }
+        output = StringIO()
+        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
+
+        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
+        self.assertEqual(
+            sorted([i.uid for i in calverify.results["Dark Events"]]),
+            ["INVITE_INVALID_ORGANIZER_1_ICS", "INVITE_INVALID_ORGANIZER_2_ICS", ]
+        )
+        self.assertEqual(calverify.results["Number of dark events"], 2)
+        self.assertTrue("Fix dark events" not in calverify.results)
+        self.assertTrue("Fix remove" not in calverify.results)
+
+        sync_token_newl1 = (yield (yield self.calendarUnderTest(self.uuidl1)).syncToken())
+        self.assertEqual(sync_token_oldl1, sync_token_newl1)
+
+
+    @inlineCallbacks
+    def test_fixDarkEvents(self):
+        """
+        CalVerifyService.doScan with fix for dark events. Make sure it detects
+        as much as it can. Make sure sync-token is changed.
+        """
+
+        sync_token_oldl1 = (yield (yield self.calendarUnderTest(self.uuidl1)).syncToken())
+        self.commit()
+
+        options = {
+            "ical": False,
+            "badcua": False,
+            "mismatch": False,
+            "nobase64": False,
+            "double": True,
+            "dark-purge": False,
+            "fix": True,
+            "verbose": False,
+            "details": False,
+            "summary": False,
+            "days": 365,
+            "uid": "",
+            "uuid": self.uuidl1,
+            "tzid": "utc",
+            "start": PyCalendarDateTime(nowYear, 1, 1, 0, 0, 0),
+            "no-organizer": False,
+            "invalid-organizer": False,
+            "disabled-organizer": False,
+        }
+        output = StringIO()
+        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
+
+        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
+        self.assertEqual(
+            sorted([i.uid for i in calverify.results["Dark Events"]]),
+            ["INVITE_INVALID_ORGANIZER_1_ICS", "INVITE_INVALID_ORGANIZER_2_ICS", ]
+        )
+        self.assertEqual(calverify.results["Number of dark events"], 2)
+        self.assertEqual(calverify.results["Fix dark events"], 2)
+        self.assertTrue("Fix remove" in calverify.results)
+
+        sync_token_newl1 = (yield (yield self.calendarUnderTest(self.uuidl1)).syncToken())
+        self.assertNotEqual(sync_token_oldl1, sync_token_newl1)
+
+        # Re-scan after changes to make sure there are no errors
+        self.commit()
+        options["fix"] = False
+        options["uuid"] = self.uuidl1
+        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
+
+        self.assertEqual(calverify.results["Number of events to process"], 2)
+        self.assertEqual(len(calverify.results["Dark Events"]), 0)
+        self.assertTrue("Fix dark events" not in calverify.results)
+        self.assertTrue("Fix remove" not in calverify.results)
+
+
+    @inlineCallbacks
+    def test_fixDarkEventsNoOrganizerOnly(self):
+        """
+        CalVerifyService.doScan with fix for dark events. Make sure it detects
+        as much as it can. Make sure sync-token is changed.
+        """
+
+        sync_token_oldl1 = (yield (yield self.calendarUnderTest(self.uuidl1)).syncToken())
+        self.commit()
+
+        options = {
+            "ical": False,
+            "badcua": False,
+            "mismatch": False,
+            "nobase64": False,
+            "double": True,
+            "dark-purge": False,
+            "fix": True,
+            "verbose": False,
+            "details": False,
+            "summary": False,
+            "days": 365,
+            "uid": "",
+            "uuid": self.uuidl1,
+            "tzid": "utc",
+            "start": PyCalendarDateTime(nowYear, 1, 1, 0, 0, 0),
+            "no-organizer": True,
+            "invalid-organizer": False,
+            "disabled-organizer": False,
+        }
+        output = StringIO()
+        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
+
+        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
+        self.assertEqual(
+            sorted([i.uid for i in calverify.results["Dark Events"]]),
+            ["INVITE_NO_ORGANIZER_ICS", ]
+        )
+        self.assertEqual(calverify.results["Number of dark events"], 1)
+        self.assertEqual(calverify.results["Fix dark events"], 1)
+        self.assertTrue("Fix remove" in calverify.results)
+
+        sync_token_newl1 = (yield (yield self.calendarUnderTest(self.uuidl1)).syncToken())
+        self.assertNotEqual(sync_token_oldl1, sync_token_newl1)
+
+        # Re-scan after changes to make sure there are no errors
+        self.commit()
+        options["fix"] = False
+        options["uuid"] = self.uuidl1
+        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
+
+        self.assertEqual(calverify.results["Number of events to process"], 3)
+        self.assertEqual(len(calverify.results["Dark Events"]), 0)
+        self.assertTrue("Fix dark events" not in calverify.results)
+        self.assertTrue("Fix remove" not in calverify.results)
+
+
+    @inlineCallbacks
+    def test_fixDarkEventsAllTypes(self):
+        """
+        CalVerifyService.doScan with fix for dark events. Make sure it detects
+        as much as it can. Make sure sync-token is changed.
+        """
+
+        sync_token_oldl1 = (yield (yield self.calendarUnderTest(self.uuidl1)).syncToken())
+        self.commit()
+
+        options = {
+            "ical": False,
+            "badcua": False,
+            "mismatch": False,
+            "nobase64": False,
+            "double": True,
+            "dark-purge": False,
+            "fix": True,
+            "verbose": False,
+            "details": False,
+            "summary": False,
+            "days": 365,
+            "uid": "",
+            "uuid": self.uuidl1,
+            "tzid": "utc",
+            "start": PyCalendarDateTime(nowYear, 1, 1, 0, 0, 0),
+            "no-organizer": True,
+            "invalid-organizer": True,
+            "disabled-organizer": True,
+        }
+        output = StringIO()
+        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
+
+        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
+        self.assertEqual(
+            sorted([i.uid for i in calverify.results["Dark Events"]]),
+            ["INVITE_INVALID_ORGANIZER_1_ICS", "INVITE_INVALID_ORGANIZER_2_ICS", "INVITE_NO_ORGANIZER_ICS", ]
+        )
+        self.assertEqual(calverify.results["Number of dark events"], 3)
+        self.assertEqual(calverify.results["Fix dark events"], 3)
+        self.assertTrue("Fix remove" in calverify.results)
+
+        sync_token_newl1 = (yield (yield self.calendarUnderTest(self.uuidl1)).syncToken())
+        self.assertNotEqual(sync_token_oldl1, sync_token_newl1)
+
+        # Re-scan after changes to make sure there are no errors
+        self.commit()
+        options["fix"] = False
+        options["uuid"] = self.uuidl1
+        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
+
+        self.assertEqual(calverify.results["Number of events to process"], 1)
+        self.assertEqual(len(calverify.results["Dark Events"]), 0)
+        self.assertTrue("Fix dark events" not in calverify.results)
+        self.assertTrue("Fix remove" not in calverify.results)

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_config.py (from rev 11028, CalendarServer/trunk/calendarserver/tools/test/test_config.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_config.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_config.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,232 @@
+##
+# Copyright (c) 2013 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.config import ConfigDict
+from calendarserver.tools.config import WritableConfig, setKeyPath, getKeyPath, flattenDictionary
+from calendarserver.tools.test.test_gateway import RunCommandTestCase
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.filepath import FilePath
+from xml.parsers.expat import ExpatError
+import plistlib
+
+PREAMBLE = """<?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">
+"""
+class WritableConfigTestCase(TestCase):
+
+    def setUp(self):
+        self.configFile = self.mktemp()
+        self.fp = FilePath(self.configFile)
+
+
+    def test_readSuccessful(self):
+        content = """<plist version="1.0">
+    <dict>
+        <key>string</key>
+        <string>foo</string>
+    </dict>
+</plist>"""
+        self.fp.setContent(PREAMBLE + content)
+
+        config = ConfigDict()
+        writable = WritableConfig(config, self.configFile)
+        writable.read()
+        self.assertEquals(writable.currentConfigSubset, {"string": "foo"})
+
+
+    def test_readInvalidXML(self):
+        self.fp.setContent("invalid")
+        config = ConfigDict()
+        writable = WritableConfig(config, self.configFile)
+        self.assertRaises(ExpatError, writable.read)
+
+
+    def test_updates(self):
+        content = """<plist version="1.0">
+    <dict>
+        <key>key1</key>
+        <string>before</string>
+        <key>key2</key>
+        <integer>10</integer>
+    </dict>
+</plist>"""
+        self.fp.setContent(PREAMBLE + content)
+        config = ConfigDict()
+        writable = WritableConfig(config, self.configFile)
+        writable.read()
+        writable.set({"key1": "after"})
+        writable.set({"key2": 15})
+        writable.set({"key2": 20}) # override previous set
+        writable.set({"key3": ["a", "b", "c"]})
+        self.assertEquals(writable.currentConfigSubset, {"key1": "after", "key2": 20, "key3": ["a", "b", "c"]})
+        writable.save()
+
+        writable2 = WritableConfig(config, self.configFile)
+        writable2.read()
+        self.assertEquals(writable2.currentConfigSubset, {"key1": "after", "key2": 20, "key3": ["a", "b", "c"]})
+
+
+    def test_convertToValue(self):
+        self.assertEquals(True, WritableConfig.convertToValue("True"))
+        self.assertEquals(False, WritableConfig.convertToValue("False"))
+        self.assertEquals(1, WritableConfig.convertToValue("1"))
+        self.assertEquals(1.2, WritableConfig.convertToValue("1.2"))
+        self.assertEquals("xyzzy", WritableConfig.convertToValue("xyzzy"))
+        self.assertEquals("xy.zzy", WritableConfig.convertToValue("xy.zzy"))
+
+
+
+class ConfigTestCase(RunCommandTestCase):
+
+    @inlineCallbacks
+    def test_readConfig(self):
+        """
+        Verify readConfig returns with only the writable keys
+        """
+        results = yield self.runCommand(command_readConfig,
+            script="calendarserver_config")
+
+        self.assertEquals(results["result"]["RedirectHTTPToHTTPS"], False)
+        self.assertEquals(results["result"]["EnableSearchAddressBook"], False)
+        self.assertEquals(results["result"]["EnableCalDAV"], True)
+        self.assertEquals(results["result"]["EnableCardDAV"], True)
+        self.assertEquals(results["result"]["EnableSSL"], False)
+        self.assertEquals(results["result"]["Notifications"]["Services"]["APNS"]["Enabled"], False)
+        self.assertEquals(results["result"]["Notifications"]["Services"]["APNS"]["CalDAV"]["CertificatePath"], "/example/calendar.cer")
+
+        # Verify not all keys are present, such as ServerRoot which is not writable
+        self.assertFalse("ServerRoot" in results["result"])
+
+
+    @inlineCallbacks
+    def test_writeConfig(self):
+        """
+        Verify writeConfig updates the writable plist file only
+        """
+        results = yield self.runCommand(command_writeConfig,
+            script="calendarserver_config")
+
+        self.assertEquals(results["result"]["EnableCalDAV"], False)
+        self.assertEquals(results["result"]["EnableCardDAV"], False)
+        self.assertEquals(results["result"]["EnableSSL"], True)
+        self.assertEquals(results["result"]["Notifications"]["Services"]["APNS"]["Enabled"], True)
+        self.assertEquals(results["result"]["Notifications"]["Services"]["APNS"]["CalDAV"]["CertificatePath"], "/example/changed.cer")
+        dataRoot = "Data/%s/%s" % (unichr(208), u"\ud83d\udca3")
+        self.assertTrue(results["result"]["DataRoot"].endswith(dataRoot))
+
+        # The static plist should still have EnableCalDAV = True
+        staticPlist = plistlib.readPlist(self.configFileName)
+        self.assertTrue(staticPlist["EnableCalDAV"])
+
+
+    @inlineCallbacks
+    def test_error(self):
+        """
+        Verify sending a bogus command returns an error
+        """
+        results = yield self.runCommand(command_bogusCommand,
+            script="calendarserver_config")
+        self.assertEquals(results["error"], "Unknown command 'bogus'")
+
+
+    def test_keyPath(self):
+        d = ConfigDict()
+        setKeyPath(d, "one", "A")
+        setKeyPath(d, "one", "B")
+        setKeyPath(d, "two.one", "C")
+        setKeyPath(d, "two.one", "D")
+        setKeyPath(d, "two.two", "E")
+        setKeyPath(d, "three.one.one", "F")
+        setKeyPath(d, "three.one.two", "G")
+
+        self.assertEquals(d.one, "B")
+        self.assertEquals(d.two.one, "D")
+        self.assertEquals(d.two.two, "E")
+        self.assertEquals(d.three.one.one, "F")
+        self.assertEquals(d.three.one.two, "G")
+
+        self.assertEquals(getKeyPath(d, "one"), "B")
+        self.assertEquals(getKeyPath(d, "two.one"), "D")
+        self.assertEquals(getKeyPath(d, "two.two"), "E")
+        self.assertEquals(getKeyPath(d, "three.one.one"), "F")
+        self.assertEquals(getKeyPath(d, "three.one.two"), "G")
+
+
+    def test_flattenDictionary(self):
+        dictionary = {
+            "one" : "A",
+            "two" : {
+                "one" : "D",
+                "two" : "E",
+            },
+            "three" : {
+                "one" : {
+                    "one" : "F",
+                    "two" : "G",
+                },
+            },
+        }
+        self.assertEquals(
+            set(list(flattenDictionary(dictionary))),
+            set([("one", "A"), ("three.one.one", "F"), ("three.one.two", "G"), ("two.one", "D"), ("two.two", "E")])
+        )
+
+
+command_readConfig = """<?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>command</key>
+        <string>readConfig</string>
+</dict>
+</plist>
+"""
+
+command_writeConfig = """<?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>command</key>
+        <string>writeConfig</string>
+        <key>Values</key>
+        <dict>
+            <key>EnableCalDAV</key>
+            <false/>
+            <key>EnableCardDAV</key>
+            <false/>
+            <key>EnableSSL</key>
+            <true/>
+            <key>Notifications.Services.APNS.Enabled</key>
+            <true/>
+            <key>Notifications.Services.APNS.CalDAV.CertificatePath</key>
+            <string>/example/changed.cer</string>
+            <key>DataRoot</key>
+            <string>Data/%s/%s</string>
+        </dict>
+</dict>
+</plist>
+""" % (unichr(208), u"\ud83d\udca3")
+
+command_bogusCommand = """<?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>command</key>
+        <string>bogus</string>
+</dict>
+</plist>
+"""

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_gateway.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_gateway.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_gateway.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -29,10 +29,10 @@
 from calendarserver.tools.util import getDirectory
 
 
-class GatewayTestCase(TestCase):
+class RunCommandTestCase(TestCase):
 
     def setUp(self):
-        super(GatewayTestCase, self).setUp()
+        super(RunCommandTestCase, self).setUp()
 
         testRoot = os.path.join(os.path.dirname(__file__), "gateway")
         templateName = os.path.join(testRoot, "caldavd.plist")
@@ -42,6 +42,7 @@
 
         newConfig = template % {
             "ServerRoot" : os.path.abspath(config.ServerRoot),
+            "WritablePlist" : os.path.join(os.path.abspath(config.ConfigRoot), "caldavd-writable.plist"),
         }
         configFilePath = FilePath(os.path.join(config.ConfigRoot, "caldavd.plist"))
         configFilePath.setContent(newConfig)
@@ -70,8 +71,10 @@
         reactor.callLater(0, d.callback, True)
         return d
 
+
     @inlineCallbacks
-    def runCommand(self, command, error=False):
+    def runCommand(self, command, error=False,
+        script="calendarserver_command_gateway"):
         """
         Run the given command by feeding it as standard input to
         calendarserver_command_gateway in a subprocess.
@@ -82,9 +85,9 @@
 
         sourceRoot = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
         python = sys.executable
-        gateway = os.path.join(sourceRoot, "bin", "calendarserver_command_gateway")
+        script = os.path.join(sourceRoot, "bin", script)
 
-        args = [python, gateway, "-f", self.configFileName]
+        args = [python, script, "-f", self.configFileName]
         if error:
             args.append("--error")
 
@@ -101,14 +104,19 @@
 
         returnValue(plist)
 
+
+
+class GatewayTestCase(RunCommandTestCase):
+
     @inlineCallbacks
     def test_getLocationList(self):
         results = yield self.runCommand(command_getLocationList)
         self.assertEquals(len(results["result"]), 10)
 
+
     @inlineCallbacks
     def test_getLocationAttributes(self):
-        results = yield self.runCommand(command_createLocation)
+        yield self.runCommand(command_createLocation)
         results = yield self.runCommand(command_getLocationAttributes)
         self.assertEquals(results["result"]["Building"], "Test Building")
         self.assertEquals(results["result"]["City"], "Cupertino")
@@ -119,34 +127,37 @@
         self.assertEquals(results["result"]["RecordName"], ["createdlocation01"])
         self.assertEquals(results["result"]["State"], "CA")
         self.assertEquals(results["result"]["Street"], "1 Infinite Loop")
-        self.assertEquals(results["result"]["RealName"], "Created Location 01 %s" % unichr(208))
+        self.assertEquals(results["result"]["RealName"],
+            "Created Location 01 %s %s" % (unichr(208), u"\ud83d\udca3"))
         self.assertEquals(results["result"]["Comment"], "Test Comment")
         self.assertEquals(results["result"]["AutoSchedule"], True)
         self.assertEquals(results["result"]["AutoAcceptGroup"], "E5A6142C-4189-4E9E-90B0-9CD0268B314B")
         self.assertEquals(set(results["result"]["ReadProxies"]), set(['user03', 'user04']))
         self.assertEquals(set(results["result"]["WriteProxies"]), set(['user05', 'user06']))
 
+
     @inlineCallbacks
     def test_getResourceList(self):
         results = yield self.runCommand(command_getResourceList)
         self.assertEquals(len(results["result"]), 10)
 
+
     @inlineCallbacks
     def test_getResourceAttributes(self):
-        results = yield self.runCommand(command_createResource)
+        yield self.runCommand(command_createResource)
         results = yield self.runCommand(command_getResourceAttributes)
         self.assertEquals(results["result"]["Comment"], "Test Comment")
         self.assertEquals(results["result"]["Type"], "Computer")
         self.assertEquals(set(results["result"]["ReadProxies"]), set(['user03', 'user04']))
         self.assertEquals(set(results["result"]["WriteProxies"]), set(['user05', 'user06']))
 
+
     @inlineCallbacks
     def test_createLocation(self):
         directory = getDirectory()
 
         record = directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
         self.assertEquals(record, None)
-
         yield self.runCommand(command_createLocation)
 
         directory.flushCaches()
@@ -157,7 +168,8 @@
         augmentService.refresh()
 
         record = directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
-        self.assertEquals(record.fullName.decode("utf-8"), "Created Location 01 %s" % unichr(208))
+        self.assertEquals(record.fullName.decode("utf-8"),
+            "Created Location 01 %s %s" % (unichr(208), u"\ud83d\udca3"))
 
         self.assertNotEquals(record, None)
         self.assertEquals(record.autoSchedule, True)
@@ -177,12 +189,12 @@
         self.assertEquals(set(results["result"]["ReadProxies"]), set(['user03', 'user04']))
         self.assertEquals(set(results["result"]["WriteProxies"]), set(['user05', 'user06']))
 
+
     @inlineCallbacks
     def test_setLocationAttributes(self):
         directory = getDirectory()
 
         yield self.runCommand(command_createLocation)
-        record = directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
         yield self.runCommand(command_setLocationAttributes)
         directory.flushCaches()
 
@@ -226,6 +238,7 @@
         record = directory.recordWithUID("location01")
         self.assertEquals(record, None)
 
+
     @inlineCallbacks
     def test_createResource(self):
         directory = getDirectory()
@@ -239,6 +252,7 @@
         record = directory.recordWithUID("AF575A61-CFA6-49E1-A0F6-B5662C9D9801")
         self.assertNotEquals(record, None)
 
+
     @inlineCallbacks
     def test_setResourceAttributes(self):
         directory = getDirectory()
@@ -254,6 +268,7 @@
         record = directory.recordWithUID("AF575A61-CFA6-49E1-A0F6-B5662C9D9801")
         self.assertEquals(record.fullName, "Updated Laptop 1")
 
+
     @inlineCallbacks
     def test_destroyResource(self):
         directory = getDirectory()
@@ -267,17 +282,20 @@
         record = directory.recordWithUID("resource01")
         self.assertEquals(record, None)
 
+
     @inlineCallbacks
     def test_addWriteProxy(self):
         results = yield self.runCommand(command_addWriteProxy)
         self.assertEquals(len(results["result"]["Proxies"]), 1)
 
+
     @inlineCallbacks
     def test_removeWriteProxy(self):
-        results = yield self.runCommand(command_addWriteProxy)
+        yield self.runCommand(command_addWriteProxy)
         results = yield self.runCommand(command_removeWriteProxy)
         self.assertEquals(len(results["result"]["Proxies"]), 0)
 
+
     @inlineCallbacks
     def test_purgeOldEvents(self):
         results = yield self.runCommand(command_purgeOldEvents)
@@ -287,6 +305,7 @@
         self.assertEquals(results["result"]["RetainDays"], 365)
 
 
+
 command_addReadProxy = """<?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">
@@ -328,7 +347,7 @@
         <key>GeneratedUID</key>
         <string>836B1B66-2E9A-4F46-8B1C-3DD6772C20B2</string>
         <key>RealName</key>
-        <string>Created Location 01 %s</string>
+        <string>Created Location 01 %s %s</string>
         <key>RecordName</key>
         <array>
                 <string>createdlocation01</string>
@@ -367,7 +386,7 @@
         </array>
 </dict>
 </plist>
-""" % unichr(208)
+""" % (unichr(208), u"\ud83d\udca3")
 
 
 command_createResource = """<?xml version="1.0" encoding="UTF-8"?>

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_principals.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_principals.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/test/test_principals.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -15,7 +15,6 @@
 ##
 
 import os
-import signal
 import sys
 
 from twext.python.filepath import CachingFilePath as FilePath
@@ -26,13 +25,12 @@
 from twistedcaldav.directory.directory import DirectoryError
 from twistedcaldav.directory import calendaruserproxy
 
-from twistedcaldav.test.util import TestCase, CapturingProcessProtocol,\
+from twistedcaldav.test.util import TestCase, CapturingProcessProtocol, \
     ErrorOutput
 
 from calendarserver.tap.util import directoryFromConfig
 from calendarserver.tools.principals import (parseCreationArgs, matchStrings,
-    updateRecord, principalForPrincipalID, getProxies, setProxies,
-    triggerGroupCacherUpdate)
+    updateRecord, principalForPrincipalID, getProxies, setProxies)
 
 
 class ManagePrincipalsTestCase(TestCase):
@@ -53,6 +51,9 @@
 
         newConfig = template % {
             "ServerRoot" : os.path.abspath(config.ServerRoot),
+            "DataRoot" : os.path.abspath(config.DataRoot),
+            "DocumentRoot" : os.path.abspath(config.DocumentRoot),
+            "LogRoot" : os.path.abspath(config.LogRoot),
         }
         configFilePath = FilePath(os.path.join(config.ConfigRoot, "caldavd.plist"))
         configFilePath.setContent(newConfig)
@@ -100,11 +101,13 @@
         output = yield deferred
         returnValue(output)
 
+
     @inlineCallbacks
     def test_help(self):
         results = yield self.runCommand("--help")
         self.assertTrue(results.startswith("usage:"))
 
+
     @inlineCallbacks
     def test_principalTypes(self):
         results = yield self.runCommand("--list-principal-types")
@@ -113,12 +116,14 @@
         self.assertTrue("locations" in results)
         self.assertTrue("resources" in results)
 
+
     @inlineCallbacks
     def test_listPrincipals(self):
         results = yield self.runCommand("--list-principals=users")
         for i in xrange(1, 10):
             self.assertTrue("user%02d" % (i,) in results)
 
+
     @inlineCallbacks
     def test_search(self):
         results = yield self.runCommand("--search=user")
@@ -126,6 +131,7 @@
         for i in xrange(1, 10):
             self.assertTrue("user%02d" % (i,) in results)
 
+
     @inlineCallbacks
     def test_addRemove(self):
         results = yield self.runCommand("--add", "resources", "New Resource",
@@ -157,6 +163,7 @@
         results = yield self.runCommand("--list-principals=resources")
         self.assertFalse("newresource" in results)
 
+
     def test_parseCreationArgs(self):
 
         self.assertEquals(("full name", None, None),
@@ -181,6 +188,7 @@
             parseCreationArgs, ("full name", "non guid", "non guid")
         )
 
+
     def test_matchStrings(self):
         self.assertEquals("abc", matchStrings("a", ("abc", "def")))
         self.assertEquals("def", matchStrings("de", ("abc", "def")))
@@ -189,6 +197,7 @@
             matchStrings, "foo", ("abc", "def")
         )
 
+
     @inlineCallbacks
     def test_modifyWriteProxies(self):
         results = yield self.runCommand("--add-write-proxy=users:user01",
@@ -206,6 +215,7 @@
             "locations:location01")
         self.assertTrue('No write proxies for "Room 01" (locations:location01)' in results)
 
+
     @inlineCallbacks
     def test_modifyReadProxies(self):
         results = yield self.runCommand("--add-read-proxy=users:user01",
@@ -276,11 +286,11 @@
         guid = "EEE28807-A8C5-46C8-A558-A08281C558A7"
 
         (yield updateRecord(True, directory, "locations",
-            guid=guid, fullName="Test Location", shortNames=["testlocation",],)
+            guid=guid, fullName="Test Location", shortNames=["testlocation", ],)
         )
         try:
             (yield updateRecord(True, directory, "locations",
-                guid=guid, fullName="Test Location", shortNames=["testlocation",],)
+                guid=guid, fullName="Test Location", shortNames=["testlocation", ],)
             )
         except DirectoryError:
             # We're expecting an error for trying to create a record with
@@ -295,7 +305,7 @@
         self.assertTrue(record.autoSchedule)
 
         (yield updateRecord(False, directory, "locations",
-            guid=guid, fullName="Changed", shortNames=["testlocation",],)
+            guid=guid, fullName="Changed", shortNames=["testlocation", ],)
         )
         record = directory.recordWithGUID(guid)
         self.assertTrue(record is not None)
@@ -305,11 +315,10 @@
         record = directory.recordWithGUID(guid)
         self.assertTrue(record is None)
 
-
         # Create a user, change autoSchedule
         guid = "F0DE73A8-39D4-4830-8D32-1FA03ABA3470"
         (yield updateRecord(True, directory, "users",
-            guid=guid, fullName="Test User", shortNames=["testuser",],
+            guid=guid, fullName="Test User", shortNames=["testuser", ],
             autoSchedule=True)
         )
         record = directory.recordWithGUID(guid)
@@ -318,7 +327,7 @@
         self.assertTrue(record.autoSchedule)
 
         (yield updateRecord(False, directory, "users",
-            guid=guid, fullName="Test User", shortNames=["testuser",],
+            guid=guid, fullName="Test User", shortNames=["testuser", ],
             autoSchedule=False)
         )
         record = directory.recordWithGUID(guid)
@@ -339,36 +348,13 @@
         self.assertEquals(readProxies, []) # initially empty
         self.assertEquals(writeProxies, []) # initially empty
 
-        (yield setProxies(principal, ["users:user03", "users:user04"], ["users:user05"], directory=directory))
+        (yield setProxies(None, principal, ["users:user03", "users:user04"], ["users:user05"], directory=directory))
         readProxies, writeProxies = (yield getProxies(principal, directory=directory))
         self.assertEquals(set(readProxies), set(["user03", "user04"]))
         self.assertEquals(set(writeProxies), set(["user05"]))
 
         # Using None for a proxy list indicates a no-op
-        (yield setProxies(principal, [], None, directory=directory))
+        (yield setProxies(None, principal, [], None, directory=directory))
         readProxies, writeProxies = (yield getProxies(principal, directory=directory))
         self.assertEquals(readProxies, []) # now empty
         self.assertEquals(set(writeProxies), set(["user05"])) # unchanged
-
-
-    def test_triggerGroupCacherUpdate(self):
-        """
-        Verify triggerGroupCacherUpdate can read a pidfile and send a SIGHUP
-        """
-
-        self.calledArgs = None
-        def killMethod(pid, sig):
-            self.calledArgs = (pid, sig)
-
-        class StubConfig(object):
-            def __init__(self, runRootPath):
-                self.RunRoot = runRootPath
-
-        runRootDir = FilePath(self.mktemp())
-        runRootDir.createDirectory()
-        pidFile = runRootDir.child("groupcacher.pid")
-        pidFile.setContent("1234")
-        testConfig = StubConfig(runRootDir.path)
-        triggerGroupCacherUpdate(testConfig, killMethod=killMethod)
-        self.assertEquals(self.calledArgs, (1234, signal.SIGHUP))
-        runRootDir.remove()

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/upgrade.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/upgrade.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/upgrade.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -15,12 +15,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from __future__ import print_function
 
 """
 This tool allows any necessary upgrade to complete, then exits.
 """
 
+from __future__ import print_function
 import os
 import sys
 import time

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/util.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/tools/util.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -31,21 +31,27 @@
 import socket
 from pwd import getpwnam
 from grp import getgrnam
+from uuid import UUID
 
+from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+
+
 from twisted.python.filepath import FilePath
 from twisted.python.reflect import namedClass
 from twext.python.log import Logger
+from twisted.internet.defer import inlineCallbacks, returnValue
 
+from txdav.xml import element as davxml
 
 from calendarserver.provision.root import RootResource
 
 from twistedcaldav import memcachepool
-from twistedcaldav.config import config, ConfigurationError
 from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.directory.directory import scheduleNextGroupCachingUpdate
 from calendarserver.push.notifier import NotifierFactory
-from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 
 from txdav.common.datastore.file import CommonDataStore
 
@@ -70,6 +76,8 @@
 
     return config
 
+
+
 def getDirectory(config=config):
 
     class MyDirectoryService (AggregateDirectoryService):
@@ -87,7 +95,7 @@
                     notifierFactory = None
 
                 # Need a data store
-                _newStore = CommonDataStore(FilePath(config.DocumentRoot), 
+                _newStore = CommonDataStore(FilePath(config.DocumentRoot),
                     notifierFactory, True, False)
                 if notifierFactory is not None:
                     notifierFactory.store = _newStore
@@ -130,6 +138,8 @@
         def principalForCalendarUserAddress(self, cua):
             return self.principalCollection.principalForCalendarUserAddress(cua)
 
+        def principalForUID(self, uid):
+            return self.principalCollection.principalForUID(uid)
 
     # Load augment/proxy db classes now
     if config.AugmentService.type:
@@ -148,7 +158,6 @@
     while not directory.isAvailable():
         sleep(5)
 
-
     directories = [directory]
 
     if config.ResourceService.Enabled:
@@ -183,34 +192,47 @@
 
     return aggregate
 
+
+
 class DummyDirectoryService (DirectoryService):
     realmName = ""
     baseGUID = "51856FD4-5023-4890-94FE-4356C4AAC3E4"
-    def recordTypes(self): return ()
-    def listRecords(self): return ()
-    def recordWithShortName(self): return None
+    def recordTypes(self):
+        return ()
 
+
+    def listRecords(self):
+        return ()
+
+
+    def recordWithShortName(self):
+        return None
+
 dummyDirectoryRecord = DirectoryRecord(
-    service = DummyDirectoryService(),
-    recordType = "dummy",
-    guid = "8EF0892F-7CB6-4B8E-B294-7C5A5321136A",
-    shortNames = ("dummy",),
-    fullName = "Dummy McDummerson",
-    firstName = "Dummy",
-    lastName = "McDummerson",
+    service=DummyDirectoryService(),
+    recordType="dummy",
+    guid="8EF0892F-7CB6-4B8E-B294-7C5A5321136A",
+    shortNames=("dummy",),
+    fullName="Dummy McDummerson",
+    firstName="Dummy",
+    lastName="McDummerson",
 )
 
 class UsageError (StandardError):
     pass
 
+
+
 def booleanArgument(arg):
-    if   arg in ("true",  "yes", "yup",  "uh-huh", "1", "t", "y"):
+    if   arg in ("true", "yes", "yup", "uh-huh", "1", "t", "y"):
         return True
-    elif arg in ("false", "no",  "nope", "nuh-uh", "0", "f", "n"):
+    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
@@ -229,6 +251,7 @@
         config.Memcached.Pools.Default.ClientEnabled = False
 
 
+
 def setupMemcached(config):
     #
     # Connect to memcached
@@ -240,6 +263,7 @@
     autoDisableMemcached(config)
 
 
+
 def checkDirectory(dirpath, description, access=None, create=None, wait=False):
     """
     Make sure dirpath is an existing directory, and optionally ensure it has the
@@ -312,3 +336,186 @@
 
 
 
+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("/"):
+        segments = principalID.strip("/").split("/")
+        if (len(segments) == 3 and
+            segments[0] == "principals" and segments[1] == "__uids__"):
+            uid = segments[2]
+        else:
+            raise ValueError("Can't resolve all paths yet")
+
+        if checkOnly:
+            return None
+
+        return directory.principalCollection.principalForUID(uid)
+
+    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:
+        UUID(principalID)
+
+        if checkOnly:
+            return None
+
+        x = directory.principalCollection.principalForUID(principalID)
+        return x
+    except ValueError:
+        pass
+
+    raise ValueError("Invalid principal identifier: %s" % (principalID,))
+
+
+
+def proxySubprincipal(principal, proxyType):
+    return principal.getChild("calendar-proxy-" + proxyType)
+
+
+
+ at inlineCallbacks
+def action_addProxyPrincipal(rootResource, directory, store, principal, proxyType, proxyPrincipal):
+    try:
+        (yield addProxy(rootResource, directory, store, principal, proxyType, proxyPrincipal))
+        print("Added %s as a %s proxy for %s" % (
+            prettyPrincipal(proxyPrincipal), proxyType,
+            prettyPrincipal(principal)))
+    except ProxyError, e:
+        print("Error:", e)
+    except ProxyWarning, e:
+        print(e)
+
+
+
+ at inlineCallbacks
+def action_removeProxyPrincipal(rootResource, directory, store, principal, proxyPrincipal, **kwargs):
+    try:
+        removed = (yield removeProxy(rootResource, directory, store,
+            principal, proxyPrincipal, **kwargs))
+        if removed:
+            print("Removed %s as a proxy for %s" % (
+                prettyPrincipal(proxyPrincipal),
+                prettyPrincipal(principal)))
+    except ProxyError, e:
+        print("Error:", e)
+    except ProxyWarning, e:
+        print(e)
+
+
+
+ at inlineCallbacks
+def addProxy(rootResource, directory, store, principal, proxyType, proxyPrincipal):
+    proxyURL = proxyPrincipal.url()
+
+    subPrincipal = proxySubprincipal(principal, proxyType)
+    if subPrincipal is None:
+        raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
+            prettyPrincipal(principal)))
+
+    membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+
+    for memberURL in membersProperty.children:
+        if str(memberURL) == proxyURL:
+            raise ProxyWarning("%s is already a %s proxy for %s" % (
+                prettyPrincipal(proxyPrincipal), proxyType,
+                prettyPrincipal(principal)))
+
+    else:
+        memberURLs = list(membersProperty.children)
+        memberURLs.append(davxml.HRef(proxyURL))
+        membersProperty = davxml.GroupMemberSet(*memberURLs)
+        (yield subPrincipal.writeProperty(membersProperty, None))
+
+    proxyTypes = ["read", "write"]
+    proxyTypes.remove(proxyType)
+
+    (yield action_removeProxyPrincipal(rootResource, directory, store,
+        principal, proxyPrincipal, proxyTypes=proxyTypes))
+
+    yield scheduleNextGroupCachingUpdate(store, 0)
+
+
+
+ at inlineCallbacks
+def removeProxy(rootResource, directory, store, principal, proxyPrincipal, **kwargs):
+    removed = False
+    proxyTypes = kwargs.get("proxyTypes", ("read", "write"))
+    for proxyType in proxyTypes:
+        proxyURL = proxyPrincipal.url()
+
+        subPrincipal = proxySubprincipal(principal, proxyType)
+        if subPrincipal is None:
+            raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
+                prettyPrincipal(principal)))
+
+        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
+        else:
+            removed = True
+
+        membersProperty = davxml.GroupMemberSet(*memberURLs)
+        (yield subPrincipal.writeProperty(membersProperty, None))
+
+    if removed:
+        yield scheduleNextGroupCachingUpdate(store, 0)
+    returnValue(removed)
+
+
+
+def prettyPrincipal(principal):
+    record = principal.record
+    return "\"%s\" (%s:%s)" % (record.fullName, record.recordType,
+        record.shortNames[0])
+
+
+
+class ProxyError(Exception):
+    """
+    Raised when proxy assignments cannot be performed
+    """
+
+
+
+class ProxyWarning(Exception):
+    """
+    Raised for harmless proxy assignment failures such as trying to add a
+    duplicate or remove a non-existent assignment.
+    """

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/webadmin/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/webadmin/resource.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/webadmin/resource.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -28,7 +28,7 @@
 import operator
 import urlparse
 
-from calendarserver.tools.principals import (
+from calendarserver.tools.util import (
     principalForPrincipalID, proxySubprincipal, action_addProxyPrincipal,
     action_removeProxyPrincipal
 )
@@ -107,7 +107,6 @@
         else:
             returnValue(tag)
 
-
     _searchResults = None
 
     @inlineCallbacks
@@ -158,6 +157,7 @@
             return ""
 
 
+
 def searchToSlots(results, tag):
     """
     Convert the result of doing a search to an iterable of tags.
@@ -370,7 +370,6 @@
             tag(selected='selected')
         return tag
 
-
     _matrix = None
 
     @inlineCallbacks
@@ -491,7 +490,6 @@
             result.append(ProxyRow(tag.clone(), idx, readProxy, writeProxy))
         returnValue(result)
 
-
     _proxySearchResults = None
 
     def performProxySearch(self):
@@ -569,39 +567,49 @@
     Web administration HTTP resource.
     """
 
-    def __init__(self, path, root, directory, principalCollections=()):
+    def __init__(self, path, root, directory, store, principalCollections=()):
         self.root = root
         self.directory = directory
+        self.store = store
         super(WebAdminResource, self).__init__(path,
             principalCollections=principalCollections)
 
+
     # Only allow administrators to access
     def defaultAccessControlList(self):
         return davxml.ACL(*config.AdminACEs)
 
+
     def etag(self):
         # Can't be calculated here
         return succeed(None)
 
+
     def contentLength(self):
         # Can't be calculated here
         return None
 
+
     def lastModified(self):
         return None
 
+
     def exists(self):
         return True
 
+
     def displayName(self):
         return "Web Admin"
 
+
     def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8");
+        return MimeType.fromString("text/html; charset=utf-8")
 
+
     def contentEncoding(self):
         return None
 
+
     def createSimilarFile(self, path):
         return DAVFile(path, principalCollections=self.principalCollections())
 
@@ -632,7 +640,7 @@
         # Update the auto-schedule value if specified.
         if autoSchedule is not None and (autoSchedule == "true" or
                                          autoSchedule == "false"):
-            if ( principal.record.recordType != "users" and
+            if (principal.record.recordType != "users" and
                  principal.record.recordType != "groups" or
                  principal.record.recordType == "users" and
                  config.Scheduling.Options.AutoSchedule.AllowUsers):
@@ -642,16 +650,18 @@
         # Update the proxies if specified.
         for proxyId in removeProxies:
             proxy = self.getResourceById(request, proxyId)
-            (yield action_removeProxyPrincipal(principal, proxy,
-                                               proxyTypes=["read", "write"]))
+            (yield action_removeProxyPrincipal(self.root, self.directory, self.store,
+                principal, proxy, proxyTypes=["read", "write"]))
 
         for proxyId in makeReadProxies:
             proxy = self.getResourceById(request, proxyId)
-            (yield action_addProxyPrincipal(principal, "read", proxy))
+            (yield action_addProxyPrincipal(self.root, self.directory, self.store,
+                principal, "read", proxy))
 
         for proxyId in makeWriteProxies:
             proxy = self.getResourceById(request, proxyId)
-            (yield action_addProxyPrincipal(principal, "write", proxy))
+            (yield action_addProxyPrincipal(self.root, self.directory, self.store,
+                principal, "write", proxy))
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/webadmin/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/webadmin/test/test_resource.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/calendarserver/webadmin/test/test_resource.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -22,7 +22,7 @@
 
 from functools import partial
 
-from twisted.trial.unittest import TestCase
+from twistedcaldav.test.util import TestCase
 
 from twisted.web.microdom import parseString, getElementsByTagName
 from twisted.web.domhelpers import gatherTextNodes
@@ -66,7 +66,7 @@
 
     def setUp(self):
         self.expectedSearches = {}
-        self.resource = WebAdminResource(self.mktemp(), None, self)
+        self.resource = WebAdminResource(self.mktemp(), None, self, None)
 
 
     @inlineCallbacks
@@ -227,7 +227,7 @@
     @inlineCallbacks
     def test_selectResourceById(self):
         """
-        When a resource is selected by a 'resourceId' parameter, 
+        When a resource is selected by a 'resourceId' parameter,
         """
         self.resource.getResourceById = partial(FakePrincipalResource, self)
         document = yield self.renderPage(dict(resourceId=["qux"]))
@@ -335,9 +335,9 @@
         When rendering a resource, an "Auto-Schedule Mode" menu with various options
         should be displayed.
         """
-        
+
         modes = ("default", "none", "accept-always", "decline-always", "accept-if-free", "decline-if-busy", "automatic",)
-    
+
         for ctr, expectValue in enumerate(modes):
 
             self.resource.getResourceById = partial(FakePrincipalResource, self,
@@ -421,7 +421,6 @@
         self.assertIn(cgi.escape(propertyValue),
                       gatherTextNodes(document))
 
-
     # Properties for being a fake directory service as far as the implementation
     # of DirectoryRecord is concerned.
     realmName = 'Fake'
@@ -500,5 +499,3 @@
 
     def getAutoScheduleMode(self):
         return self.autoschedmode
-
-

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/conf/auth/augments-test.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/conf/auth/augments-test.xml	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/conf/auth/augments-test.xml	2013-04-11 16:24:18 UTC (rev 11029)
@@ -1,21 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
-<!--
-Copyright (c) 2009-2013 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>
@@ -111,4 +94,46 @@
     <enable-addressbook>false</enable-addressbook>
     <auto-schedule>false</auto-schedule>
   </record>
+  <record>
+    <uid>03DFF660-8BCC-4198-8588-DD77F776F518</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <uid>80689D41-DAF8-4189-909C-DB017B271892</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <uid>C38BEE7A-36EE-478C-9DCB-CBF4612AFE65</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+    <auto-accept-group>group01</auto-accept-group>
+  </record>
+  <record>
+    <uid>CCE95217-A57B-481A-AC3D-FEC9AB6CE3A9</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <uid>0CE0BF31-5F9E-4801-A489-8C70CF287F5F</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
 </augments>

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/conf/auth/resources-test.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/conf/auth/resources-test.xml	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/conf/auth/resources-test.xml	2013-04-11 16:24:18 UTC (rev 11029)
@@ -1,94 +1,227 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-Copyright (c) 2006-2013 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 "accounts.dtd">
-
 <accounts realm="Test Realm">
-  <location repeat="10">
-    <uid>location%02d</uid>
-    <guid>location%02d</guid>
-    <password>location%02d</password>
-    <name>Room %02d</name>
+  <location>
+    <uid>jupiter</uid>
+    <guid>jupiter</guid>
+    <name>Jupiter Conference Room, Building 2, 1st Floor</name>
   </location>
-  <resource repeat="20">
-    <uid>resource%02d</uid>
-    <guid>resource%02d</guid>
-    <password>resource%02d</password>
-    <name>Resource %02d</name>
-  </resource>
   <location>
+    <uid>uranus</uid>
+    <guid>uranus</guid>
+    <name>Uranus Conference Room, Building 3, 1st Floor</name>
+  </location>
+  <location>
+    <uid>morgensroom</uid>
+    <guid>03DFF660-8BCC-4198-8588-DD77F776F518</guid>
+    <name>Morgen's Room</name>
+  </location>
+  <location>
     <uid>mercury</uid>
     <guid>mercury</guid>
-    <password>test</password>
-    <name>Mecury Conference Room, Building 1, 2nd Floor</name>
+    <name>Mercury Conference Room, Building 1, 2nd Floor</name>
   </location>
   <location>
-    <uid>venus</uid>
-    <guid>venus</guid>
-    <password>test</password>
-    <name>Venus Conference Room, Building 1, 2nd Floor</name>
+    <uid>location09</uid>
+    <guid>location09</guid>
+    <name>Room 09</name>
   </location>
   <location>
-    <uid>Earth</uid>
-    <guid>Earth</guid>
-    <password>test</password>
-    <name>Earth Conference Room, Building 1, 1st Floor</name>
+    <uid>location08</uid>
+    <guid>location08</guid>
+    <name>Room 08</name>
   </location>
   <location>
+    <uid>location07</uid>
+    <guid>location07</guid>
+    <name>Room 07</name>
+  </location>
+  <location>
+    <uid>location06</uid>
+    <guid>location06</guid>
+    <name>Room 06</name>
+  </location>
+  <location>
+    <uid>location05</uid>
+    <guid>location05</guid>
+    <name>Room 05</name>
+  </location>
+  <location>
+    <uid>location04</uid>
+    <guid>location04</guid>
+    <name>Room 04</name>
+  </location>
+  <location>
+    <uid>location03</uid>
+    <guid>location03</guid>
+    <name>Room 03</name>
+  </location>
+  <location>
+    <uid>location02</uid>
+    <guid>location02</guid>
+    <name>Room 02</name>
+  </location>
+  <location>
+    <uid>location01</uid>
+    <guid>location01</guid>
+    <name>Room 01</name>
+  </location>
+  <location>
+    <uid>delegatedroom</uid>
+    <guid>delegatedroom</guid>
+    <name>Delegated Conference Room</name>
+  </location>
+  <location>
     <uid>mars</uid>
     <guid>redplanet</guid>
-    <password>test</password>
     <name>Mars Conference Room, Building 1, 1st Floor</name>
   </location>
   <location>
-    <uid>jupiter</uid>
-    <guid>jupiter</guid>
-    <password>test</password>
-    <name>Jupiter Conference Room, Building 2, 1st Floor</name>
+    <uid>sharissroom</uid>
+    <guid>80689D41-DAF8-4189-909C-DB017B271892</guid>
+    <name>Shari's Room</name>
   </location>
   <location>
-    <uid>neptune</uid>
-    <guid>neptune</guid>
-    <password>test</password>
-    <name>Neptune Conference Room, Building 2, 1st Floor</name>
-  </location>
-  <location>
     <uid>pluto</uid>
     <guid>pluto</guid>
-    <password>test</password>
     <name>Pluto Conference Room, Building 2, 1st Floor</name>
   </location>
   <location>
     <uid>saturn</uid>
     <guid>saturn</guid>
-    <password>test</password>
     <name>Saturn Conference Room, Building 2, 1st Floor</name>
   </location>
   <location>
-    <uid>uranus</uid>
-    <guid>uranus</guid>
-    <password>test</password>
-    <name>Uranus Conference Room, Building 3, 1st Floor</name>
+    <uid>location10</uid>
+    <guid>location10</guid>
+    <name>Room 10</name>
   </location>
   <location>
-    <uid>delegatedroom</uid>
-    <guid>delegatedroom</guid>
-    <password>delegatedroom</password>
-    <name>Delegated Conference Room</name>
+    <uid>neptune</uid>
+    <guid>neptune</guid>
+    <name>Neptune Conference Room, Building 2, 1st Floor</name>
   </location>
+  <location>
+    <uid>Earth</uid>
+    <guid>Earth</guid>
+    <name>Earth Conference Room, Building 1, 1st Floor</name>
+  </location>
+  <location>
+    <uid>venus</uid>
+    <guid>venus</guid>
+    <name>Venus Conference Room, Building 1, 2nd Floor</name>
+  </location>
+  <resource>
+    <uid>sharisotherresource</uid>
+    <guid>CCE95217-A57B-481A-AC3D-FEC9AB6CE3A9</guid>
+    <name>Shari's Other Resource</name>
+  </resource>
+  <resource>
+    <uid>resource15</uid>
+    <guid>resource15</guid>
+    <name>Resource 15</name>
+  </resource>
+  <resource>
+    <uid>resource14</uid>
+    <guid>resource14</guid>
+    <name>Resource 14</name>
+  </resource>
+  <resource>
+    <uid>resource17</uid>
+    <guid>resource17</guid>
+    <name>Resource 17</name>
+  </resource>
+  <resource>
+    <uid>resource16</uid>
+    <guid>resource16</guid>
+    <name>Resource 16</name>
+  </resource>
+  <resource>
+    <uid>resource11</uid>
+    <guid>resource11</guid>
+    <name>Resource 11</name>
+  </resource>
+  <resource>
+    <uid>resource10</uid>
+    <guid>resource10</guid>
+    <name>Resource 10</name>
+  </resource>
+  <resource>
+    <uid>resource13</uid>
+    <guid>resource13</guid>
+    <name>Resource 13</name>
+  </resource>
+  <resource>
+    <uid>resource12</uid>
+    <guid>resource12</guid>
+    <name>Resource 12</name>
+  </resource>
+  <resource>
+    <uid>resource19</uid>
+    <guid>resource19</guid>
+    <name>Resource 19</name>
+  </resource>
+  <resource>
+    <uid>resource18</uid>
+    <guid>resource18</guid>
+    <name>Resource 18</name>
+  </resource>
+  <resource>
+    <uid>sharisresource</uid>
+    <guid>C38BEE7A-36EE-478C-9DCB-CBF4612AFE65</guid>
+    <name>Shari's Resource</name>
+  </resource>
+  <resource>
+    <uid>resource20</uid>
+    <guid>resource20</guid>
+    <name>Resource 20</name>
+  </resource>
+  <resource>
+    <uid>resource06</uid>
+    <guid>resource06</guid>
+    <name>Resource 06</name>
+  </resource>
+  <resource>
+    <uid>resource07</uid>
+    <guid>resource07</guid>
+    <name>Resource 07</name>
+  </resource>
+  <resource>
+    <uid>resource04</uid>
+    <guid>resource04</guid>
+    <name>Resource 04</name>
+  </resource>
+  <resource>
+    <uid>resource05</uid>
+    <guid>resource05</guid>
+    <name>Resource 05</name>
+  </resource>
+  <resource>
+    <uid>resource02</uid>
+    <guid>resource02</guid>
+    <name>Resource 02</name>
+  </resource>
+  <resource>
+    <uid>resource03</uid>
+    <guid>resource03</guid>
+    <name>Resource 03</name>
+  </resource>
+  <resource>
+    <uid>resource01</uid>
+    <guid>resource01</guid>
+    <name>Resource 01</name>
+  </resource>
+  <resource>
+    <uid>sharisotherresource1</uid>
+    <guid>0CE0BF31-5F9E-4801-A489-8C70CF287F5F</guid>
+    <name>Shari's Other Resource1</name>
+  </resource>
+  <resource>
+    <uid>resource08</uid>
+    <guid>resource08</guid>
+    <name>Resource 08</name>
+  </resource>
+  <resource>
+    <uid>resource09</uid>
+    <guid>resource09</guid>
+    <name>Resource 09</name>
+  </resource>
 </accounts>


Property changes on: CalendarServer/branches/users/cdaboo/store-scheduling/conf/auth/resources-test.xml
___________________________________________________________________
Added: svn:executable
   + *

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/conf/caldavd-apple.plist	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/conf/caldavd-apple.plist	2013-04-11 16:24:18 UTC (rev 11029)
@@ -105,6 +105,13 @@
     <dict>
         <key>Ctl</key>
         <string>xpg_ctl</string>
+        <key>Options</key>
+        <array>
+        <!-- <string>-c log_statement=all</string> -->
+            <string>-c log_lock_waits=TRUE</string>
+            <string>-c deadlock_timeout=10</string>
+            <string>-c log_line_prefix='%m [%p] '</string>
+        </array>
     </dict>
 
     <!-- Data root -->
@@ -522,5 +529,13 @@
     <key>OpenDirectoryModule</key>
     <string>calendarserver.platform.darwin.od.opendirectory</string>
 
+    <key>Includes</key>
+    <array>
+        <string>/Library/Server/Calendar and Contacts/Config/caldavd-system.plist</string>
+        <string>/Library/Server/Calendar and Contacts/Config/caldavd-user.plist</string>
+    </array>
+    <key>WritableConfigFile</key>
+    <string>/Library/Server/Calendar and Contacts/Config/caldavd-system.plist</string>
+
   </dict>
 </plist>

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/conf/caldavd-test.plist	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/conf/caldavd-test.plist	2013-04-11 16:24:18 UTC (rev 11029)
@@ -772,8 +772,6 @@
         <false/>
         <key>AttendeeRefreshBatch</key>
         <integer>0</integer>
-        <key>V1Compatibility</key> <!-- Allow /path-based CUAs in scheduling replies -->
-        <false/>
 
 		<key>AutoSchedule</key>
 		<dict>

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/contrib/performance/loadtest/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/contrib/performance/loadtest/ical.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/contrib/performance/loadtest/ical.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -1186,7 +1186,7 @@
         self._pushFactories.append(factory)
         connect(GAIEndpoint(self.reactor, host, port), factory)
 
-    def _receivedPush(self, inboundID):
+    def _receivedPush(self, inboundID, dataChangedTimestamp):
         for href, id in self.ampPushKeys.iteritems():
             if inboundID == id:
                 self._checkCalendarsForEvents(href, push=True)

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/RFC6764-srv-CalDAV.txt (from rev 11028, CalendarServer/trunk/doc/RFC/RFC6764-srv-CalDAV.txt)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/RFC6764-srv-CalDAV.txt	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/RFC6764-srv-CalDAV.txt	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,787 @@
+
+
+
+
+
+
+Internet Engineering Task Force (IETF)                          C. Daboo
+Request for Comments: 6764                                    Apple Inc.
+Updates: 4791, 6352                                        February 2013
+Category: Standards Track
+ISSN: 2070-1721
+
+
+            Locating Services for Calendaring Extensions to
+        WebDAV (CalDAV) and vCard Extensions to WebDAV (CardDAV)
+
+Abstract
+
+   This specification describes how DNS SRV records, DNS TXT records,
+   and well-known URIs can be used together or separately to locate
+   CalDAV (Calendaring Extensions to Web Distributed Authoring and
+   Versioning (WebDAV)) or CardDAV (vCard Extensions to WebDAV)
+   services.
+
+Status of This Memo
+
+   This is an Internet Standards Track document.
+
+   This document is a product of the Internet Engineering Task Force
+   (IETF).  It represents the consensus of the IETF community.  It has
+   received public review and has been approved for publication by the
+   Internet Engineering Steering Group (IESG).  Further information on
+   Internet Standards is available in Section 2 of RFC 5741.
+
+   Information about the current status of this document, any errata,
+   and how to provide feedback on it may be obtained at
+   http://www.rfc-editor.org/info/rfc6764.
+
+Copyright Notice
+
+   Copyright (c) 2013 IETF Trust and the persons identified as the
+   document authors.  All rights reserved.
+
+   This document is subject to BCP 78 and the IETF Trust's Legal
+   Provisions Relating to IETF Documents
+   (http://trustee.ietf.org/license-info) in effect on the date of
+   publication of this document.  Please review these documents
+   carefully, as they describe your rights and restrictions with respect
+   to this document.  Code Components extracted from this document must
+   include Simplified BSD License text as described in Section 4.e of
+   the Trust Legal Provisions and are provided without warranty as
+   described in the Simplified BSD License.
+
+
+
+
+
+Daboo                        Standards Track                    [Page 1]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+Table of Contents
+
+   1. Introduction ....................................................2
+   2. Conventions Used in This Document ...............................3
+   3. CalDAV SRV Service Labels .......................................3
+   4. CalDAV and CardDAV Service TXT Records ..........................4
+   5. CalDAV and CardDAV Service Well-Known URI .......................4
+      5.1. Example: Well-Known URI Redirects to Actual
+           "Context Path" .............................................5
+   6. Client "Bootstrapping" Procedures ...............................5
+   7. Guidance for Service Providers ..................................8
+   8. Security Considerations .........................................9
+   9. IANA Considerations .............................................9
+      9.1. Well-Known URI Registrations ...............................9
+           9.1.1. caldav Well-Known URI Registration .................10
+           9.1.2. carddav Well-Known URI Registration ................10
+      9.2. Service Name Registrations ................................10
+           9.2.1. caldav Service Name Registration ...................10
+           9.2.2. caldavs Service Name Registration ..................11
+           9.2.3. carddav Service Name Registration ..................11
+           9.2.4. carddavs Service Name Registration .................12
+   10. Acknowledgments ...............................................12
+   11. References ....................................................12
+      11.1. Normative References .....................................12
+      11.2. Informative References ...................................14
+
+1.  Introduction
+
+   [RFC4791] defines the CalDAV calendar access protocol, based on HTTP
+   [RFC2616], for accessing calendar data stored on a server.  CalDAV
+   clients need to be able to discover appropriate CalDAV servers within
+   their local area network and at other domains, e.g., to minimize the
+   need for end users to know specific details such as the fully
+   qualified domain name (FQDN) and port number for their servers.
+
+   [RFC6352] defines the CardDAV address book access protocol based on
+   HTTP [RFC2616], for accessing contact data stored on a server.  As
+   with CalDAV, clients also need to be able to discover CardDAV
+   servers.
+
+   [RFC2782] defines a DNS-based service discovery protocol that has
+   been widely adopted as a means of locating particular services within
+   a local area network and beyond, using DNS SRV Resource Records
+   (RRs).  This has been enhanced to provide additional service meta-
+   data by use of DNS TXT RRs as per [RFC6763].
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 2]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+   This specification defines new SRV service types for the CalDAV
+   protocol and gives an example of how clients can use this together
+   with other protocol features to enable simple client configuration.
+   SRV service types for CardDAV are already defined in Section 11 of
+   [RFC6352].
+
+   Another issue with CalDAV or CardDAV service discovery is that the
+   service might not be located at the "root" URI of the HTTP server
+   hosting it.  Thus, a client needs to be able to determine the
+   complete path component of the Request-URI to use in HTTP requests:
+   the "context path".  For example, if CalDAV is implemented as a
+   "servlet" in a web server "container", the servlet "context path"
+   might be "/caldav/".  So the URI for the CalDAV service would be,
+   e.g., "http://caldav.example.com/caldav/" rather than
+   "http://caldav.example.com/".  SRV RRs by themselves only provide an
+   FQDN and port number for the service, not a path.  Since the client
+   "bootstrapping" process requires initial access to the "context path"
+   of the service, there needs to be a simple way for clients to also
+   discover what that path is.
+
+   This specification makes use of the "well-known URI" feature
+   [RFC5785] of HTTP servers to provide a well-known URI for CalDAV or
+   CardDAV services that clients can use.  The well-known URI will point
+   to a resource on the server that is simply a "stub" resource that
+   provides a redirect to the actual "context path" resource
+   representing the service endpoint.
+
+2.  Conventions Used in This Document
+
+   The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
+   "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
+   document are to be interpreted as described in [RFC2119].
+
+3.  CalDAV SRV Service Labels
+
+   This specification adds two SRV service labels for use with CalDAV:
+
+   _caldav:   Identifies a CalDAV server that uses HTTP without
+      Transport Layer Security (TLS) [RFC2818].
+
+   _caldavs:  Identifies a CalDAV server that uses HTTP with TLS
+      [RFC2818].
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 3]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+   Clients MUST honor Priority and Weight values in the SRV RRs, as
+   described by [RFC2782].
+
+   Example: service record for server without TLS
+
+       _caldav._tcp     SRV 0 1 80 calendar.example.com.
+
+   Example: service record for server with TLS
+
+       _caldavs._tcp    SRV 0 1 443 calendar.example.com.
+
+4.  CalDAV and CardDAV Service TXT Records
+
+   When SRV RRs are used to advertise CalDAV and CardDAV services, it is
+   also convenient to be able to specify a "context path" in the DNS to
+   be retrieved at the same time.  To enable that, this specification
+   uses a TXT RR that follows the syntax defined in Section 6 of
+   [RFC6763] and defines a "path" key for use in that record.  The value
+   of the key MUST be the actual "context path" to the corresponding
+   service on the server.
+
+   A site might provide TXT records in addition to SRV records for each
+   service.  When present, clients MUST use the "path" value as the
+   "context path" for the service in HTTP requests.  When not present,
+   clients use the ".well-known" URI approach described next.
+
+   Example: text record for service with TLS
+
+       _caldavs._tcp    TXT path=/caldav
+
+5.  CalDAV and CardDAV Service Well-Known URI
+
+   Two ".well-known" URIs are registered by this specification for
+   CalDAV and CardDAV services, "caldav" and "carddav" respectively (see
+   Section 9).  These URIs point to a resource that the client can use
+   as the initial "context path" for the service they are trying to
+   connect to.  The server MUST redirect HTTP requests for that resource
+   to the actual "context path" using one of the available mechanisms
+   provided by HTTP (e.g., using a 301, 303, or 307 response).  Clients
+   MUST handle HTTP redirects on the ".well-known" URI.  Servers MUST
+   NOT locate the actual CalDAV or CardDAV service endpoint at the
+   ".well-known" URI as per Section 1.1 of [RFC5785].
+
+   Servers SHOULD set an appropriate Cache-Control header value (as per
+   Section 14.9 of [RFC2616]) in the redirect response to ensure caching
+   occurs or does not occur as needed or as required by the type of
+   response generated.  For example, if it is anticipated that the
+
+
+
+
+Daboo                        Standards Track                    [Page 4]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+   location of the redirect might change over time, then a "no-cache"
+   value would be used.
+
+   To facilitate "context paths" that might differ from user to user,
+   the server MAY require authentication when a client tries to access
+   the ".well-known" URI (i.e., the server would return a 401 status
+   response to the unauthenticated request from the client, then return
+   the redirect response only after a successful authentication by the
+   client).
+
+5.1.  Example: Well-Known URI Redirects to Actual "Context Path"
+
+   A CalDAV server has a "context path" that is "/servlet/caldav".  The
+   client will use "/.well-known/caldav" as the path for its
+   "bootstrapping" process after it has first found the FQDN and port
+   number via an SRV lookup or via manual entry of information by the
+   user, from which the client can parse suitable information.  When the
+   client makes an HTTP request against "/.well-known/caldav", the
+   server would issue an HTTP redirect response with a Location response
+   header using the path "/servlet/caldav".  The client would then
+   "follow" this redirect to the new resource and continue making HTTP
+   requests there to complete its "bootstrapping" process.
+
+6.  Client "Bootstrapping" Procedures
+
+   This section describes a procedure that CalDAV or CardDAV clients
+   SHOULD use to do their initial configuration based on minimal user
+   input.  The goal is to determine an http: or https: URI that
+   describes the full path to the user's principal-URL [RFC3744].
+
+   1.  Processing user input:
+
+       *  For a CalDAV server:
+
+          +  Minimal input from a user would consist of a calendar user
+             address and a password.  A calendar user address is defined
+             by iCalendar [RFC5545] to be a URI [RFC3986].  Provided a
+             user identifier and a domain name can be extracted from the
+             URI, this simple "bootstrapping" configuration can be done.
+
+          +  If the calendar user address is a "mailto:" [RFC6068] URI,
+             the "mailbox" portion of the URI is examined, and the
+             "local-part" and "domain" portions are extracted.
+
+          +  If the calendar user address is an "http:" [RFC2616] or
+             "https:" [RFC2818] URI, the "userinfo" and "host" portion
+             of the URI [RFC3986] is extracted.
+
+
+
+
+Daboo                        Standards Track                    [Page 5]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+       *  For a CardDAV server:
+
+          +  Minimal input from a user would consist of their email
+             address [RFC5322] for the domain where the CardDAV service
+             is hosted, and a password.  The "mailbox" portion of the
+             email address is examined, and the "local-part" and
+             "domain" portions are extracted.
+
+   2.  Determination of service FQDN and port number:
+
+       *  An SRV lookup for _caldavs._tcp (for CalDAV) or _carddavs._tcp
+          (for CardDAV) is done with the extracted "domain" as the
+          service domain.
+
+       *  If no result is found, the client can try _caldav._tcp (for
+          CalDAV) or _carddav._tcp (for CardDAV) provided non-TLS
+          connections are appropriate.
+
+       *  If an SRV record is returned, the client extracts the target
+          FQDN and port number.  If multiple SRV records are returned,
+          the client MUST use the Priority and Weight fields in the
+          record to determine which one to pick (as per [RFC2782]).
+
+       *  If an SRV record is not found, the client will need to prompt
+          the user to enter the FQDN and port number information
+          directly or use some other heuristic, for example, using the
+          extracted "domain" as the FQDN and default HTTPS or HTTP port
+          numbers.  In this situation, clients MUST first attempt an
+          HTTP connection with TLS.
+
+   3.  Determination of initial "context path":
+
+       *  When an SRV lookup is done and a valid SRV record returned,
+          the client MUST also query for a corresponding TXT record and
+          check for the presence of a "path" key in its response.  If
+          present, the value of the "path" key is used for the initial
+          "context path".
+
+       *  When an initial "context path" has not been determined from a
+          TXT record, the initial "context path" is taken to be
+          "/.well-known/caldav" (for CalDAV) or "/.well-known/carddav"
+          (for CardDAV).
+
+       *  If the initial "context path" derived from a TXT record
+          generates HTTP errors when targeted by requests, the client
+          SHOULD repeat its "bootstrapping" procedure using the
+          appropriate ".well-known" URI instead.
+
+
+
+
+Daboo                        Standards Track                    [Page 6]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+   4.  Determination of user identifier:
+
+       *  The client will need to make authenticated HTTP requests to
+          the service.  Typically, a "user identifier" is required for
+          some form of user/password authentication.  When a user
+          identifier is required, clients MUST first use the "mailbox"
+          portion of the calendar user address provided by the user in
+          the case of a "mailto:" address and, if that results in an
+          authentication failure, SHOULD fall back to using the "local-
+          part" extracted from the "mailto:" address.  For an "http:" or
+          "https:" calendar user address, the "userinfo" portion is used
+          as the user identifier for authentication.  This is in line
+          with the guidance outlined in Section 7.  If these user
+          identifiers result in authentication failure, the client
+          SHOULD prompt the user for a valid identifier.
+
+   5.  Connecting to the service:
+
+       *  Subsequent to configuration, the client will make HTTP
+          requests to the service.  When using "_caldavs" or "_carddavs"
+          services, a TLS negotiation is done immediately upon
+          connection.  The client MUST do certificate verification using
+          the procedure outlined in Section 6 of [RFC6125] in regard to
+          verification with an SRV RR as the starting point.
+
+       *  The client does a "PROPFIND" [RFC4918] request with the
+          request URI set to the initial "context path".  The body of
+          the request SHOULD include the DAV:current-user-principal
+          [RFC5397] property as one of the properties to return.  Note
+          that clients MUST properly handle HTTP redirect responses for
+          the request.  The server will use the HTTP authentication
+          procedure outlined in [RFC2617] or use some other appropriate
+          authentication schemes to authenticate the user.
+
+       *  If the server returns a 404 ("Not Found") HTTP status response
+          to the request on the initial "context path", clients MAY try
+          repeating the request on the "root" URI "/" or prompt the user
+          for a suitable path.
+
+       *  If the DAV:current-user-principal property is returned on the
+          request, the client uses that value for the principal-URL of
+          the authenticated user.  With that, it can execute a
+          "PROPFIND" request on the principal-URL and discover
+          additional properties for configuration (e.g., calendar or
+          address book "home" collections).
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 7]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+       *  If the DAV:current-user-principal property is not returned,
+          then the client will need to request the principal-URL path
+          from the user in order to continue with configuration.
+
+   Once a successful account discovery step has been done, clients
+   SHOULD cache the service details that were successfully used (user
+   identity, principal-URL with full scheme/host/port details) and reuse
+   those when connecting again at a later time.
+
+   If a subsequent connection attempt fails, or authentication fails
+   persistently, clients SHOULD retry the SRV lookup and account
+   discovery to "refresh" the cached data.
+
+7.  Guidance for Service Providers
+
+   Service providers wanting to offer CalDAV or CardDAV services that
+   can be configured by clients using SRV records need to follow certain
+   procedures to ensure proper operation.
+
+   o  CalDAV or CardDAV servers SHOULD be configured to allow
+      authentication with calendar user addresses (just taking the
+      "mailbox" portion of any "mailto:" URI) or email addresses
+      respectively, or with "user identifiers" extracted from them.  In
+      the former case, the addresses MUST NOT conflict with other forms
+      of a permitted user login name.  In the latter case, the extracted
+      "user identifiers" need to be unique across the server and MUST
+      NOT conflict with any login name on the server.
+
+   o  Servers MUST force authentication for "PROPFIND" requests that
+      retrieve the DAV:current-user-principal property to ensure that
+      the value of the DAV:current-user-principal property returned
+      corresponds to the principal-URL of the user making the request.
+
+   o  If the service provider uses TLS, the service provider MUST ensure
+      a certificate is installed that can be verified by clients using
+      the procedure outlined in Section 6 of [RFC6125] in regard to
+      verification with an SRV RR as the starting point.  In particular,
+      certificates SHOULD include SRV-ID and DNS-ID identifiers as
+      appropriate, as described in Section 8.
+
+   o  Service providers should install the appropriate SRV records for
+      the offered services and optionally include TXT records.
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 8]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+8.  Security Considerations
+
+   Clients that support TLS as defined by [RFC2818] SHOULD try the
+   "_caldavs" or "_carddavs" services first before trying the "_caldav"
+   or "_carddav" services respectively.  If a user has explicitly
+   requested a connection with TLS, the client MUST NOT use any service
+   information returned for the "_caldav" or "_carddav" services.
+   Clients MUST follow the certificate-verification process specified in
+   [RFC6125].
+
+   A malicious attacker with access to the DNS server data, or that is
+   able to get spoofed answers cached in a recursive resolver, can
+   potentially cause clients to connect to any server chosen by the
+   attacker.  In the absence of a secure DNS option, clients SHOULD
+   check that the target FQDN returned in the SRV record matches the
+   original service domain that was queried.  If the target FQDN is not
+   in the queried domain, clients SHOULD verify with the user that the
+   SRV target FQDN is suitable for use before executing any connections
+   to the host.  Alternatively, if TLS is being used for the service,
+   clients MUST use the procedure outlined in Section 6 of [RFC6125] to
+   verify the service.  When the target FQDN does not match the original
+   service domain that was queried, clients MUST check the SRV-ID
+   identifier in the server's certificate.  If the FQDN does match,
+   clients MUST check any SRV-ID identifiers in the server's certificate
+   or, if no SRV-ID identifiers are present, MUST check the DNS-ID
+   identifiers in the server's certificate.
+
+   Implementations of TLS [RFC5246], used as the basis for TLS
+   ([RFC2818]), typically support multiple versions of the protocol as
+   well as the older SSL (Secure Sockets Layer) protocol.  Because of
+   known security vulnerabilities, clients and servers MUST NOT request,
+   offer, or use SSL 2.0.  See Appendix E.2 of [RFC5246] for further
+   details.
+
+9.  IANA Considerations
+
+9.1.  Well-Known URI Registrations
+
+   This document defines two ".well-known" URIs using the registration
+   procedure and template from Section 5.1 of [RFC5785].
+
+
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 9]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+9.1.1.  caldav Well-Known URI Registration
+
+   URI suffix:  caldav
+
+   Change controller:  IETF
+
+   Specification document(s):  This RFC
+
+   Related information:  See also [RFC4791].
+
+9.1.2.  carddav Well-Known URI Registration
+
+   URI suffix:  carddav
+
+   Change controller:  IETF
+
+   Specification document(s):  This RFC
+
+   Related information:  See also [RFC6352].
+
+9.2.  Service Name Registrations
+
+   This document registers four new service names as per [RFC6335].  Two
+   are defined in this document, and two are defined in [RFC6352],
+   Section 11.
+
+9.2.1.  caldav Service Name Registration
+
+   Service Name:  caldav
+
+   Transport Protocol(s):  TCP
+
+   Assignee:  IESG <iesg at ietf.org>
+
+   Contact:  IETF Chair <chair at ietf.org>
+
+   Description:  Calendaring Extensions to WebDAV (CalDAV) - non-TLS
+
+   Reference:  [RFC6764]
+
+   Assignment Note:  This is an extension of the http service.  Defined
+      TXT keys: path=<context path>
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                   [Page 10]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+9.2.2.  caldavs Service Name Registration
+
+   Service Name:  caldavs
+
+   Transport Protocol(s):  TCP
+
+   Assignee:  IESG <iesg at ietf.org>
+
+   Contact:  IETF Chair <chair at ietf.org>
+
+   Description:  Calendaring Extensions to WebDAV (CalDAV) - over TLS
+
+   Reference:  [RFC6764]
+
+   Assignment Note:  This is an extension of the https service.  Defined
+      TXT keys: path=<context path>
+
+9.2.3.  carddav Service Name Registration
+
+   Service Name:  carddav
+
+   Transport Protocol(s):  TCP
+
+   Assignee:  IESG <iesg at ietf.org>
+
+   Contact:  IETF Chair <chair at ietf.org>
+
+   Description:  vCard Extensions to WebDAV (CardDAV) - non-TLS
+
+   Reference:  [RFC6352]
+
+   Assignment Note:  This is an extension of the http service.  Defined
+      TXT keys: path=<context path>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                   [Page 11]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+9.2.4.  carddavs Service Name Registration
+
+   Service Name:  carddavs
+
+   Transport Protocol(s):  TCP
+
+   Assignee:  IESG <iesg at ietf.org>
+
+   Contact:  IETF Chair <chair at ietf.org>
+
+   Description:  vCard Extensions to WebDAV (CardDAV) - over TLS
+
+   Reference:  [RFC6352]
+
+   Assignment Note:  This is an extension of the https service.  Defined
+      TXT keys: path=<context path>
+
+10.  Acknowledgments
+
+   This specification was suggested by discussion that took place within
+   the Calendaring and Scheduling Consortium's CalDAV Technical
+   Committee.  The author thanks the following for their contributions:
+   Stuart Cheshire, Bernard Desruisseaux, Eran Hammer-Lahav, Helge Hess,
+   Arnaud Quillaud, Wilfredo Sanchez, and Joe Touch.
+
+11.  References
+
+11.1.  Normative References
+
+   [RFC2119]  Bradner, S., "Key words for use in RFCs to Indicate
+              Requirement Levels", BCP 14, RFC 2119, March 1997.
+
+   [RFC2616]  Fielding, R., Gettys, J., Mogul, J., Frystyk, H.,
+              Masinter, L., Leach, P., and T. Berners-Lee, "Hypertext
+              Transfer Protocol -- HTTP/1.1", RFC 2616, June 1999.
+
+   [RFC2617]  Franks, J., Hallam-Baker, P., Hostetler, J., Lawrence, S.,
+              Leach, P., Luotonen, A., and L. Stewart, "HTTP
+              Authentication: Basic and Digest Access Authentication",
+              RFC 2617, June 1999.
+
+   [RFC2782]  Gulbrandsen, A., Vixie, P., and L. Esibov, "A DNS RR for
+              specifying the location of services (DNS SRV)", RFC 2782,
+              February 2000.
+
+   [RFC2818]  Rescorla, E., "HTTP Over TLS", RFC 2818, May 2000.
+
+
+
+
+
+Daboo                        Standards Track                   [Page 12]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+   [RFC3744]  Clemm, G., Reschke, J., Sedlar, E., and J. Whitehead, "Web
+              Distributed Authoring and Versioning (WebDAV)
+              Access Control Protocol", RFC 3744, May 2004.
+
+   [RFC3986]  Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform
+              Resource Identifier (URI): Generic Syntax", STD 66,
+              RFC 3986, January 2005.
+
+   [RFC4791]  Daboo, C., Desruisseaux, B., and L. Dusseault,
+              "Calendaring Extensions to WebDAV (CalDAV)", RFC 4791,
+              March 2007.
+
+   [RFC4918]  Dusseault, L., "HTTP Extensions for Web Distributed
+              Authoring and Versioning (WebDAV)", RFC 4918, June 2007.
+
+   [RFC5246]  Dierks, T. and E. Rescorla, "The Transport Layer Security
+              (TLS) Protocol Version 1.2", RFC 5246, August 2008.
+
+   [RFC5322]  Resnick, P., Ed., "Internet Message Format", RFC 5322,
+              October 2008.
+
+   [RFC5397]  Sanchez, W. and C. Daboo, "WebDAV Current Principal
+              Extension", RFC 5397, December 2008.
+
+   [RFC5785]  Nottingham, M. and E. Hammer-Lahav, "Defining Well-Known
+              Uniform Resource Identifiers (URIs)", RFC 5785,
+              April 2010.
+
+   [RFC6068]  Duerst, M., Masinter, L., and J. Zawinski, "The 'mailto'
+              URI Scheme", RFC 6068, October 2010.
+
+   [RFC6125]  Saint-Andre, P. and J. Hodges, "Representation and
+              Verification of Domain-Based Application Service Identity
+              within Internet Public Key Infrastructure Using X.509
+              (PKIX) Certificates in the Context of Transport Layer
+              Security (TLS)", RFC 6125, March 2011.
+
+   [RFC6335]  Cotton, M., Eggert, L., Touch, J., Westerlund, M., and S.
+              Cheshire, "Internet Assigned Numbers Authority (IANA)
+              Procedures for the Management of the Service Name and
+              Transport Protocol Port Number Registry", BCP 165,
+              RFC 6335, August 2011.
+
+   [RFC6352]  Daboo, C., "CardDAV: vCard Extensions to Web Distributed
+              Authoring and Versioning (WebDAV)", RFC 6352, August 2011.
+
+   [RFC6763]  Cheshire, S. and M. Krochmal, "DNS-Based Service
+              Discovery", RFC 6763, February 2013.
+
+
+
+Daboo                        Standards Track                   [Page 13]
+
+RFC 6764                SRV for CalDAV & CardDAV           February 2013
+
+
+11.2.  Informative References
+
+   [RFC5545]  Desruisseaux, B., "Internet Calendaring and Scheduling
+              Core Object Specification (iCalendar)", RFC 5545,
+              September 2009.
+
+Author's Address
+
+   Cyrus Daboo
+   Apple Inc.
+   1 Infinite Loop
+   Cupertino, CA  95014
+   USA
+
+   EMail: cyrus at daboo.name
+   URI:   http://www.apple.com/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                   [Page 14]
+

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/RFC6868-Parameter Value Encoding.txt (from rev 11028, CalendarServer/trunk/doc/RFC/RFC6868-Parameter Value Encoding.txt)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/RFC6868-Parameter Value Encoding.txt	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/RFC6868-Parameter Value Encoding.txt	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+Internet Engineering Task Force (IETF)                          C. Daboo
+Request for Comments: 6868                                         Apple
+Updates: 5545, 6321, 6350, 6351                            February 2013
+Category: Standards Track
+ISSN: 2070-1721
+
+
+            Parameter Value Encoding in iCalendar and vCard
+
+Abstract
+
+   This specification updates the data formats for iCalendar (RFC 5545)
+   and vCard (RFC 6350) to allow parameter values to include certain
+   characters forbidden by the existing specifications.
+
+Status of This Memo
+
+   This is an Internet Standards Track document.
+
+   This document is a product of the Internet Engineering Task Force
+   (IETF).  It represents the consensus of the IETF community.  It has
+   received public review and has been approved for publication by the
+   Internet Engineering Steering Group (IESG).  Further information on
+   Internet Standards is available in Section 2 of RFC 5741.
+
+   Information about the current status of this document, any errata,
+   and how to provide feedback on it may be obtained at
+   http://www.rfc-editor.org/info/rfc6868.
+
+Copyright Notice
+
+   Copyright (c) 2013 IETF Trust and the persons identified as the
+   document authors.  All rights reserved.
+
+   This document is subject to BCP 78 and the IETF Trust's Legal
+   Provisions Relating to IETF Documents
+   (http://trustee.ietf.org/license-info) in effect on the date of
+   publication of this document.  Please review these documents
+   carefully, as they describe your rights and restrictions with respect
+   to this document.  Code Components extracted from this document must
+   include Simplified BSD License text as described in Section 4.e of
+   the Trust Legal Provisions and are provided without warranty as
+   described in the Simplified BSD License.
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 1]
+
+RFC 6868                   Parameter Encoding              February 2013
+
+
+Table of Contents
+
+   1. Introduction ....................................................2
+   2. Conventions Used in This Document ...............................2
+   3. Parameter Value Encoding Scheme .................................3
+      3.1. iCalendar Example ..........................................4
+      3.2. vCard Example ..............................................4
+   4. Security Considerations .........................................4
+   5. Acknowledgments .................................................4
+   6. Normative References ............................................5
+   Appendix A. Choice of Quoting Mechanism ............................6
+
+1.  Introduction
+
+   The iCalendar [RFC5545] specification defines a standard way to
+   describe calendar data.  The vCard [RFC6350] specification defines a
+   standard way to describe contact data.  Both of these use a similar
+   text-based data format.  Each iCalendar and vCard data object can
+   include "properties" that have "parameters" and a "value".  The value
+   of a "parameter" is typically a token or URI value, but a "generic"
+   text value is also allowed.  However, the syntax rules for both
+   iCalendar and vCard prevent the use of a double-quote character or
+   control characters in such values, though double-quote characters and
+   some subset of control characters are allowed in the actual property
+   values.
+
+   As more and more extensions are being developed for these data
+   formats, there is a need to allow at least double-quotes and line
+   feeds to be included in parameter values.  The \-escaping mechanism
+   used for property text values is not defined for use with parameter
+   values and cannot be easily used in a backwards-compatible manner.
+   This specification defines a new character escaping mechanism,
+   compatible with existing parsers and chosen to minimize any impact on
+   existing data.
+
+2.  Conventions Used in This Document
+
+   The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
+   "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
+   "OPTIONAL" in this document are to be interpreted as described in
+   [RFC2119].
+
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 2]
+
+RFC 6868                   Parameter Encoding              February 2013
+
+
+3.  Parameter Value Encoding Scheme
+
+   This specification defines the ^ character (U+005E -- Circumflex
+   Accent) as an escape character in parameter values whose value type
+   is defined using the "param-value" syntax element (Section 3.1 of
+   iCalendar [RFC5545] and Section 3.3 of vCard [RFC6350]).  The
+   ^-escaping mechanism can be used when the value is either unquoted or
+   quoted (i.e., whether or not the value is surrounded by double-
+   quotes).
+
+   When generating iCalendar or vCard parameter values, the following
+   apply:
+
+   o  formatted text line breaks are encoded into ^n (U+005E, U+006E)
+
+   o  the ^ character (U+005E) is encoded into ^^ (U+005E, U+005E)
+
+   o  the " character (U+0022) is encoded into ^' (U+005E, U+0027)
+
+   When parsing iCalendar or vCard parameter values, the following
+   apply:
+
+   o  the character sequence ^n (U+005E, U+006E) is decoded into an
+      appropriate formatted line break according to the type of system
+      being used
+
+   o  the character sequence ^^ (U+005E, U+005E) is decoded into the ^
+      character (U+005E)
+
+   o  the character sequence ^' (U+005E, U+0027) is decoded into the "
+      character (U+0022)
+
+   o  if a ^ (U+005E) character is followed by any character other than
+      the ones above, parsers MUST leave both the ^ and the following
+      character in place
+
+   When converting between iCalendar and vCard text-based data formats
+   and alternative data-format representations such as XML (as described
+   in [RFC6321] and [RFC6351], respectively), implementations MUST
+   ensure that parameter value escape sequences are generated correctly
+   in the text-based format and are decoded when the parameter values
+   appear in the alternate data formats.
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 3]
+
+RFC 6868                   Parameter Encoding              February 2013
+
+
+3.1.  iCalendar Example
+
+   The following example is an "ATTENDEE" property with a "CN" parameter
+   whose value includes two double-quote characters.  The parameter
+   value is not quoted, as there are no characters in the value that
+   would trigger quoting as required by iCalendar.
+
+   ATTENDEE;CN=George Herman ^'Babe^' Ruth:mailto:babe at example.com
+
+   The unescaped parameter value is
+
+   George Herman "Babe" Ruth
+
+3.2.  vCard Example
+
+   The following example is a "GEO" property with an "X-ADDRESS"
+   parameter whose value includes several line feed characters.  The
+   parameter value is also quoted, since it contains a comma, which
+   triggers quoting as required by vCard.
+
+   GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt
+    sburgh, PA 15212":geo:40.446816,-80.00566
+
+   The unescaped parameter value (where each line is terminated by a
+   line break character sequence) is
+
+   Pittsburgh Pirates
+   115 Federal St
+   Pittsburgh, PA 15212
+
+4.  Security Considerations
+
+   There are no additional security issues beyond those of iCalendar
+   [RFC5545] and vCard [RFC6350].
+
+5.  Acknowledgments
+
+   Thanks to Michael Angstadt, Tim Bray, Mike Douglass, Barry Leiba,
+   Simon Perreault, and Pete Resnick for feedback on this specification.
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 4]
+
+RFC 6868                   Parameter Encoding              February 2013
+
+
+6.  Normative References
+
+   [RFC2119]  Bradner, S., "Key words for use in RFCs to Indicate
+              Requirement Levels", BCP 14, RFC 2119, March 1997.
+
+   [RFC5545]  Desruisseaux, B., "Internet Calendaring and Scheduling
+              Core Object Specification (iCalendar)", RFC 5545,
+              September 2009.
+
+   [RFC6321]  Daboo, C., Douglass, M., and S. Lees, "xCal: The XML
+              Format for iCalendar", RFC 6321, August 2011.
+
+   [RFC6350]  Perreault, S., "vCard Format Specification", RFC 6350,
+              August 2011.
+
+   [RFC6351]  Perreault, S., "xCard: vCard XML Representation",
+              RFC 6351, August 2011.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 5]
+
+RFC 6868                   Parameter Encoding              February 2013
+
+
+Appendix A.  Choice of Quoting Mechanism
+
+   Having recognized the need for escaping parameter values, the
+   question is what mechanism to use?  One obvious choice would be to
+   adopt the \-escaping used for property values.  However, that could
+   not be used as-is, because it escapes a double-quote as the sequence
+   of \ followed by double-quote.  Consider what the example in
+   Section 3.1 might look like using \-escaping:
+
+   ATTENDEE;CN="George Herman \"Babe\" Ruth":mailto:babe at example.com
+
+   Existing iCalendar/vCard parsers know nothing about escape sequences
+   in parameters.  So they would parse the parameter value as:
+
+   George Herman \
+
+   i.e., the text between the first and second occurrence of a double-
+   quote.  However, the text after the second double-quote ought to be
+   either a : or a ; (to delimit the parameter value from the following
+   parameter or property) but is not, so the parser could legitimately
+   throw an error at that point because the data is syntactically
+   invalid.  Thus, for backwards-compatibility reasons, a double-quote
+   cannot be escaped using a sequence that itself includes a double-
+   quote, and hence the choice of using a single-quote in this
+   specification.
+
+   Another option would be to use a form of \-escaping modified for use
+   in parameter values only.  However, some incorrect, non-interoperable
+   use of \ in parameter values has been observed, and thus it is best
+   to steer clear of that to achieve guaranteed, reliable
+   interoperability.  Also, given that double-quote gets changed to
+   single-quote in the escape sequence for a parameter, but not for a
+   value, it is better to not give the impression that the same escape
+   mechanism (and thus code) can be used for both (which could lead to
+   other issues, such as an implementation incorrectly escaping a ; as
+   \; as opposed to quoting the parameter value).
+
+   The choice of ^ as the escape character was made based on the
+   requirement that an ASCII symbol (non-alphanumeric character) be
+   used, and it ought to be one least likely to be found in existing
+   data.
+
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 6]
+
+RFC 6868                   Parameter Encoding              February 2013
+
+
+Author's Address
+
+   Cyrus Daboo
+   Apple Inc.
+   1 Infinite Loop
+   Cupertino, CA  95014
+   USA
+
+   EMail: cyrus at daboo.name
+   URI:   http://www.apple.com/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                        Standards Track                    [Page 7]
+
\ No newline at end of file

Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/draft-daboo-srv-caldav.txt
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/draft-daboo-srv-caldav.txt	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/doc/RFC/draft-daboo-srv-caldav.txt	2013-04-11 16:24:18 UTC (rev 11029)
@@ -1,840 +0,0 @@
-
-
-
-Network Working Group                                           C. Daboo
-Internet-Draft                                                Apple Inc.
-Updates: 4791,CardDAV-RFC-to-be                       September 16, 2010
-(if approved)
-Intended status: Standards Track
-Expires: March 20, 2011
-
-
-                  Locating CalDAV and CardDAV services
-                       draft-daboo-srv-caldav-10
-
-Abstract
-
-   This specification describes how DNS SRV records, DNS TXT records and
-   well-known URIs can be used together or separately to locate
-   Calendaring Extensions to WebDAV (CalDAV) or vCard Extensions to
-   WebDAV (CardDAV) services.
-
-Status of This Memo
-
-   This Internet-Draft is submitted in full conformance with the
-   provisions of BCP 78 and BCP 79.
-
-   Internet-Drafts are working documents of the Internet Engineering
-   Task Force (IETF).  Note that other groups may also distribute
-   working documents as Internet-Drafts.  The list of current Internet-
-   Drafts is at http://datatracker.ietf.org/drafts/current/.
-
-   Internet-Drafts are draft documents valid for a maximum of six months
-   and may be updated, replaced, or obsoleted by other documents at any
-   time.  It is inappropriate to use Internet-Drafts as reference
-   material or to cite them other than as "work in progress."
-
-   This Internet-Draft will expire on March 20, 2011.
-
-Copyright Notice
-
-   Copyright (c) 2010 IETF Trust and the persons identified as the
-   document authors.  All rights reserved.
-
-   This document is subject to BCP 78 and the IETF Trust's Legal
-   Provisions Relating to IETF Documents
-   (http://trustee.ietf.org/license-info) in effect on the date of
-   publication of this document.  Please review these documents
-   carefully, as they describe your rights and restrictions with respect
-   to this document.  Code Components extracted from this document must
-   include Simplified BSD License text as described in Section 4.e of
-   the Trust Legal Provisions and are provided without warranty as
-
-
-
-Daboo                    Expires March 20, 2011                 [Page 1]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-   described in the Simplified BSD License.
-
-Table of Contents
-
-   1.  Introduction . . . . . . . . . . . . . . . . . . . . . . . . .  3
-   2.  Conventions Used in This Document  . . . . . . . . . . . . . .  4
-   3.  CalDAV SRV Service Labels  . . . . . . . . . . . . . . . . . .  4
-   4.  CalDAV and CardDAV Service TXT Records . . . . . . . . . . . .  4
-   5.  CalDAV and CardDAV Service Well-Known URI  . . . . . . . . . .  5
-     5.1.  Example: well-known URI redirects to actual context
-           path . . . . . . . . . . . . . . . . . . . . . . . . . . .  5
-   6.  Client "Bootstrapping" Procedures  . . . . . . . . . . . . . .  5
-   7.  Guidance for Service Providers . . . . . . . . . . . . . . . .  8
-   8.  Security Considerations  . . . . . . . . . . . . . . . . . . .  9
-   9.  IANA Considerations  . . . . . . . . . . . . . . . . . . . . .  9
-     9.1.  caldav Well-Known URI Registration . . . . . . . . . . . . 10
-     9.2.  carddav Well-Known URI Registration  . . . . . . . . . . . 10
-     9.3.  SRV Service Label Registration . . . . . . . . . . . . . . 10
-   10. Acknowledgments  . . . . . . . . . . . . . . . . . . . . . . . 10
-   11. References . . . . . . . . . . . . . . . . . . . . . . . . . . 10
-     11.1. Normative References . . . . . . . . . . . . . . . . . . . 10
-     11.2. Informative References . . . . . . . . . . . . . . . . . . 12
-   Appendix A.  Change History (to be removed prior to
-                publication as an RFC)  . . . . . . . . . . . . . . . 13
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Daboo                    Expires March 20, 2011                 [Page 2]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-1.  Introduction
-
-   [RFC4791] defines the CalDAV calendar access protocol, based on HTTP
-   [RFC2616], for accessing calendar data stored on a server.  CalDAV
-   clients need to be able to discover appropriate CalDAV servers within
-   their local area network and at other domains, e.g., to minimize the
-   need for end users to know specific details such as the fully
-   qualified domain name (FQDN) and port number for their servers.
-
-   [I-D.ietf-vcarddav-carddav] defines the CardDAV address book access
-   protocol based on HTTP [RFC2616], for accessing contact data stored
-   on a server.  As with CalDAV, clients also need to be able to
-   discover CardDAV servers.
-
-   [RFC2782] defines a DNS-based service discovery protocol that has
-   been widely adopted as a means of locating particular services within
-   a local area network and beyond, using DNS SRV Resource Records
-   (RRs).  This has been enhanced to provide additional service meta-
-   data by use of DNS TXT Resource Records as per
-   [I-D.cheshire-dnsext-dns-sd].
-
-   This specification defines new SRV service types for the CalDAV
-   protocol, and gives an example of how clients can use this together
-   with other protocol features to enable simple client configuration.
-   SRV service types for CardDAV are already defined in Section 11 of
-   [I-D.ietf-vcarddav-carddav].
-
-   Another issue with CalDAV or CardDAV service discovery is that the
-   service might not be located at the "root" URI of the HTTP server
-   hosting it.  Thus a client needs to be able to determine the complete
-   path component of the Request-URI to use in HTTP requests: the
-   "context path".  For example, if CalDAV is implemented as a "servlet"
-   in a web server "container", the servlet "context path" might be
-   "/caldav/".  So the URI for the CalDAV service would be, e.g.,
-   "http://caldav.example.com/caldav/" rather than
-   "http://caldav.example.com/".  SRV RRs by themselves only provide a
-   FQDN and port number for the service, not a path.  Since the client
-   "bootstrapping" process requires initial access to the "context path"
-   of the service, there needs to be a simple way for clients to also
-   discover what that path is.
-
-   This specification makes use of the "well known URI" feature
-   [RFC5785] of HTTP servers to provide a well known URI for CalDAV or
-   CardDAV services that clients can make use of.  The well known URI
-   will point to a resource on the server that is simply a "stub"
-   resource that provides a redirect to the actual "context path"
-   resource representing the service endpoint.
-
-
-
-
-Daboo                    Expires March 20, 2011                 [Page 3]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-2.  Conventions Used in This Document
-
-   The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
-   "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
-   document are to be interpreted as described in [RFC2119].
-
-3.  CalDAV SRV Service Labels
-
-   This specification adds two SRV service labels for use with CalDAV:
-
-   _caldav:  Identifies a CalDAV server that uses HTTP without transport
-      layer security ([RFC2818]).
-
-   _caldavs:  Identifies a CalDAV server that uses HTTP with transport
-      layer security ([RFC2818]).
-
-   Clients MUST honor "Priority" and "Weight" values in the SRV RRs, as
-   described by [RFC2782].
-
-   Example: service record for server without transport layer security
-
-       _caldav._tcp     SRV 0 1 80 calendar.example.com.
-
-   Example: service record for server with transport layer security
-
-       _caldavs._tcp    SRV 0 1 443 calendar.example.com.
-
-4.  CalDAV and CardDAV Service TXT Records
-
-   When SRV RRs are used to advertise CalDAV and CardDAV services, it is
-   also convenient to be able to specify a "context path" in the DNS to
-   be retrieved at the same time.  To enable that, this specification
-   uses a TXT RR that follows the syntax defined in Section 6 of
-   [I-D.cheshire-dnsext-dns-sd] and defines a "path" key for use in that
-   record.  The value of the key MUST be the actual "context path" to
-   the corresponding service on the server.
-
-   A site might provide TXT records in addition to SRV records for each
-   service.  When present, clients MUST use the "path" value as the
-   "context path" for the service in HTTP requests.  When not present,
-   clients use the ".well-known" URI approach described next.
-
-   Example: text record for service with transport layer security
-
-       _caldavs._tcp    TXT path=/caldav
-
-
-
-
-
-
-Daboo                    Expires March 20, 2011                 [Page 4]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-5.  CalDAV and CardDAV Service Well-Known URI
-
-   Two ".well-known" URIs are registered by this specification for
-   CalDAV and CardDAV services, "caldav" and "carddav" respectively (see
-   Section 9).  These URIs point to a resource that the client can use
-   as the initial "context path" for the service they are trying to
-   connect to.  The server MUST redirect HTTP requests for that resource
-   to the actual "context path" using one of the available mechanisms
-   provided by HTTP (e.g., using a 301, 303, 307 response).  Clients
-   MUST handle HTTP redirects on the ".well-known" URI.  Servers MUST
-   NOT locate the actual CalDAV or CardDAV service endpoint at the
-   ".well-known" URI as per Section 1.1 of [RFC5785].
-
-   Servers SHOULD set an appropriate Cache-Control header value (as per
-   Section 14.9 of [RFC2616]) in the redirect response to ensure caching
-   occurs or does not occur as needed, or as required by the type of
-   response generated.  For example, if it is anticipated that the
-   location of the redirect might change over time, then a "no-cache"
-   value would be used.
-
-   To facilitate "context path's" that might differ from user to user,
-   the server MAY require authentication when a client tries to access
-   the ".well-known" URI (i.e., the server would return a 401 status
-   response to the unauthenticated request from the client, then return
-   the redirect response only after a successful authentication by the
-   client).
-
-5.1.  Example: well-known URI redirects to actual context path
-
-   A CalDAV server has a "context path" that is "/servlet/caldav".  The
-   client will use "/.well-known/caldav" as the path for its
-   "bootstrapping" process after it has first found the FQDN and port
-   number via an SRV lookup or via manual entry of information by the
-   user which the client can parse suitable information from.  When the
-   client makes an HTTP request against "/.well-known/caldav", the
-   server would issue an HTTP redirect response with a Location response
-   header using the path "/servlet/caldav".  The client would then
-   "follow" this redirect to the new resource and continue making HTTP
-   requests there to complete its "bootstrapping" process.
-
-6.  Client "Bootstrapping" Procedures
-
-   This section describes a procedure that CalDAV or CardDAV clients
-   SHOULD use to do their initial configuration based on minimal user
-   input.  The goal is to determine an http: or https: URI that
-   describes the full path to the user's principal-URL [RFC3744].
-
-
-
-
-
-Daboo                    Expires March 20, 2011                 [Page 5]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-   1.  Processing user input:
-
-       *  For a CalDAV server:
-
-          +  Minimal input from a user would consist of a calendar user
-             address and a password.  A calendar user address is defined
-             by iCalendar [RFC5545] to be a URI [RFC3986].  Provided a
-             user identifier and a domain name can be extracted from the
-             URI, this simple "bootstrap" configuration can be done.
-
-          +  If the calendar user address is a "mailto:" [RFC2368] URI,
-             the "mailbox" portion of the URI is examined and the
-             "local-part" and "domain" portions extracted.
-
-          +  If the calendar user address is an "http:" [RFC2616] or
-             "https:" [RFC2818] URI, the "userinfo" and "host" portion
-             of the URI [RFC3986] is extracted.
-
-       *  For a CardDAV server:
-
-          +  Minimal input from a user would consist of their email
-             address [RFC5322] for the domain where the CardDAV service
-             is hosted, and a password.  The "mailbox" portion of the
-             email address is examined and the "local-part" and "domain"
-             portions extracted.
-
-   2.  Determination of service FQDN and port number:
-
-       *  An SRV lookup for _caldavs._tcp (for CalDAV) or _carddavs._tcp
-          (for CardDAV) is done with the extracted "domain" as the
-          service domain.
-
-       *  If no result is found, the client can try _caldav._tcp (for
-          CalDAV) or _carddav._tcp (for CardDAV) provided non-SSL
-          connections are appropriate.
-
-       *  If an SRV record is returned, the client extracts the target
-          FQDN and port number.  In the case of multiple SRV records
-          returned, the client MUST use the priority and weight fields
-          in the record to determine which one to pick (as per
-          [RFC2782]).
-
-       *  If an SRV record is not found, the client will need to prompt
-          the user to enter the FQDN and port number information
-          directly, or use some other heuristic, for example using the
-          extracted "domain" as the FQDN and default HTTPS or HTTP port
-          numbers.  In this situation clients MUST first attempt an HTTP
-          connection with transport layer security.
-
-
-
-Daboo                    Expires March 20, 2011                 [Page 6]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-   3.  Determination of initial "context path":
-
-       *  When an SRV lookup is done and a valid SRV record returned,
-          the client MUST also query for a corresponding TXT record and
-          check for the presence of a "path" key in its response.  If
-          present, the value of the "path" key is used for the initial
-          "context path".
-
-       *  When an initial "context path" has not been determined from a
-          TXT record, the initial "context path" is taken to be "/.well-
-          known/caldav" (for CalDAV) or "/.well-known/carddav" (for
-          CardDAV).
-
-       *  If the initial "context path" derived from a TXT record
-          generates HTTP errors when targeted by requests, the client
-          SHOULD repeat its bootstrap procedure using the appropriate
-          ".well-known" URI instead.
-
-   4.  Determination of user identifier:
-
-       *  The client will need to make authenticated HTTP requests to
-          the service.  Typically a "user identifier" is required for
-          some form of user/password authentication.  When a user
-          identifier is required, clients MUST first use the "mailbox"
-          portion of the calendar user address provided by the user in
-          the case of a "mailto:" address, and if that results in an
-          authentication failure, SHOULD fall back to using the "local-
-          part" extracted from the "mailto:" address.  For an "http:" or
-          "https:" calendar user address, the "userinfo" portion is used
-          as the user identifier for authentication.  This is in line
-          with the guidance outlined in Section 7.  If these user
-          identifiers result in authentication failure, the client
-          SHOULD prompt the user for a valid identifier.
-
-   5.  Connecting to the service:
-
-       *  Subsequent to configuration, the client will make HTTP
-          requests to the service.  When using "_caldavs" or "_carddavs"
-          services, a transport layer security negotiation is done
-          immediately upon connection.  The client MUST do certificate
-          verification using the procedure outlined in Section 4 of
-          [I-D.saintandre-tls-server-id-check] in regard to verification
-          with an SRV RR as the starting point.
-
-       *  The client does a "PROPFIND" [RFC4918] request with the
-          request URI set to the initial "context path".  The body of
-          the request SHOULD include the DAV:current-user-principal
-          [RFC5397] property as one of the properties to return.  Note
-
-
-
-Daboo                    Expires March 20, 2011                 [Page 7]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-          that clients MUST properly handle HTTP redirect responses for
-          the request.  The server will use the HTTP authentication
-          procedure outlined in [RFC2617] or use some other appropriate
-          authentication schemes to authenticate the user.
-
-       *  If the server returns a 404 Not Found HTTP status response to
-          the request on the initial "context path", clients MAY try
-          repeating the request on the "root" URI "/" or prompt the user
-          for a suitable path.
-
-       *  If the DAV:current-user-principal property is returned on the
-          request, the client uses that value for the principal-URL of
-          the authenticated user.  With that, it can execute a
-          "PROPFIND" request on the principal-URL and discover
-          additional properties for configuration (e.g., calendar or
-          address book "home" collections).
-
-       *  If the DAV:current-user-principal property is not returned,
-          then the client will need to request the principal-URL path
-          from the user in order to continue with configuration.
-
-   Once a successful account discovery step has been done, clients
-   SHOULD cache the service details that were successfully used (user
-   identity, principal-URL with full scheme/host/port details), and re-
-   use those when connecting again at a later time.
-
-   If a subsequent connection attempt fails, or authentication fails
-   persistently, clients SHOULD re-try the SRV lookup and account
-   discovery to "refresh" the cached data.
-
-7.  Guidance for Service Providers
-
-   Service providers wanting to offer CalDAV or CardDAV services that
-   can be configured by clients using SRV records need to follow certain
-   procedures to ensure proper operation.
-
-   o  CalDAV or CardDAV servers SHOULD be configured to allow
-      authentication with calendar user addresses (just taking the
-      "mailbox" portion of any "mailto:" URI) or email addresses
-      respectively, or "user identifiers" extracted from them.  In the
-      former case, the addresses MUST NOT conflict with other forms of
-      permitted user login name.  In the latter case, the extracted
-      "user identifiers" need to be unique across the server and MUST
-      NOT conflict with any login name on the server.
-
-   o  Servers MUST force authentication for "PROPFIND" requests that
-      retrieve the DAV:current-user-principal property to ensure that
-      the value of the DAV:current-user-principal property returned
-
-
-
-Daboo                    Expires March 20, 2011                 [Page 8]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-      corresponds to the principal-URL of the user making the request.
-
-   o  If the service provider uses transport layer security, the service
-      provider MUST ensure a certificate is installed that can be
-      verified by clients using the procedure outlined in Section 4 of
-      [I-D.saintandre-tls-server-id-check] in regard to verification
-      with an SRV RR as the starting point.
-
-   o  Install the appropriate SRV records for the offered services.
-      Optionally include TXT records.
-
-8.  Security Considerations
-
-   Clients that support transport layer security as defined by [RFC2818]
-   SHOULD try the "_caldavs" or "_carddavs" services first before trying
-   the "_caldav" or "_carddav" services respectively.  If a user has
-   explicitly requested a connection with transport layer security, the
-   client MUST NOT use any service information returned for the
-   "_caldav" or "_carddav" services.  Clients MUST follow the
-   certificate verification process specified in
-   [I-D.saintandre-tls-server-id-check].
-
-   A malicious attacker with access to the DNS server data, or able to
-   get spoofed answers cached in a recursive resolver, can potentially
-   cause clients to connect to any server chosen by the attacker.  In
-   the absence of a secure DNS option, clients SHOULD check that the
-   target FQDN returned in the SRV record matches the original service
-   domain that was queried.  If the target FQDN is not in the queried
-   domain, clients SHOULD verify with the user that the SRV target FQDN
-   is suitable for use before executing any connections to the host.
-   Alternatively, if transport layer security is being used for the
-   service, clients MUST use the procedure outlined in Section 4 of
-   [I-D.saintandre-tls-server-id-check] to verify the service.
-
-   Implementations of TLS [RFC5246], used as the basis for transport
-   layer security ([RFC2818]), typically support multiple versions of
-   the protocol as well as the older Secure Sockets Layer (SSL)
-   protocol.  Because of known security vulnerabilities, clients and
-   servers MUST NOT request, offer, or use SSL 2.0.  See Appendix E.2 of
-   [RFC5246] for further details.
-
-9.  IANA Considerations
-
-   This document defines two ".well-known" URIs using the registration
-   procedure and template from Section 5.1 of [RFC5785].
-
-
-
-
-
-
-Daboo                    Expires March 20, 2011                 [Page 9]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-9.1.  caldav Well-Known URI Registration
-
-   URI suffix:  caldav
-
-   Change controller:  IETF.
-
-   Specification document(s):  This RFC.
-
-   Related information:  See also [RFC4791].
-
-9.2.  carddav Well-Known URI Registration
-
-   URI suffix:  carddav
-
-   Change controller:  IETF.
-
-   Specification document(s):  This RFC.
-
-   Related information:  See also [I-D.ietf-vcarddav-carddav].
-
-9.3.  SRV Service Label Registration
-
-   Service labels have been registered according to
-   <http://www.dns-sd.org/ServiceTypes.html> [1] and will be
-   incorporated into IANA once a new registry is available there.
-
-10.  Acknowledgments
-
-   This specification was suggested by discussion that took place within
-   the Calendaring and Scheduling Consortium's CalDAV Technical
-   Committee.  The author thanks the following for their contributions:
-   Stuart Cheshire, Bernard Desruisseaux, Eran Hammer-Lahav, Helge Hess,
-   Arnaud Quillaud, Wilfredo Sanchez, and Joe Touch.
-
-11.  References
-
-11.1.  Normative References
-
-   [I-D.cheshire-dnsext-dns-sd]          Cheshire, S. and M. Krochmal,
-                                         "DNS-Based Service Discovery",
-                                         draft-cheshire-dnsext-dns-sd-06
-                                         (work in progress), March 2010.
-
-   [I-D.ietf-vcarddav-carddav]           Daboo, C., "vCard Extensions to
-                                         WebDAV (CardDAV)",
-                                         draft-ietf-vcarddav-carddav-10
-                                         (work in progress),
-                                         November 2009.
-
-
-
-Daboo                    Expires March 20, 2011                [Page 10]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-   [I-D.saintandre-tls-server-id-check]  Saint-Andre, P. and J. Hodges,
-                                         "Representation and
-                                         Verification of Domain-Based
-                                         Application Service Identity in
-                                         Certificates Used with
-                                         Transport Layer Security", draf
-                                         t-saintandre-tls-server-id-
-                                         check-09 (work in progress),
-                                         August 2010.
-
-   [RFC2119]                             Bradner, S., "Key words for use
-                                         in RFCs to Indicate Requirement
-                                         Levels", BCP 14, RFC 2119,
-                                         March 1997.
-
-   [RFC2368]                             Hoffman, P., Masinter, L., and
-                                         J. Zawinski, "The mailto URL
-                                         scheme", RFC 2368, July 1998.
-
-   [RFC2616]                             Fielding, R., Gettys, J.,
-                                         Mogul, J., Frystyk, H.,
-                                         Masinter, L., Leach, P., and T.
-                                         Berners-Lee, "Hypertext
-                                         Transfer Protocol -- HTTP/1.1",
-                                         RFC 2616, June 1999.
-
-   [RFC2617]                             Franks, J., Hallam-Baker, P.,
-                                         Hostetler, J., Lawrence, S.,
-                                         Leach, P., Luotonen, A., and L.
-                                         Stewart, "HTTP Authentication:
-                                         Basic and Digest Access
-                                         Authentication", RFC 2617,
-                                         June 1999.
-
-   [RFC2782]                             Gulbrandsen, A., Vixie, P., and
-                                         L. Esibov, "A DNS RR for
-                                         specifying the location of
-                                         services (DNS SRV)", RFC 2782,
-                                         February 2000.
-
-   [RFC2818]                             Rescorla, E., "HTTP Over TLS",
-                                         RFC 2818, May 2000.
-
-   [RFC3744]                             Clemm, G., Reschke, J., Sedlar,
-                                         E., and J. Whitehead, "Web
-                                         Distributed Authoring and
-                                         Versioning (WebDAV)
-                                         Access Control Protocol",
-
-
-
-Daboo                    Expires March 20, 2011                [Page 11]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-                                         RFC 3744, May 2004.
-
-   [RFC3986]                             Berners-Lee, T., Fielding, R.,
-                                         and L. Masinter, "Uniform
-                                         Resource Identifier (URI):
-                                         Generic Syntax", STD 66,
-                                         RFC 3986, January 2005.
-
-   [RFC4791]                             Daboo, C., Desruisseaux, B.,
-                                         and L. Dusseault, "Calendaring
-                                         Extensions to WebDAV (CalDAV)",
-                                         RFC 4791, March 2007.
-
-   [RFC4918]                             Dusseault, L., "HTTP Extensions
-                                         for Web Distributed Authoring
-                                         and Versioning (WebDAV)",
-                                         RFC 4918, June 2007.
-
-   [RFC5246]                             Dierks, T. and E. Rescorla,
-                                         "The Transport Layer Security
-                                         (TLS) Protocol Version 1.2",
-                                         RFC 5246, August 2008.
-
-   [RFC5322]                             Resnick, P., Ed., "Internet
-                                         Message Format", RFC 5322,
-                                         October 2008.
-
-   [RFC5397]                             Sanchez, W. and C. Daboo,
-                                         "WebDAV Current Principal
-                                         Extension", RFC 5397,
-                                         December 2008.
-
-   [RFC5785]                             Nottingham, M. and E. Hammer-
-                                         Lahav, "Defining Well-Known
-                                         Uniform Resource Identifiers
-                                         (URIs)", RFC 5785, April 2010.
-
-11.2.  Informative References
-
-   [RFC5545]                             Desruisseaux, B., "Internet
-                                         Calendaring and Scheduling Core
-                                         Object Specification
-                                         (iCalendar)", RFC 5545,
-                                         September 2009.
-
-URIs
-
-   [1]  <http://www.dns-sd.org/ServiceTypes.html>
-
-
-
-Daboo                    Expires March 20, 2011                [Page 12]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-Appendix A.  Change History (to be removed prior to publication as an
-             RFC)
-
-   Changes in -09:
-
-   1.  IESG Review: minor editorial changes.
-
-   2.  GenART Review: minor editorial changes.
-
-   3.  GenART Review: "guideline" -> "procedure".
-
-   4.  GenART Review: "port" -> "port number".
-
-   5.  GenART Review: added definition of "context path".
-
-   6.  GenART Review: clarified OPTIONAL nature of suggested client
-       procedure.
-
-   7.  GenART Review: clarified that TXT lookup is an additional query.
-
-   8.  IESG Review: now allow any HTTP redirect response, not just 301.
-
-   9.  IESG Review: added text on cache interaction with redirect.
-
-   Changes in -10:
-
-   1.  AD Review: make client procedure a SHOULD.
-
-   Changes in -08:
-
-   1.  Clarify that email address is a valid input in Section 7 for
-       CardDAV.
-
-   2.  Clarified aspects of DAV:current-user-principal handling for
-       servers.
-
-   3.  Added additional text to indicate TXT being used in abstract and
-       introduction.
-
-   Changes in -07:
-
-   1.  Add password to required minimal user input
-
-   2.  Section 3 -> Section 4 of server-id check draft.
-
-   Changes in -06:
-
-
-
-
-
-Daboo                    Expires March 20, 2011                [Page 13]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-   1.  Last call comments: Revised title, abstract and text to indicate
-       that SRV and .well-known can be done separately.
-
-   2.  Revised IANA section to use dns-sd registry for now.
-
-   3.  Added optional TXT RR with path key for service context path in
-       the DNS
-
-   4.  Re-organized client bootstrap to take account of TXT and to call-
-       out the different "phases" involved via a numbered list.
-
-   Changes in -05:
-
-   1.  AD Review: Added "Updates" for 4791 and CardDAV.
-
-   2.  AD Review: Changed SHOULD to MUST for honoring priority and
-       weight.
-
-   3.  AD Review: Added additional reference to 3986 when talking about
-       userinfo/host portions of the URI.
-
-   4.  AD Review: Changed section reference for tls-server-id-check
-       draft.
-
-   5.  AD Review: Changed should to SHOULD when describing PROPFIND
-       request and made 5397 normative.
-
-   6.  AD Review: Made 3744 and 5322 normative references.
-
-   7.  AD Review: Added IANA SRV registration request.
-
-   Changes in -04:
-
-   1.  Added addition text to client guidelines indicating that clients
-       cache the discovery details and can re-do discovery if
-       connections later fail.
-
-   2.  Changed principal-URI to principal-URL.
-
-   Changes in -03:
-
-   1.  Updated to RFC 5785 reference.
-
-   2.  Added SSL v2 restriction from srv-email document added after IESG
-       review.
-
-   3.  Tweaked client/server guidelines to better match HTTP challenge/
-       response authentication mechanism.
-
-
-
-Daboo                    Expires March 20, 2011                [Page 14]
-
-Internet-Draft          SRV for CalDAV & CardDAV          September 2010
-
-
-   Changes in -02:
-
-   1.  Re-organized introduction.
-
-   2.  Brought terminology into line with srv-email document which has
-       been through last call.
-
-   3.  Brought security section into line with srv-email document which
-       has been through last call.
-
-   Changes in -01:
-
-   1.  Added discovery of CardDAV service.
-
-   2.  Now makes use of well-known URIs for the service "context path".
-
-   3.  Updated to RFC 5545 reference.
-
-   4.  Added reference to certificate verification spec.
-
-Author's Address
-
-   Cyrus Daboo
-   Apple Inc.
-   1 Infinite Loop
-   Cupertino, CA  95014
-   USA
-
-   EMail: cyrus at daboo.name
-   URI:   http://www.apple.com/
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Daboo                    Expires March 20, 2011                [Page 15]
-

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/doc/calendarserver_config.8 (from rev 11028, CalendarServer/trunk/doc/calendarserver_config.8)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/doc/calendarserver_config.8	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/doc/calendarserver_config.8	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,59 @@
+.\"
+.\" Copyright (c) 2006-2013 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.
+.\"
+.\" The following requests are required for all man pages.
+.Dd January 14, 2013
+.Dt CALENDARSERVER_CONFIG 8
+.Os
+.Sh NAME
+.Nm calendarserver_config
+.Nd Calendar Server Configuration Utility
+.Sh SYNOPSIS
+.Nm
+.Op Fl -config Ar file
+.Op key ...
+.Sh DESCRIPTION
+.Nm
+stores and retrieves configuration values for calendar server.  It's primary
+purpose is to carry out operations on behalf of the Apple OS X Server
+administration application; in this mode of operation is reads stdin for
+keys provided in plist format, and writes results to stdout, also in plist
+form.  For interactive use, you can list one or more keys as command line
+arguments.
+.Pp
+.Sh OPTIONS
+.Bl -tag -width flag
+.It Fl h, -help
+Displays usage information
+.It Fl f, -config Ar FILE
+Use the Calendar Server configuration specified in the given file.
+Defaults to /Applications/Server.app/Contents/ServerRoot/private/etc/caldavd/caldavd-apple.plist on Apple servers, /etc/caldavd/caldavd.plist on other servers.
+.El
+.Sh EXAMPLES
+Retrieve the value for EnableCalDAV
+.Pp
+.Dl "calendarserver_config EnableCalDAV"
+.Pp
+Set EnableCalDAV to True
+.Pp
+.Dl "calendarserver_config EnableCalDAV=True"
+.Pp
+.Sh FILES
+.Bl -tag -width flag
+.It /Applications/Server.app/Contents/ServerRoot/private/etc/caldavd/caldavd-apple.plist
+The static Calendar Server configuration file (not to be edited).
+.It /Library/Server/Calendar and Contacts/Config/caldavd-system.plist
+The configuration file which stores local overriding values (which calendarserver_config modifies).
+.El

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/run
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/run	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/run	2013-04-11 16:24:18 UTC (rev 11029)
@@ -190,20 +190,15 @@
 
   # If we've been asked to read a configuration key, just read it and exit.
   if [ -n "${read_key}" ]; then
-    "${caldav}/bin/calendarserver_config" "${read_key}";
+    value="$("${caldav}/bin/calendarserver_config" "${read_key}")";
+    IFS="="; set ${value}; echo "$2"; unset IFS;
     exit $?;
   fi;
 
   if "${kill}" || "${restart}"; then
-    # mimic logic of 'fullServerPath' from twistedcaldav/config.py to find the pid file
     pidfile="$("${caldav}/bin/calendarserver_config" "PIDFile")";
-    serverroot="$("${caldav}/bin/calendarserver_config" "ServerRoot")";
-    runroot="$("${caldav}/bin/calendarserver_config" "RunRoot")";
-    # examine first character of $pidfile
-    if ( [ "${pidfile:0:1}" == "/" ] || [ "${pidfile:0:1}" == "." ]; ) then
-        pidfile=$pidfile;
-        else pidfile=${serverroot}/${runroot}/${pidfile};
-    fi
+    # Split key and value on "=" and just grab the value
+    IFS="="; set ${pidfile}; pidfile="$2"; unset IFS;
     if [ ! -r "${pidfile}" ]; then
       echo "Unreadable PID file: ${pidfile}";
       exit 1

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/setup.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/setup.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/setup.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -83,11 +83,13 @@
     extensions.append(
         Extension(
             "calendarserver.platform.darwin._sacl",
-            extra_link_args = ["-framework", "Security"],
-            sources = ["calendarserver/platform/darwin/_sacl.c"]
+            extra_link_args=["-framework", "Security"],
+            sources=["calendarserver/platform/darwin/_sacl.c"]
         )
     )
 
+
+
 #
 # Run setup
 #
@@ -96,18 +98,18 @@
     from distutils.core import setup
 
     dist = setup(
-        name             = "Calendar and Contacts Server",
-        version          = version_string,
-        description      = description,
-        long_description = long_description,
-        url              = None,
-        classifiers      = classifiers,
-        author           = "Apple Inc.",
-        author_email     = None,
-        license          = None,
-        platforms        = ["all"],
-        packages         = find_modules(),
-        package_data     = {
+        name="Calendar and Contacts Server",
+        version=version_string,
+        description=description,
+        long_description=long_description,
+        url=None,
+        classifiers=classifiers,
+        author="Apple Inc.",
+        author_email=None,
+        license=None,
+        platforms=["all"],
+        packages=find_modules(),
+        package_data={
                              "twistedcaldav": [
                                "*.html",
                                "zoneinfo/*.ics",
@@ -127,12 +129,12 @@
                                "sql_schema/*/*/*.sql",
                              ],
                            },
-        scripts          = [
+        scripts=[
                              "bin/caldavd",
                              "bin/calendarserver_backup",
                              "bin/calendarserver_bootstrap_database",
                              "bin/calendarserver_command_gateway",
-                            #"bin/calendarserver_config", # Used by run script.
+                             "bin/calendarserver_config",
                             #"bin/calendarserver_dbinspect",
                             #"bin/calendarserver_dkimtool",
                              "bin/calendarserver_export",
@@ -153,9 +155,9 @@
                             #"bin/calendarserver_upgrade",
                             #"bin/calendarserver_verify_data",
                            ],
-        data_files       = [ ("caldavd", ["conf/caldavd.plist"]), ],
-        ext_modules      = extensions,
-        py_modules       = [],
+        data_files=[("caldavd", ["conf/caldavd.plist"]), ],
+        ext_modules=extensions,
+        py_modules=[],
     )
 
     if "install" in dist.commands:
@@ -215,5 +217,3 @@
 
 if __name__ == "__main__":
     doSetup()
-
-

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/support/Apple.make (from rev 11028, CalendarServer/trunk/support/Apple.make)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/support/Apple.make	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/support/Apple.make	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,170 @@
+# -*- mode: Makefile; -*-
+##
+# B&I Makefile for CalendarServer
+#
+# This is only useful internally at Apple, probably.
+##
+# Copyright (c) 2005-2013 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.
+##
+
+# Project info
+Project	    = CalendarServer
+ProjectName = CalendarServer
+UserType    = Server
+ToolType    = Applications
+
+# Include common makefile targets for B&I
+include $(MAKEFILEPATH)/CoreOS/ReleaseControl/Common.make
+include /AppleInternal/ServerTools/ServerBuildVariables.xcconfig
+
+SIPP = $(SERVER_INSTALL_PATH_PREFIX)
+SERVERSETUP = $(SIPP)$(NSSYSTEMDIR)$(NSLIBRARYSUBDIR)/ServerSetup
+
+Cruft += .dependencies
+Extra_Environment += PATH="$(SIPP)/usr/bin:$$PATH"
+
+CALDAVDSUBDIR = /caldavd
+
+PYTHON = $(USRBINDIR)/python
+PY_HOME = $(SIPP)$(SHAREDIR)$(CALDAVDSUBDIR)
+PY_INSTALL_FLAGS = --root="$(DSTROOT)" --prefix="$(SIPP)" --install-lib="$(PY_HOME)/lib/python" --install-scripts="$(SIPP)$(LIBEXECDIR)$(CALDAVDSUBDIR)"
+CS_INSTALL_FLAGS = --install-scripts="$(SIPP)$(USRSBINDIR)" --install-data="$(SIPP)$(ETCDIR)"
+CS_BUILD_EXT_FLAGS = --include-dirs="$(SIPP)/usr/include" --library-dirs="$(SIPP)/usr/lib"
+
+CS_USER  = _calendar
+CS_GROUP = _calendar
+
+#
+# Build
+#
+
+.phony: $(Project) pycalendar build setup prep install install-ossfiles buildit
+
+CalDAVTester::          $(BuildDirectory)/CalDAVTester
+PyKerberos::            $(BuildDirectory)/PyKerberos
+pycalendar::            $(BuildDirectory)/pycalendar
+PyGreSQL-4.0::          $(BuildDirectory)/PyGreSQL-4.0
+sqlparse-0.1.2::        $(BuildDirectory)/sqlparse-0.1.2
+setproctitle-1.1.6::	$(BuildDirectory)/setproctitle-1.1.6
+psutil-0.6.1::		$(BuildDirectory)/psutil-0.6.1
+pycrypto-2.5::		$(BuildDirectory)/pycrypto-2.5
+$(Project)::            $(BuildDirectory)/$(Project)
+
+build:: PyKerberos pycalendar PyGreSQL-4.0 sqlparse-0.1.2 setproctitle-1.1.6 psutil-0.6.1 pycrypto-2.5 $(Project)
+
+setup:
+	$(_v) ./run -g
+
+prep:: setup CalDAVTester.tgz PyKerberos.tgz pycalendar.tgz PyGreSQL-4.0.tgz sqlparse-0.1.2.tgz setproctitle-1.1.6.tgz psutil-0.6.1.tgz pycrypto-2.5.tgz
+
+PyKerberos pycalendar PyGreSQL-4.0 sqlparse-0.1.2 setproctitle-1.1.6 psutil-0.6.1 pycrypto-2.5 $(Project)::
+	@echo "Building $@..."
+	$(_v) cd $(BuildDirectory)/$@ && $(Environment) $(PYTHON) setup.py build
+
+install:: build
+	$(_v) cd $(BuildDirectory)/$(Project)         && $(Environment) $(PYTHON) setup.py build_ext $(CS_BUILD_EXT_FLAGS) install $(PY_INSTALL_FLAGS) $(CS_INSTALL_FLAGS)
+	$(_v) cd $(BuildDirectory)/PyKerberos         && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
+	$(_v) cd $(BuildDirectory)/pycalendar         && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
+	$(_v) cd $(BuildDirectory)/PyGreSQL-4.0       && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
+	$(_v) cd $(BuildDirectory)/sqlparse-0.1.2     && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
+	$(_v) cd $(BuildDirectory)/setproctitle-1.1.6 && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
+	$(_v) cd $(BuildDirectory)/psutil-0.6.1       && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
+	$(_v) cd $(BuildDirectory)/pycrypto-2.5       && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
+	$(_v) for so in $$(find "$(DSTROOT)$(PY_HOME)/lib" -type f -name '*.so'); do $(STRIP) -Sx "$${so}"; done 
+	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(ETCDIR)$(CALDAVDSUBDIR)"
+	$(_v) $(INSTALL_FILE) "$(Sources)/conf/caldavd-apple.plist" "$(DSTROOT)$(SIPP)$(ETCDIR)$(CALDAVDSUBDIR)/caldavd-apple.plist"
+	$(_v) chmod -R ugo+r "$(DSTROOT)$(PY_HOME)"
+	$(_v) for f in $$(find "$(DSTROOT)$(SIPP)$(ETCDIR)" -type f ! -name '*.default'); do cp "$${f}" "$${f}.default"; done
+
+install::
+	@echo "Installing manual pages..."
+	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/caldavd.8"                              "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_bootstrap_database.8"    "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_command_gateway.8"       "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_export.8"                "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_manage_principals.8"     "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_migrate_resources.8"     "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_purge_attachments.8"     "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_purge_events.8"          "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_purge_principals.8"      "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_manage_push.8"           "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_shell.8"                 "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
+	$(_v) gzip -9 -f "$(DSTROOT)$(SIPP)$(MANDIR)/man8/"*.[0-9]
+	@echo "Installing launchd config..."
+	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(NSLOCALDIR)/$(NSLIBRARYSUBDIR)/Server/Calendar and Contacts"
+	$(_v) $(INSTALL_DIRECTORY) -o "$(CS_USER)" -g "$(CS_GROUP)" -m 0755 "$(DSTROOT)$(VARDIR)/log$(CALDAVDSUBDIR)"
+	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(NSLIBRARYDIR)/LaunchDaemons"
+	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/launchd/calendarserver.plist" "$(DSTROOT)$(SIPP)$(NSLIBRARYDIR)/LaunchDaemons/org.calendarserver.calendarserver.plist"
+	@echo "Installing changeip script..."
+	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/changeip"
+	$(_v) $(INSTALL_FILE) "$(Sources)/calendarserver/tools/changeip_calendar.py" "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/changeip/changeip_calendar.py"
+	$(_v) chmod ugo+x "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/changeip/changeip_calendar.py"
+
+install::
+	@echo "Installing CalDAVTester package..."
+	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)/AppleInternal/ServerTools"
+	$(_v) $(INSTALL_FILE) "$(Sources)/CalDAVTester.tgz" "$(DSTROOT)/AppleInternal/ServerTools/CalDAVTester.tgz"
+
+#
+# Automatic Extract
+#
+
+$(BuildDirectory)/$(Project):
+	@echo "Copying source for $(Project)..."
+	$(_v) $(MKDIR) -p "$@"
+	$(_v) pax -rw bin conf Makefile lib-patches setup.py calendarserver twistedcaldav twext txdav twisted support "$@/"
+
+$(BuildDirectory)/%: %.tgz
+	@echo "Extracting source for $(notdir $<)..."
+	$(_v) $(MKDIR) -p "$(BuildDirectory)"
+	$(_v) $(RMDIR) "$@"
+	$(_v) $(TAR) -C "$(BuildDirectory)" -xzf "$<"
+
+%.tgz: ../%
+	@echo "Archiving sources for $(notdir $<)..."
+	$(_v) if [ -f "$</setup.py" ] && grep setuptools "$</setup.py" > /dev/null; then \
+	        echo "Working around setuptools' stupid need to download a new version."; \
+	        cd "$<" && $(PYTHON) "setup.py" --help >/dev/null; \
+	      fi
+	$(_v) $(TAR) -C "$(dir $<)"        \
+	          --exclude=.svn           \
+	          --exclude=build          \
+	          --exclude=_trial_temp    \
+	          --exclude=dropin.cache   \
+	          -czf "$@" "$(notdir $<)"
+
+#
+# Open Source Hooey
+#
+
+OSV = $(USRDIR)/local/OpenSourceVersions
+OSL = $(USRDIR)/local/OpenSourceLicenses
+
+#install:: install-ossfiles
+
+install-ossfiles::
+	$(_v) $(INSTALL_DIRECTORY) $(DSTROOT)/$(OSV)
+	$(_v) $(INSTALL_FILE) $(Sources)/$(ProjectName).plist $(DSTROOT)/$(OSV)/$(ProjectName).plist
+	$(_v) $(INSTALL_DIRECTORY) $(DSTROOT)/$(OSL)
+	$(_v) $(INSTALL_FILE) $(BuildDirectory)/$(Project)/LICENSE $(DSTROOT)/$(OSL)/$(ProjectName).txt
+
+#
+# B&I Hooey
+#
+
+buildit: prep
+	@echo "Running buildit..."
+	$(_v) sudo ~rc/bin/buildit $(CC_Archs) $(Sources)

Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.tmproj
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.tmproj	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.tmproj	2013-04-11 16:24:18 UTC (rev 11029)
@@ -1,122 +0,0 @@
-<?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>documents</key>
-	<array>
-		<dict>
-			<key>expanded</key>
-			<true/>
-			<key>name</key>
-			<string>twistedcaldav</string>
-			<key>regexFolderFilter</key>
-			<string>!.*/(\.[^/]*)$</string>
-			<key>sourceDirectory</key>
-			<string>../twistedcaldav</string>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>web2</string>
-			<key>regexFolderFilter</key>
-			<string>!.*/(\.[^/]*)$</string>
-			<key>sourceDirectory</key>
-			<string>../../Twisted/twisted/web2</string>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>twisted</string>
-			<key>regexFolderFilter</key>
-			<string>!.*/(\.[^/]*)$</string>
-			<key>sourceDirectory</key>
-			<string>../../Twisted/twisted</string>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>PyOpenDirectory</string>
-			<key>regexFolderFilter</key>
-			<string>!.*/(\.[^/]*)$</string>
-			<key>sourceDirectory</key>
-			<string>../../PyOpenDirectory</string>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>PyKerberos</string>
-			<key>regexFolderFilter</key>
-			<string>!.*/(\.[^/]*)$</string>
-			<key>sourceDirectory</key>
-			<string>../../PyKerberos</string>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>conf</string>
-			<key>regexFolderFilter</key>
-			<string>!.*/(\.[^/]*)$</string>
-			<key>sourceDirectory</key>
-			<string>../conf</string>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>bin</string>
-			<key>regexFolderFilter</key>
-			<string>!.*/(\.[^/]*)$</string>
-			<key>sourceDirectory</key>
-			<string>../bin</string>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>doc</string>
-			<key>regexFolderFilter</key>
-			<string>!.*/(\.[^/]*)$</string>
-			<key>sourceDirectory</key>
-			<string>../doc</string>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>support</string>
-			<key>regexFolderFilter</key>
-			<string>!.*/(\.[^/]*|.*\.xcodeproj)$</string>
-			<key>sourceDirectory</key>
-			<string></string>
-		</dict>
-		<dict>
-			<key>children</key>
-			<array>
-				<dict>
-					<key>filename</key>
-					<string>../run</string>
-				</dict>
-				<dict>
-					<key>filename</key>
-					<string>../test</string>
-				</dict>
-				<dict>
-					<key>filename</key>
-					<string>../testcaldav</string>
-				</dict>
-				<dict>
-					<key>filename</key>
-					<string>../setup.py</string>
-				</dict>
-				<dict>
-					<key>filename</key>
-					<string>../LICENSE</string>
-				</dict>
-				<dict>
-					<key>filename</key>
-					<string>../README</string>
-				</dict>
-			</array>
-			<key>name</key>
-			<string>topfiles</string>
-		</dict>
-	</array>
-	<key>fileHierarchyDrawerWidth</key>
-	<integer>249</integer>
-	<key>metaData</key>
-	<dict/>
-	<key>showFileHierarchyDrawer</key>
-	<true/>
-	<key>windowFrame</key>
-	<string>{{414, 114}, {987, 1030}}</string>
-</dict>
-</plist>


Property changes on: CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj
___________________________________________________________________
Added: svn:ignore
   + xcuserdata


Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.pbxproj
===================================================================
--- CalendarServer/trunk/support/CalendarServer.xcodeproj/project.pbxproj	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.pbxproj	2013-04-11 16:24:18 UTC (rev 11029)
@@ -1,112 +0,0 @@
-// !$*UTF8*$!
-{
-	archiveVersion = 1;
-	classes = {
-	};
-	objectVersion = 46;
-	objects = {
-
-/* Begin PBXFileReference section */
-		3519AFAB170E546B0054F087 /* CalendarServer */ = {isa = PBXFileReference; lastKnownFileType = folder; name = CalendarServer; path = ..; sourceTree = "<group>"; };
-/* End PBXFileReference section */
-
-/* Begin PBXGroup section */
-		35E050F7170E534C00D4A7CA = {
-			isa = PBXGroup;
-			children = (
-				3519AFAB170E546B0054F087 /* CalendarServer */,
-			);
-			sourceTree = "<group>";
-		};
-/* End PBXGroup section */
-
-/* Begin PBXLegacyTarget section */
-		35E050FC170E534C00D4A7CA /* CalendarServer */ = {
-			isa = PBXLegacyTarget;
-			buildArgumentsString = "-f XCode.make $(ACTION)";
-			buildConfigurationList = 35E050FF170E534C00D4A7CA /* Build configuration list for PBXLegacyTarget "CalendarServer" */;
-			buildPhases = (
-			);
-			buildToolPath = /usr/bin/make;
-			buildWorkingDirectory = "";
-			dependencies = (
-			);
-			name = CalendarServer;
-			passBuildSettingsInEnvironment = 0;
-			productName = CalendarServer;
-		};
-/* End PBXLegacyTarget section */
-
-/* Begin PBXProject section */
-		35E050F8170E534C00D4A7CA /* Project object */ = {
-			isa = PBXProject;
-			attributes = {
-				LastUpgradeCheck = 0460;
-				ORGANIZATIONNAME = "Mac OS Forge";
-			};
-			buildConfigurationList = 35E050FB170E534C00D4A7CA /* Build configuration list for PBXProject "CalendarServer" */;
-			compatibilityVersion = "Xcode 3.2";
-			developmentRegion = English;
-			hasScannedForEncodings = 0;
-			knownRegions = (
-				en,
-			);
-			mainGroup = 35E050F7170E534C00D4A7CA;
-			projectDirPath = "";
-			projectRoot = "";
-			targets = (
-				35E050FC170E534C00D4A7CA /* CalendarServer */,
-			);
-		};
-/* End PBXProject section */
-
-/* Begin XCBuildConfiguration section */
-		35E050FD170E534C00D4A7CA /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-			};
-			name = Debug;
-		};
-		35E050FE170E534C00D4A7CA /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-			};
-			name = Release;
-		};
-		35E05100170E534C00D4A7CA /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-			};
-			name = Debug;
-		};
-		35E05101170E534C00D4A7CA /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-			};
-			name = Release;
-		};
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
-		35E050FB170E534C00D4A7CA /* Build configuration list for PBXProject "CalendarServer" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				35E050FD170E534C00D4A7CA /* Debug */,
-				35E050FE170E534C00D4A7CA /* Release */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-		35E050FF170E534C00D4A7CA /* Build configuration list for PBXLegacyTarget "CalendarServer" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				35E05100170E534C00D4A7CA /* Debug */,
-				35E05101170E534C00D4A7CA /* Release */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-/* End XCConfigurationList section */
-	};
-	rootObject = 35E050F8170E534C00D4A7CA /* Project object */;
-}

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.pbxproj (from rev 11028, CalendarServer/trunk/support/CalendarServer.xcodeproj/project.pbxproj)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.pbxproj	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.pbxproj	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,112 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXFileReference section */
+		3519AFAB170E546B0054F087 /* CalendarServer */ = {isa = PBXFileReference; lastKnownFileType = folder; name = CalendarServer; path = ..; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXGroup section */
+		35E050F7170E534C00D4A7CA = {
+			isa = PBXGroup;
+			children = (
+				3519AFAB170E546B0054F087 /* CalendarServer */,
+			);
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXLegacyTarget section */
+		35E050FC170E534C00D4A7CA /* CalendarServer */ = {
+			isa = PBXLegacyTarget;
+			buildArgumentsString = "-f XCode.make $(ACTION)";
+			buildConfigurationList = 35E050FF170E534C00D4A7CA /* Build configuration list for PBXLegacyTarget "CalendarServer" */;
+			buildPhases = (
+			);
+			buildToolPath = /usr/bin/make;
+			buildWorkingDirectory = "";
+			dependencies = (
+			);
+			name = CalendarServer;
+			passBuildSettingsInEnvironment = 0;
+			productName = CalendarServer;
+		};
+/* End PBXLegacyTarget section */
+
+/* Begin PBXProject section */
+		35E050F8170E534C00D4A7CA /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 0460;
+				ORGANIZATIONNAME = "Mac OS Forge";
+			};
+			buildConfigurationList = 35E050FB170E534C00D4A7CA /* Build configuration list for PBXProject "CalendarServer" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+			);
+			mainGroup = 35E050F7170E534C00D4A7CA;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				35E050FC170E534C00D4A7CA /* CalendarServer */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin XCBuildConfiguration section */
+		35E050FD170E534C00D4A7CA /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+			};
+			name = Debug;
+		};
+		35E050FE170E534C00D4A7CA /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+			};
+			name = Release;
+		};
+		35E05100170E534C00D4A7CA /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+			};
+			name = Debug;
+		};
+		35E05101170E534C00D4A7CA /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		35E050FB170E534C00D4A7CA /* Build configuration list for PBXProject "CalendarServer" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				35E050FD170E534C00D4A7CA /* Debug */,
+				35E050FE170E534C00D4A7CA /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		35E050FF170E534C00D4A7CA /* Build configuration list for PBXLegacyTarget "CalendarServer" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				35E05100170E534C00D4A7CA /* Debug */,
+				35E05101170E534C00D4A7CA /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 35E050F8170E534C00D4A7CA /* Project object */;
+}


Property changes on: CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.xcworkspace
___________________________________________________________________
Added: svn:ignore
   + xcuserdata


Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.xcworkspace/contents.xcworkspacedata
===================================================================
--- CalendarServer/trunk/support/CalendarServer.xcodeproj/project.xcworkspace/contents.xcworkspacedata	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.xcworkspace/contents.xcworkspacedata	2013-04-11 16:24:18 UTC (rev 11029)
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Workspace
-   version = "1.0">
-   <FileRef
-      location = "self:CalendarServer.xcodeproj">
-   </FileRef>
-</Workspace>

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.xcworkspace/contents.xcworkspacedata (from rev 11028, CalendarServer/trunk/support/CalendarServer.xcodeproj/project.xcworkspace/contents.xcworkspacedata)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.xcworkspace/contents.xcworkspacedata	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/support/CalendarServer.xcodeproj/project.xcworkspace/contents.xcworkspacedata	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:CalendarServer.xcodeproj">
+   </FileRef>
+</Workspace>

Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/support/Makefile.Apple
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/support/Makefile.Apple	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/support/Makefile.Apple	2013-04-11 16:24:18 UTC (rev 11029)
@@ -1,194 +0,0 @@
-# -*- mode: Makefile; -*-
-##
-# B&I Makefile for CalendarServer
-#
-# This is only useful internally at Apple, probably.
-##
-# Copyright (c) 2005-2013 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.
-##
-
-# Project info
-Project	    = CalendarServer
-ProjectName = CalendarServer
-UserType    = Server
-ToolType    = Applications
-
-# Include common makefile targets for B&I
-include $(MAKEFILEPATH)/CoreOS/ReleaseControl/Common.make
-include /AppleInternal/ServerTools/ServerBuildVariables.xcconfig
-
-SIPP = $(SERVER_INSTALL_PATH_PREFIX)
-SERVERSETUP = $(SIPP)$(NSSYSTEMDIR)$(NSLIBRARYSUBDIR)/ServerSetup
-
-Cruft += .dependencies
-Extra_Environment += PATH="$(SIPP)/usr/bin:$$PATH"
-
-CALDAVDSUBDIR = /caldavd
-WEBAPPSSUBDIR = /apache2/webapps
-
-PYTHON = $(USRBINDIR)/python
-PY_HOME = $(SIPP)$(SHAREDIR)$(CALDAVDSUBDIR)
-PY_INSTALL_FLAGS = --root="$(DSTROOT)" --prefix="$(SIPP)" --install-lib="$(PY_HOME)/lib/python" --install-scripts="$(SIPP)$(LIBEXECDIR)$(CALDAVDSUBDIR)"
-CS_INSTALL_FLAGS = --install-scripts="$(SIPP)$(USRSBINDIR)" --install-data="$(SIPP)$(ETCDIR)"
-CS_BUILD_EXT_FLAGS = --include-dirs="$(SIPP)/usr/include" --library-dirs="$(SIPP)/usr/lib"
-
-CS_USER  = _calendar
-CS_GROUP = _calendar
-
-#
-# Build
-#
-
-.phony: $(Project) pycalendar build setup prep install install-ossfiles buildit
-
-CalDAVTester::          $(BuildDirectory)/CalDAVTester
-PyKerberos::            $(BuildDirectory)/PyKerberos
-pycalendar::            $(BuildDirectory)/pycalendar
-PyGreSQL-4.0::          $(BuildDirectory)/PyGreSQL-4.0
-sqlparse-0.1.2::        $(BuildDirectory)/sqlparse-0.1.2
-setproctitle-1.1.6::	$(BuildDirectory)/setproctitle-1.1.6
-psutil-0.6.1::		$(BuildDirectory)/psutil-0.6.1
-pycrypto-2.5::		$(BuildDirectory)/pycrypto-2.5
-$(Project)::            $(BuildDirectory)/$(Project)
-
-build:: PyKerberos pycalendar PyGreSQL-4.0 sqlparse-0.1.2 setproctitle-1.1.6 psutil-0.6.1 pycrypto-2.5 $(Project)
-
-setup:
-	$(_v) ./run -g
-
-prep:: setup CalDAVTester.tgz PyKerberos.tgz pycalendar.tgz PyGreSQL-4.0.tgz sqlparse-0.1.2.tgz setproctitle-1.1.6.tgz psutil-0.6.1.tgz pycrypto-2.5.tgz
-
-PyKerberos pycalendar PyGreSQL-4.0 sqlparse-0.1.2 setproctitle-1.1.6 psutil-0.6.1 pycrypto-2.5 $(Project)::
-	@echo "Building $@..."
-	$(_v) cd $(BuildDirectory)/$@ && $(Environment) $(PYTHON) setup.py build
-
-install:: build
-	$(_v) cd $(BuildDirectory)/$(Project)         && $(Environment) $(PYTHON) setup.py build_ext $(CS_BUILD_EXT_FLAGS) install $(PY_INSTALL_FLAGS) $(CS_INSTALL_FLAGS)
-	$(_v) cd $(BuildDirectory)/PyKerberos         && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
-	$(_v) cd $(BuildDirectory)/pycalendar         && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
-	$(_v) cd $(BuildDirectory)/PyGreSQL-4.0       && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
-	$(_v) cd $(BuildDirectory)/sqlparse-0.1.2     && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
-	$(_v) cd $(BuildDirectory)/setproctitle-1.1.6 && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
-	$(_v) cd $(BuildDirectory)/psutil-0.6.1       && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
-	$(_v) cd $(BuildDirectory)/pycrypto-2.5       && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
-	$(_v) for so in $$(find "$(DSTROOT)$(PY_HOME)/lib" -type f -name '*.so'); do $(STRIP) -Sx "$${so}"; done 
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(ETCDIR)$(CALDAVDSUBDIR)"
-	$(_v) $(INSTALL_FILE) "$(Sources)/conf/caldavd-apple.plist" "$(DSTROOT)$(SIPP)$(ETCDIR)$(CALDAVDSUBDIR)/caldavd.plist"
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(ETCDIR)$(WEBAPPSSUBDIR)"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/com.apple.webapp.contacts.plist" "$(DSTROOT)$(SIPP)$(ETCDIR)$(WEBAPPSSUBDIR)/com.apple.webapp.contacts.plist"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/com.apple.webapp.contactsssl.plist" "$(DSTROOT)$(SIPP)$(ETCDIR)$(WEBAPPSSUBDIR)/com.apple.webapp.contactsssl.plist"
-	$(_v) chmod -R ugo+r "$(DSTROOT)$(PY_HOME)"
-	$(_v) for f in $$(find "$(DSTROOT)$(SIPP)$(ETCDIR)" -type f ! -name '*.default'); do cp "$${f}" "$${f}.default"; done
-
-install::
-	@echo "Installing manual pages..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/caldavd.8"                              "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_bootstrap_database.8"    "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_command_gateway.8"       "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_export.8"                "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_manage_principals.8"     "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_migrate_resources.8"     "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_purge_attachments.8"     "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_purge_events.8"          "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_purge_principals.8"      "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_manage_push.8"           "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_shell.8"                 "$(DSTROOT)$(SIPP)$(MANDIR)/man8"
-	$(_v) gzip -9 -f "$(DSTROOT)$(SIPP)$(MANDIR)/man8/"*.[0-9]
-	@echo "Installing launchd config..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(NSLOCALDIR)/$(NSLIBRARYSUBDIR)/Server/Calendar and Contacts"
-	$(_v) $(INSTALL_DIRECTORY) -o "$(CS_USER)" -g "$(CS_GROUP)" -m 0755 "$(DSTROOT)$(VARDIR)/log$(CALDAVDSUBDIR)"
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(NSLIBRARYDIR)/LaunchDaemons"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/launchd/calendarserver.plist" "$(DSTROOT)$(SIPP)$(NSLIBRARYDIR)/LaunchDaemons/org.calendarserver.calendarserver.plist"
-	@echo "Installing migration extras script..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SERVERSETUP)/MigrationExtras"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/calendarmigrator.py" "$(DSTROOT)$(SERVERSETUP)/MigrationExtras/70_calendarmigrator.py"
-	$(_v) chmod ugo+x "$(DSTROOT)$(SERVERSETUP)/MigrationExtras/70_calendarmigrator.py"
-	@echo "Installing common extras script..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SERVERSETUP)/CommonExtras"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/calendarcommonextra.py" "$(DSTROOT)$(SERVERSETUP)/CommonExtras/70_calendarcommonextra.py"
-	$(_v) chmod ugo+x "$(DSTROOT)$(SERVERSETUP)/CommonExtras/70_calendarcommonextra.py"
-	@echo "Installing server promotion extras script..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SERVERSETUP)/PromotionExtras"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/calendarpromotion.py" "$(DSTROOT)$(SERVERSETUP)/PromotionExtras/59_calendarpromotion.py"
-	$(_v) chmod ugo+x "$(DSTROOT)$(SERVERSETUP)/PromotionExtras/59_calendarpromotion.py"
-	@echo "Installing server uninstall extras script..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SERVERSETUP)/UninstallExtras"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/calendardemotion.py" "$(DSTROOT)$(SERVERSETUP)/UninstallExtras/59_calendardemotion.py"
-	$(_v) chmod ugo+x "$(DSTROOT)$(SERVERSETUP)/UninstallExtras/59_calendardemotion.py"
-	@echo "Installing changeip script..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/changeip"
-	$(_v) $(INSTALL_FILE) "$(Sources)/calendarserver/tools/changeip_calendar.py" "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/changeip/changeip_calendar.py"
-	$(_v) chmod ugo+x "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/changeip/changeip_calendar.py"
-	@echo "Installing certificate update scripts..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/certupdate"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/certupdate/calendarcertupdate.py" "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/certupdate/calendarcertupdate.py"
-	$(_v) chmod ugo+x "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/certupdate/calendarcertupdate.py"
-
-install::
-	@echo "Installing CalDAVTester package..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)/AppleInternal/ServerTools"
-	$(_v) $(INSTALL_FILE) "$(Sources)/CalDAVTester.tgz" "$(DSTROOT)/AppleInternal/ServerTools/CalDAVTester.tgz"
-
-#
-# Automatic Extract
-#
-
-$(BuildDirectory)/$(Project):
-	@echo "Copying source for $(Project)..."
-	$(_v) $(MKDIR) -p "$@"
-	$(_v) pax -rw bin conf Makefile lib-patches setup.py calendarserver twistedcaldav twext txdav twisted support "$@/"
-
-$(BuildDirectory)/%: %.tgz
-	@echo "Extracting source for $(notdir $<)..."
-	$(_v) $(MKDIR) -p "$(BuildDirectory)"
-	$(_v) $(RMDIR) "$@"
-	$(_v) $(TAR) -C "$(BuildDirectory)" -xzf "$<"
-
-%.tgz: ../%
-	@echo "Archiving sources for $(notdir $<)..."
-	$(_v) if [ -f "$</setup.py" ] && grep setuptools "$</setup.py" > /dev/null; then \
-	        echo "Working around setuptools' stupid need to download a new version."; \
-	        cd "$<" && $(PYTHON) "setup.py" --help >/dev/null; \
-	      fi
-	$(_v) $(TAR) -C "$(dir $<)"        \
-	          --exclude=.svn           \
-	          --exclude=build          \
-	          --exclude=_trial_temp    \
-	          --exclude=dropin.cache   \
-	          -czf "$@" "$(notdir $<)"
-
-#
-# Open Source Hooey
-#
-
-OSV = $(USRDIR)/local/OpenSourceVersions
-OSL = $(USRDIR)/local/OpenSourceLicenses
-
-#install:: install-ossfiles
-
-install-ossfiles::
-	$(_v) $(INSTALL_DIRECTORY) $(DSTROOT)/$(OSV)
-	$(_v) $(INSTALL_FILE) $(Sources)/$(ProjectName).plist $(DSTROOT)/$(OSV)/$(ProjectName).plist
-	$(_v) $(INSTALL_DIRECTORY) $(DSTROOT)/$(OSL)
-	$(_v) $(INSTALL_FILE) $(BuildDirectory)/$(Project)/LICENSE $(DSTROOT)/$(OSL)/$(ProjectName).txt
-
-#
-# B&I Hooey
-#
-
-buildit: prep
-	@echo "Running buildit..."
-	$(_v) sudo ~rc/bin/buildit $(CC_Archs) $(Sources)

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/support/XCode.make (from rev 11028, CalendarServer/trunk/support/XCode.make)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/support/XCode.make	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/support/XCode.make	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,34 @@
+# -*- mode: Makefile; -*-
+##
+# XCode Makefile for CalendarServer
+##
+# Copyright (c) 2005-2013 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.
+##
+
+default: build
+
+#
+# Run sed to suppress XCode's misguided attempts to interpret our output.
+#
+build::
+	../run -s 2>&1 \
+	  | sed \
+	    -e 's|error|oops|' \
+	    -e 's|warning|oopsie|' \
+	    -e 's|^\(..*\):\([0-9][0-9]*\):\([0-9][0-9]*\): |\1-\2-\3: |';
+
+clean::
+	rm -rf ../.dependencies/ ../build/ ../data/ ../calendarserver/version.py;
+	find .. -name '*.pyc' -o -name '*.so' -o -name dropin.cache -print0 | xargs -0 rm -f;

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/support/build.sh
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/support/build.sh	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/support/build.sh	2013-04-11 16:24:18 UTC (rev 11029)
@@ -552,7 +552,7 @@
       ;;
     FreeBSD)
       ncpu="$(sysctl hw.ncpu)";
-      ncpu="${cpu##hw.ncpu: }";
+      ncpu="${ncpu##hw.ncpu: }";
       ;;
   esac;
 
@@ -775,7 +775,7 @@
     "${pypi}/p/python-ldap/${ld}.tar.gz";
 
   # XXX actually PyCalendar should be imported in-place.
-  py_dependency -fe -i "src" -r 10554 \
+  py_dependency -fe -i "src" -r 11005 \
     "pycalendar" "pycalendar" "pycalendar" \
     "${svn_uri_base}/PyCalendar/trunk";
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/support/submit
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/support/submit	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/support/submit	2013-04-11 16:24:18 UTC (rev 11029)
@@ -144,7 +144,7 @@
 
 echo ""
 echo "Tweaking for B&I...";
-ln -s support/Makefile.Apple "${wc}/Makefile";
+ln -s support/Apple.make "${wc}/Makefile";
 
 version_file="${wc}/SubmissionInfo.xml";
 cat - >> "${version_file}" <<EOF

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twext/enterprise/queue.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twext/enterprise/queue.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twext/enterprise/queue.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -87,7 +87,7 @@
 from twisted.application.service import MultiService
 from twisted.internet.protocol import Factory
 from twisted.internet.defer import (
-    inlineCallbacks, returnValue, Deferred, passthru
+    inlineCallbacks, returnValue, Deferred, passthru, succeed
 )
 from twisted.internet.endpoints import TCP4ClientEndpoint
 from twisted.protocols.amp import AMP, Command, Integer, Argument, String
@@ -635,7 +635,7 @@
         """
         The total load of all currently connected workers.
         """
-        return sum(worker.currentLoad() for worker in self.workers)
+        return sum(worker.currentLoad for worker in self.workers)
 
 
     def _selectLowestLoadWorker(self):
@@ -646,7 +646,7 @@
         @return: a worker connection with the lowest current load.
         @rtype: L{ConnectionFromWorker}
         """
-        return sorted(self.workers[:], key=lambda w: w.currentLoad())[0]
+        return sorted(self.workers[:], key=lambda w: w.currentLoad)[0]
 
 
     def performWork(self, table, workID):
@@ -865,6 +865,9 @@
 
 
 
+
+
+
 class WorkerFactory(Factory, object):
     """
     Factory, to be used as the client to connect from the worker to the
@@ -1197,8 +1200,6 @@
         return self.choosePerformer(onlyLocally=True).performWork(table, workID)
 
 
-
-
     def allWorkItemTypes(self):
         """
         Load all the L{WorkItem} types that this node can process and return
@@ -1208,7 +1209,14 @@
         """
         # TODO: For completeness, this may need to involve a plugin query to
         # make sure that all WorkItem subclasses are imported first.
-        return WorkItem.__subclasses__()
+        for workItemSubclass in WorkItem.__subclasses__():
+            # TODO: It might be a good idea to offload this table-filtering to
+            # SchemaSyntax.__contains__, adding in some more structure-
+            # comparison of similarly-named tables.  For now a name check is
+            # sufficient.
+            if workItemSubclass.table.model.name in set([x.model.name for x in
+                                                         self.schema]):
+                yield workItemSubclass
 
 
     def totalNumberOfNodes(self):
@@ -1441,4 +1449,41 @@
         """
         Choose to perform the work locally.
         """
-        return LocalPerformer(self.txnFactory)
\ No newline at end of file
+        return LocalPerformer(self.txnFactory)
+
+
+
+class NonPerformer(object):
+    """
+    Implementor of C{performWork} that doesn't actual perform any work.  This
+    is used in the case where you want to be able to enqueue work for someone
+    else to do, but not take on any work yourself (such as a command line tool).
+    """
+    implements(_IWorkPerformer)
+
+    def performWork(self, table, workID):
+        """
+        Don't perform work.
+        """
+        return succeed(None)
+
+
+class NonPerformingQueuer(_BaseQueuer):
+    """
+    When work is enqueued with this queuer, it is never executed locally.
+    It's expected that the polling machinery will find the work and perform it.
+    """
+    implements(IQueuer)
+
+    def __init__(self, reactor=None):
+        super(NonPerformingQueuer, self).__init__()
+        if reactor is None:
+            from twisted.internet import reactor
+        self.reactor = reactor
+
+
+    def choosePerformer(self):
+        """
+        Choose to perform the work locally.
+        """
+        return NonPerformer()
\ No newline at end of file

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twext/enterprise/test/test_queue.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twext/enterprise/test/test_queue.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twext/enterprise/test/test_queue.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -54,7 +54,7 @@
 from zope.interface.verify import verifyObject
 from twisted.test.proto_helpers import StringTransport
 
-from twext.enterprise.queue import _BaseQueuer
+from twext.enterprise.queue import _BaseQueuer, NonPerformingQueuer
 import twext.enterprise.queue
 
 class Clock(_Clock):
@@ -337,7 +337,7 @@
 
 
     @inlineCallbacks
-    def FIXME_test_notBeforeWhenCheckingForLostWork(self):
+    def test_notBeforeWhenCheckingForLostWork(self):
         """
         L{PeerConnectionPool._periodicLostWorkCheck} should execute any
         outstanding work items, but only those that are expired.
@@ -654,3 +654,14 @@
         queuer.enqueueWork(None, None)
         self.assertNotEqual(self.proposal, None)
 
+
+class NonPerformingQueuerTests(TestCase):
+
+    @inlineCallbacks
+    def test_choosePerformer(self):
+        queuer = NonPerformingQueuer()
+        performer = queuer.choosePerformer()
+        result = (yield performer.performWork(None, None))
+        self.assertEquals(result, None)
+
+

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twext/python/sendfd.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twext/python/sendfd.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twext/python/sendfd.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -15,7 +15,7 @@
 # limitations under the License.
 ##
 
-from struct import pack, unpack
+from struct import pack, unpack, calcsize
 from socket import SOL_SOCKET
 
 from twext.python.sendmsg import sendmsg, recvmsg, SCM_RIGHTS
@@ -43,6 +43,7 @@
     )
 
 
+
 def recvfd(socketfd):
     """
     Receive a file descriptor from a L{sendmsg} message on the given C{AF_UNIX}
@@ -57,10 +58,15 @@
 
     @rtype: 2-tuple of (C{int}, C{str})
     """
-    data, flags, ancillary = recvmsg(socketfd)
-    [(cmsg_level, cmsg_type, packedFD)] = ancillary
+    data, _ignore_flags, ancillary = recvmsg(socketfd)
+    [(_ignore_cmsg_level, _ignore_cmsg_type, packedFD)] = ancillary
     # cmsg_level and cmsg_type really need to be SOL_SOCKET / SCM_RIGHTS, but
     # since those are the *only* standard values, there's not much point in
     # checking.
-    [unpackedFD] = unpack("i", packedFD)
+    unpackedFD = 0
+    int_size = calcsize("i")
+    if len(packedFD) > int_size:       # [ar]happens on 64 bit architecture (FreeBSD)
+        [unpackedFD] = unpack("i", packedFD[0:int_size])
+    else:
+        [unpackedFD] = unpack("i", packedFD)
     return (unpackedFD, data)

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twisted/plugins/caldav.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twisted/plugins/caldav.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twisted/plugins/caldav.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -53,4 +53,3 @@
 
 
 TwistedCalDAV = TAP("calendarserver.tap.caldav.CalDAVServiceMaker")
-CalDAVGroupCacher = TAP("twistedcaldav.directory.directory.GroupMembershipCacherServiceMaker")

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/cache.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/cache.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/cache.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -20,7 +20,7 @@
 
 from zope.interface import implements
 
-from twisted.internet.defer import succeed, maybeDeferred, inlineCallbacks,\
+from twisted.internet.defer import succeed, maybeDeferred, inlineCallbacks, \
     returnValue
 from twext.web2.dav.util import allDataFromStream
 from twext.web2.http import Response
@@ -50,15 +50,15 @@
   - directoryToken - a hash of that principal's directory record
   - uriToken - a token for the request uri
   - childTokens - tokens for any child resources the request uri depends on (for depth:1)
-  
+
   The current principalToken, uriToken and childTokens values are themselves stored in the cache using the key prefix 'cacheToken:'.
 When the 'changeCache' api is called the cached value for the matching token is updated.
-  
+
 (4) When a request is being checked in the cache, the response cache entry key is first computed and any value extracted. The
 tokens in the value are then checked against the current set of tokens in the cache. If there is any mismatch between tokens, the
 cache entry is considered invalid and the cached response is not returned. If everything matches up, the cached response is returned
 to the caller and ultimately sent directly back to the client.
- 
+
 (5) Because of shared calendars/address books that can affect the calendar/address book homes of several different users at once, we
 need to keep track of the separate childTokens for each child resource. The tokens for shared resources are keyed of the sharer's uri,
 so sharee's homes use that token. That way a single token for all shared instances is used and changed just once.
@@ -72,18 +72,22 @@
     def __init__(self, *args, **kwargs):
         pass
 
+
     def changed(self):
         return succeed(None)
 
 
+
 class DisabledCache(object):
     def getResponseForRequest(self, request):
         return succeed(None)
 
+
     def cacheResponseForRequest(self, request, response):
         return succeed(response)
 
 
+
 class URINotFoundException(Exception):
     def __init__(self, uri):
         self.uri = uri
@@ -95,6 +99,7 @@
             self.uri)
 
 
+
 class MemcacheChangeNotifier(LoggingMixIn, CachePoolUserMixIn):
 
     def __init__(self, resource, cachePool=None, cacheHandle="Default"):
@@ -102,22 +107,31 @@
         self._cachePool = cachePool
         self._cachePoolHandle = cacheHandle
 
+
     def _newCacheToken(self):
         return str(uuid.uuid4())
 
+
     def changed(self):
         """
         Change the cache token for a resource
 
         return: A L{Deferred} that fires when the token has been changed.
         """
-        url = self._resource.url()
 
+        # For shared resources we use the owner URL as the cache key
+        if hasattr(self._resource, "owner_url"):
+            url = self._resource.owner_url()
+        else:
+            url = self._resource.url()
+
         self.log_debug("Changing Cache Token for %r" % (url,))
         return self.getCachePool().set(
             'cacheToken:%s' % (url,),
-            self._newCacheToken(), expireTime=config.ResponseCacheTimeout*60)
-            
+            self._newCacheToken(), expireTime=config.ResponseCacheTimeout * 60)
+
+
+
 class BaseResponseCache(LoggingMixIn):
     """
     A base class which provides some common operations
@@ -149,10 +163,10 @@
     def _canonicalizeURIForRequest(self, uri, request):
         """
         Always use canonicalized forms of the URIs for caching (i.e. __uids__ paths).
-        
-        Do this without calling locateResource which may cause a query on the store. 
+
+        Do this without calling locateResource which may cause a query on the store.
         """
-        
+
         uribits = uri.split("/")
         if len(uribits) > 1 and uribits[1] in ("principals", "calendars", "addressbooks"):
             if uribits[2] == "__uids__":
@@ -166,8 +180,7 @@
                     uribits[2] = "__uids__"
                     uribits[3] = record.uid
                     return succeed("/".join(uribits))
-                
-            
+
         # Fall back to the locateResource approach
         try:
             return request.locateResource(uri).addCallback(
@@ -190,6 +203,7 @@
 
         return d
 
+
     @inlineCallbacks
     def _requestKey(self, request):
         """
@@ -201,7 +215,7 @@
             # Give it back to the request so it can be read again
             request.stream = MemoryStream(requestBody)
             request.stream.doStartReading = None
-            
+
             # Normalize the property order by doing a "dumb" sort on lines
             requestLines = requestBody.splitlines()
             requestLines.sort()
@@ -222,6 +236,7 @@
         return d1
 
 
+
 class MemcacheResponseCache(BaseResponseCache, CachePoolUserMixIn):
     def __init__(self, docroot, cachePool=None):
         self._docroot = docroot
@@ -238,6 +253,7 @@
         else:
             return self.getCachePool().get('cacheToken:%s' % (uri,))
 
+
     @inlineCallbacks
     def _tokenForRecord(self, uri, request):
         """
@@ -247,19 +263,21 @@
         record = (yield self._getRecordForURI(uri, request))
         returnValue(record.cacheToken())
 
+
     @inlineCallbacks
     def _tokensForChildren(self, rURI, request):
         """
         Create a dict of child resource tokens for any "recorded" during this request in the childCacheURIs attribute.
         """
-        
+
         if hasattr(request, "childCacheURIs"):
             tokens = dict([(uri, (yield self._tokenForURI(uri)),) for uri in request.childCacheURIs])
             returnValue(tokens)
         else:
             returnValue({})
-    
-    @inlineCallbacks 
+
+
+    @inlineCallbacks
     def _getTokens(self, request):
         """
         Tokens are a principal token, directory record token, resource token and list
@@ -296,19 +314,19 @@
         """
         try:
             key = (yield self._hashedRequestKey(request))
-    
+
             self.log_debug("Checking cache for: %r" % (key,))
             _ignore_flags, value = (yield self.getCachePool().get(key))
-    
+
             if value is None:
                 self.log_debug("Not in cache: %r" % (key,))
                 returnValue(None)
-    
+
             self.log_debug("Found in cache: %r = %r" % (key, value))
-    
+
             (principalToken, directoryToken, uriToken, childTokens, (code, headers, body)) = cPickle.loads(value)
             currentTokens = (yield self._getTokens(request))
-    
+
             if currentTokens[0] != principalToken:
                 self.log_debug(
                     "Principal token doesn't match for %r: %r != %r" % (
@@ -316,7 +334,7 @@
                         currentTokens[0],
                         principalToken))
                 returnValue(None)
-    
+
             if currentTokens[1] != directoryToken:
                 self.log_debug(
                     "Directory Record Token doesn't match for %r: %r != %r" % (
@@ -324,7 +342,7 @@
                         currentTokens[1],
                         directoryToken))
                 returnValue(None)
-    
+
             if currentTokens[2] != uriToken:
                 self.log_debug(
                     "URI token doesn't match for %r: %r != %r" % (
@@ -332,7 +350,7 @@
                         currentTokens[2],
                         uriToken))
                 returnValue(None)
-    
+
             for childuri, token in childTokens.items():
                 currentToken = (yield self._tokenForURI(childuri))
                 if currentToken != token:
@@ -343,19 +361,20 @@
                             currentToken,
                             token))
                     returnValue(None)
-                     
+
             r = Response(code,
                          stream=MemoryStream(body))
-    
+
             for key, value in headers.iteritems():
                 r.headers.setRawHeaders(key, value)
-    
+
             returnValue(r)
 
         except URINotFoundException, e:
             self.log_debug("Could not locate URI: %r" % (e,))
             returnValue(None)
 
+
     @inlineCallbacks
     def cacheResponseForRequest(self, request, response):
         """
@@ -368,9 +387,9 @@
                 key = request.cacheKey
             else:
                 key = (yield self._hashedRequestKey(request))
-    
+
             key, responseBody = (yield self._getResponseBody(key, response))
-    
+
             response.headers.removeHeader('date')
             response.stream = MemoryStream(responseBody)
             pToken, dToken, uToken, cTokens = (yield self._getTokens(request))
@@ -388,30 +407,34 @@
             ))
             self.log_debug("Adding to cache: %r = %r" % (key, cacheEntry))
             yield self.getCachePool().set(key, cacheEntry,
-                expireTime=config.ResponseCacheTimeout*60)
+                expireTime=config.ResponseCacheTimeout * 60)
 
         except URINotFoundException, e:
             self.log_debug("Could not locate URI: %r" % (e,))
 
-        returnValue(response)            
+        returnValue(response)
 
 
+
 class _CachedResponseResource(object):
     implements(IResource)
 
     def __init__(self, response):
         self._response = response
 
+
     def renderHTTP(self, request):
         if not hasattr(request, "extendedLogItems"):
             request.extendedLogItems = {}
         request.extendedLogItems["cached"] = "1"
         return self._response
 
+
     def locateChild(self, request, segments):
         return self, []
 
 
+
 class PropfindCacheMixin(object):
     """
     A mixin that causes a resource's PROPFIND response to be cached. It also adds an api to change the
@@ -433,12 +456,15 @@
             d.addCallback(_getResponseCache)
         return d
 
+
     def changeCache(self):
         if hasattr(self, 'cacheNotifier'):
             return self.cacheNotifier.changed()
         else:
             self.log_debug("%r does not have a cacheNotifier but was changed" % (self,))
 
+
+
 class ResponseCacheMixin(object):
     """
     This is a mixin for a child resource that does not itself cache PROPFINDs, but needs to invalidate a parent
@@ -451,19 +477,25 @@
         else:
             self.log_debug("%r does not have a cacheNotifier but was changed" % (self,))
 
+
+
 class CacheStoreNotifier(object):
-    
+
     def __init__(self, resource):
         self.resource = resource
-    
+
+
     def notify(self, op="update"):
         self.resource.changeCache()
 
+
     def clone(self, label="default", id=None):
         return self
 
+
     def getID(self, label="default"):
         return None
 
+
     def nodeName(self, label="default"):
         return succeed(None)

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/config.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/config.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -30,10 +30,12 @@
     Invalid server configuration.
     """
 
+
+
 class ConfigDict(dict):
     """
     Dictionary which can be accessed using attribute syntax, because
-    that reads an writes nicer in code.  For example:
+    that reads and writes nicer in code.  For example:
       C{config.Thingo.Tiny.Tweak}
     instead of:
       C{config["Thingo"]["Tiny"]["Tweak"]}
@@ -43,9 +45,11 @@
             for key, value in mapping.iteritems():
                 self[key] = value
 
+
     def __repr__(self):
         return "*" + dict.__repr__(self)
 
+
     def __setitem__(self, key, value):
         if key.startswith("_"):
             # Names beginning with "_" are reserved for real attributes
@@ -56,24 +60,29 @@
         else:
             dict.__setitem__(self, key, value)
 
+
     def __setattr__(self, attr, value):
         if attr.startswith("_"):
             dict.__setattr__(self, attr, value)
         else:
             self[attr] = value
 
+
     def __getattr__(self, attr):
         if not attr.startswith("_") and attr in self:
             return self[attr]
         else:
             return dict.__getattribute__(self, attr)
 
+
     def __delattr__(self, attr):
         if not attr.startswith("_") and attr in self:
             del self[attr]
         else:
             dict.__delattr__(self, attr)
 
+
+
 class ConfigProvider(object):
     """
     Configuration provider, abstraction for config storage/format/defaults.
@@ -88,25 +97,29 @@
             self._defaults = ConfigDict()
         else:
             self._defaults = ConfigDict(copy.deepcopy(defaults))
-            
+
+
     def getDefaults(self):
         """
         Return defaults.
         """
         return self._defaults
-    
+
+
     def setDefaults(self, defaults):
         """
         Change defaults.
         """
         self._defaults = ConfigDict(copy.deepcopy(defaults))
-    
+
+
     def getConfigFileName(self):
         """
         Return current configuration file path and name.
         """
         return self._configFileName
-    
+
+
     def setConfigFileName(self, configFileName):
         """
         Change configuration file path and name for next load operations.
@@ -114,20 +127,23 @@
         self._configFileName = configFileName
         if self._configFileName:
             self._configFileName = os.path.abspath(configFileName)
-    
+
+
     def hasErrors(self):
         """
         Return true if last load operation encountered any errors.
         """
         return False
-            
+
+
     def loadConfig(self):
         """
         Load the configuration, return a dictionary of settings.
         """
         return self._defaults
-    
 
+
+
 class Config(object):
     def __init__(self, provider=None):
         if not provider:
@@ -140,7 +156,8 @@
         self._preUpdateHooks = []
         self._postUpdateHooks = []
         self.reset()
-        
+
+
     def __setattr__(self, attr, value):
         if "_data" in self.__dict__ and attr in self.__dict__["_data"]:
             self._data[attr] = value
@@ -162,12 +179,15 @@
             return self._data[attr]
         raise AttributeError(attr)
 
+
     def __hasattr__(self, attr):
         return attr in self._data
-    
+
+
     def __str__(self):
         return str(self._data)
 
+
     def get(self, attr, defaultValue):
         parts = attr.split(".")
         lastDict = self._data
@@ -183,6 +203,7 @@
             lastDict[configItem] = defaultValue
             return defaultValue
 
+
     def addResetHooks(self, before, after):
         """
         Hooks for preserving config across reload( ) + reset( )
@@ -193,27 +214,34 @@
         self._beforeResetHook = before
         self._afterResetHook = after
 
+
     def addPreUpdateHooks(self, hooks):
         self._preUpdateHooks.extend(hooks)
 
+
     def addPostUpdateHooks(self, hooks):
         self._postUpdateHooks.extend(hooks)
 
+
     def getProvider(self):
         return self._provider
 
+
     def setProvider(self, provider):
         self._provider = provider
         self.reset()
 
+
     def setDefaults(self, defaults):
         self._provider.setDefaults(defaults)
         self.reset()
 
+
     def updateDefaults(self, items):
-        _mergeData(self._provider.getDefaults(), items)
+        mergeData(self._provider.getDefaults(), items)
         self.update(items)
 
+
     def update(self, items=None, reloading=False):
         if self._updating:
             return
@@ -225,24 +253,26 @@
         # Call hooks
         for hook in self._preUpdateHooks:
             hook(self._data, items, reloading=reloading)
-        _mergeData(self._data, items)
+        mergeData(self._data, items)
         for hook in self._postUpdateHooks:
             hook(self._data, reloading=reloading)
 
         self._updating = False
         self._dirty = False
 
+
     def load(self, configFile):
         self._provider.setConfigFileName(configFile)
-        configDict = ConfigDict(self._provider.loadConfig())
+        configDict = self._provider.loadConfig()
         if not self._provider.hasErrors():
             self.update(configDict)
         else:
             raise ConfigurationError("Invalid configuration in %s"
                                      % (self._provider.getConfigFileName(),))
 
+
     def reload(self):
-        configDict = ConfigDict(self._provider.loadConfig())
+        configDict = self._provider.loadConfig()
         if not self._provider.hasErrors():
             if self._beforeResetHook:
                 # Give the beforeResetHook a chance to stash away values we want
@@ -259,11 +289,22 @@
             raise ConfigurationError("Invalid configuration in %s"
                 % (self._provider.getConfigFileName(), ))
 
+
     def reset(self):
         self._data = ConfigDict(copy.deepcopy(self._provider.getDefaults()))
         self._dirty = True
 
-def _mergeData(oldData, newData):
+
+
+def mergeData(oldData, newData):
+    """
+    Merge two ConfigDict objects; oldData will be updated with all the keys
+    and values from newData
+    @param oldData: the object to modify
+    @type oldData: ConfigDict
+    @param newData: the object to copy data from
+    @type newData: ConfigDict
+    """
     for key, value in newData.iteritems():
         if isinstance(value, (dict,)):
             if key in oldData:
@@ -271,10 +312,12 @@
                     "%r in %r is not a ConfigDict" % (oldData[key], oldData)
             else:
                 oldData[key] = {}
-            _mergeData(oldData[key], value)
+            mergeData(oldData[key], value)
         else:
             oldData[key] = value
 
+
+
 def fullServerPath(base, path):
     if type(path) is str:
         return os.path.join(base, path) if path and path[0] not in ('/', '.',) else path

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/appleopendirectory.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/appleopendirectory.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -56,17 +56,22 @@
         return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
 
 
-    def __init__(self, params):
+    def __init__(self, params, odModule=None):
         """
         @param params: a dictionary containing the following keys:
-            node: an OpenDirectory node name to bind to.
-            restrictEnabledRecords: C{True} if a group in the
-              directory is to be used to determine which calendar
-              users are enabled.
-            restrictToGroup: C{str} guid or name of group used to
-              restrict enabled users.
-            cacheTimeout: C{int} number of minutes before cache is invalidated.
-            negativeCache: C{False} cache the fact that a record wasn't found
+
+            - node: an OpenDirectory node name to bind to.
+
+            - restrictEnabledRecords: C{True} if a group in the directory is to
+              be used to determine which calendar users are enabled.
+
+            - restrictToGroup: C{str} guid or name of group used to restrict
+              enabled users.
+
+            - cacheTimeout: C{int} number of minutes before cache is
+              invalidated.
+
+            - negativeCache: C{False} cache the fact that a record wasn't found
         """
         defaults = {
             'node' : '/Search',
@@ -90,7 +95,9 @@
         super(OpenDirectoryService, self).__init__(params['cacheTimeout'],
                                                    params['negativeCaching'])
 
-        self.odModule = namedModule(config.OpenDirectoryModule)
+        if odModule is None:
+            odModule = namedModule(config.OpenDirectoryModule)
+        self.odModule = odModule
 
         try:
             directory = self.odModule.odInit(params['node'])
@@ -1447,7 +1454,7 @@
                     self.shortNames[0],
                     challenge,
                     response,
-                    credentials.originalMethod if credentials.originalMethod else credentials.method
+                    credentials.method
                 ):
                     try:
                         cache = self.digestcache
@@ -1465,7 +1472,8 @@
     Challenge: %s
     Response:  %s
     Method:    %s
-""" % (self.nodeName, self.shortNames[0], challenge, response, credentials.originalMethod if credentials.originalMethod else credentials.method))
+""" % (self.nodeName, self.shortNames[0], challenge, response,
+       credentials.method))
 
             except self.service.odModule.ODError, e:
                 self.log_error(

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/calendaruserproxy.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/calendaruserproxy.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -375,7 +375,9 @@
             p = self.pcollection.principalForUID(uid)
             if p:
                 # Only principals enabledForLogin can be a delegate
-                if p.record.enabledForLogin:
+                # (and groups as well)
+                if (p.record.enabledForLogin or 
+                    p.record.recordType == p.record.service.recordType_groups):
                     found.append(p)
                 # Make sure any outstanding deletion timer entries for
                 # existing principals are removed
@@ -592,7 +594,8 @@
 
         @param principalUID: the UID of the principal to remove.
         """
-
+        # FIXME: This method doesn't appear to be used anywhere.  Still needed?
+        
         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

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/directory.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/directory.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -26,6 +26,7 @@
     "DirectoryError",
     "DirectoryConfigurationError",
     "UnknownRecordTypeError",
+    "GroupMembershipCacheUpdater",
 ]
 
 import cPickle as pickle
@@ -34,7 +35,6 @@
 import itertools
 import os
 import pwd
-import signal
 import sys
 import types
 
@@ -46,24 +46,26 @@
 from twext.web2.dav.auth import IPrincipalCredentials
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue
 
-from twext.python.log import LoggingMixIn
+from twext.python.log import Logger, LoggingMixIn
 
 from twistedcaldav.config import config
+
 from twistedcaldav.directory.idirectory import IDirectoryService, IDirectoryRecord
 from twistedcaldav.directory.util import uuidFromName, normalizeUUID
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
 from twistedcaldav.scheduling.ischedule.localservers import Servers
 from twistedcaldav.memcacher import Memcacher
-from twistedcaldav import memcachepool
 from twisted.python.filepath import FilePath
-from twisted.python.reflect import namedClass
-from twisted.python.usage import Options, UsageError
-from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
-from twisted.application import service
-from twisted.plugin import IPlugin
 from xml.parsers.expat import ExpatError
 from plistlib import readPlistFromString
+from twext.enterprise.dal.record import fromTable
+from twext.enterprise.queue import WorkItem
+from txdav.common.datastore.sql_tables import schema
+from twext.enterprise.dal.syntax import Delete
 
+log = Logger()
+
+
 class DirectoryService(LoggingMixIn):
     implements(IDirectoryService, ICredentialsChecker)
 
@@ -564,20 +566,16 @@
 
     "group-cacher-populated" : contains a datestamp indicating the most recent
     population.
-
-    "group-cacher-lock" : used to prevent multiple updates, it has a value of "1"
-
     """
 
     def __init__(self, namespace, pickle=True, no_invalidation=False,
-        key_normalization=True, expireSeconds=0, lockSeconds=60):
+        key_normalization=True, expireSeconds=0):
 
         super(GroupMembershipCache, self).__init__(namespace, pickle=pickle,
             no_invalidation=no_invalidation,
             key_normalization=key_normalization)
 
         self.expireSeconds = expireSeconds
-        self.lockSeconds = lockSeconds
 
 
     def setGroupsFor(self, guid, memberships):
@@ -616,17 +614,7 @@
         returnValue(value is not None)
 
 
-    def acquireLock(self):
-        self.log_debug("add group-cacher-lock")
-        return self.add("group-cacher-lock", "1", expireTime=self.lockSeconds)
 
-
-    def releaseLock(self):
-        self.log_debug("delete group-cacher-lock")
-        return self.delete("group-cacher-lock")
-
-
-
 class GroupMembershipCacheUpdater(LoggingMixIn):
     """
     Responsible for updating memcached with group memberships.  This will run
@@ -634,11 +622,12 @@
     proxy database, and the location/resource info in the directory system.
     """
 
-    def __init__(self, proxyDB, directory, expireSeconds, lockSeconds,
+    def __init__(self, proxyDB, directory, updateSeconds, expireSeconds,
         cache=None, namespace=None, useExternalProxies=False,
         externalProxiesSource=None):
         self.proxyDB = proxyDB
         self.directory = directory
+        self.updateSeconds = updateSeconds
         self.useExternalProxies = useExternalProxies
         if useExternalProxies and externalProxiesSource is None:
             externalProxiesSource = self.directory.getExternalProxyAssignments
@@ -646,8 +635,7 @@
 
         if cache is None:
             assert namespace is not None, "namespace must be specified if GroupMembershipCache is not provided"
-            cache = GroupMembershipCache(namespace, expireSeconds=expireSeconds,
-                lockSeconds=lockSeconds)
+            cache = GroupMembershipCache(namespace, expireSeconds=expireSeconds)
         self.cache = cache
 
 
@@ -738,8 +726,6 @@
         # See if anyone has completely populated the group membership cache
         isPopulated = (yield self.cache.isPopulated())
 
-        useLock = True
-
         if fast:
             # We're in quick-start mode.  Check first to see if someone has
             # populated the membership cache, and if so, return immediately
@@ -747,9 +733,6 @@
                 self.log_info("Group membership cache is already populated")
                 returnValue((fast, 0))
 
-            # We don't care what others are doing right now, we need to update
-            useLock = False
-
         self.log_info("Updating group membership cache")
 
         dataRoot = FilePath(config.DataRoot)
@@ -767,14 +750,6 @@
             previousMembers = pickle.loads(membershipsCacheFile.getContent())
             callGroupsChanged = True
 
-        if useLock:
-            self.log_info("Attempting to acquire group membership cache lock")
-            acquiredLock = (yield self.cache.acquireLock())
-            if not acquiredLock:
-                self.log_info("Group membership cache lock held by another process")
-                returnValue((fast, 0))
-            self.log_info("Acquired lock")
-
         if not fast and self.useExternalProxies:
 
             # Load in cached copy of external proxies so we can diff against them
@@ -944,257 +919,50 @@
 
         yield self.cache.setPopulatedMarker()
 
-        if useLock:
-            self.log_info("Releasing lock")
-            yield self.cache.releaseLock()
-
         self.log_info("Group memberships cache updated")
 
         returnValue((fast, len(members), len(changedMembers)))
 
 
+class GroupCacherPollingWork(WorkItem, fromTable(schema.GROUP_CACHER_POLLING_WORK)):
 
-class GroupMembershipCacherOptions(Options):
-    optParameters = [[
-        "config", "f", DEFAULT_CONFIG_FILE, "Path to configuration file."
-    ]]
+    group = "group_cacher_polling"
 
-    def __init__(self, *args, **kwargs):
-        super(GroupMembershipCacherOptions, self).__init__(*args, **kwargs)
-
-        self.overrides = {}
-
-
-    def _coerceOption(self, configDict, key, value):
-        """
-        Coerce the given C{val} to type of C{configDict[key]}
-        """
-        if key in configDict:
-            if isinstance(configDict[key], bool):
-                value = value == "True"
-
-            elif isinstance(configDict[key], (int, float, long)):
-                value = type(configDict[key])(value)
-
-            elif isinstance(configDict[key], (list, tuple)):
-                value = value.split(',')
-
-            elif isinstance(configDict[key], dict):
-                raise UsageError(
-                    "Dict options not supported on the command line"
-                )
-
-            elif value == 'None':
-                value = None
-
-        return value
-
-
-    def _setOverride(self, configDict, path, value, overrideDict):
-        """
-        Set the value at path in configDict
-        """
-        key = path[0]
-
-        if len(path) == 1:
-            overrideDict[key] = self._coerceOption(configDict, key, value)
-            return
-
-        if key in configDict:
-            if not isinstance(configDict[key], dict):
-                raise UsageError(
-                    "Found intermediate path element that is not a dictionary"
-                )
-
-            if key not in overrideDict:
-                overrideDict[key] = {}
-
-            self._setOverride(
-                configDict[key], path[1:],
-                value, overrideDict[key]
-            )
-
-
-    def opt_option(self, option):
-        """
-        Set an option to override a value in the config file. True, False, int,
-        and float options are supported, as well as comma seperated lists. Only
-        one option may be given for each --option flag, however multiple
-        --option flags may be specified.
-        """
-
-        if "=" in option:
-            path, value = option.split('=')
-            self._setOverride(
-                DEFAULT_CONFIG,
-                path.split('/'),
-                value,
-                self.overrides
-            )
-        else:
-            self.opt_option('%s=True' % (option,))
-
-    opt_o = opt_option
-
-    def postOptions(self):
-        config.load(self['config'])
-        config.updateDefaults(self.overrides)
-        self.parent['pidfile'] = config.PIDFile
-
-
-
-class GroupMembershipCacherService(service.Service, LoggingMixIn):
-    """
-    Service to update the group membership cache at a configured interval
-    """
-
-    def __init__(self, proxyDB, directory, namespace, updateSeconds,
-        expireSeconds, lockSeconds, reactor=None, updateMethod=None,
-        useExternalProxies=False):
-
-        if updateSeconds >= expireSeconds:
-            expireSeconds = updateSeconds * 2
-            self.log_warn("Configuration warning: GroupCaching.ExpireSeconds needs to be longer than UpdateSeconds; setting to %d seconds" % (expireSeconds,))
-
-        self.updater = GroupMembershipCacheUpdater(proxyDB, directory,
-            expireSeconds, lockSeconds, namespace=namespace,
-            useExternalProxies=useExternalProxies)
-
-        if reactor is None:
-            from twisted.internet import reactor
-        self.reactor = reactor
-
-        self.updateSeconds = updateSeconds
-        self.nextUpdate = None
-        self.updateInProgress = False
-        self.updateAwaiting = False
-
-        if updateMethod:
-            self.updateMethod = updateMethod
-        else:
-            self.updateMethod = self.updater.updateCache
-
-
-    def startService(self):
-        self.previousHandler = signal.signal(signal.SIGHUP, self.sighupHandler)
-        self.log_warn("Starting group membership cacher service")
-        service.Service.startService(self)
-        return self.update()
-
-
-    def sighupHandler(self, num, frame):
-        self.reactor.callFromThread(self.update)
-
-
-    def stopService(self):
-        signal.signal(signal.SIGHUP, self.previousHandler)
-        self.log_warn("Stopping group membership cacher service")
-        service.Service.stopService(self)
-        if self.nextUpdate is not None:
-            self.nextUpdate.cancel()
-            self.nextUpdate = None
-
-
     @inlineCallbacks
-    def update(self):
-        """
-        A wrapper around updateCache, this method manages the scheduling of the
-        subsequent update, as well as prevents multiple updates from running
-        simultaneously, which could otherwise happen because SIGHUP now triggers
-        an update on demand.  If update is called while an update is in progress,
-        as soon as the first update is finished a new one is started.  Otherwise,
-        when an update finishes and there is not another one waiting, the next
-        update is scheduled for updateSeconds in the future.
+    def doWork(self):
 
-        @return: True if an update was already in progress, False otherwise
-        @rtype: C{bool}
-        """
+        # Delete all other work items
+        yield Delete(From=self.table, Where=None).on(self.transaction)
 
-        self.log_debug("Group membership update called")
-
-        # A call to update while an update is in progress sets the updateAwaiting flag
-        # so that an update happens again right after the current one is complete.
-        if self.updateInProgress:
-            self.updateAwaiting = True
-            returnValue(True)
-
-        self.nextUpdate = None
-        self.updateInProgress = True
-        self.updateAwaiting = False
-        try:
-            yield self.updateMethod()
-        finally:
-            self.updateInProgress = False
-            if self.updateAwaiting:
-                self.log_info("Performing group membership update")
-                yield self.update()
-            else:
-                self.log_info("Scheduling next group membership update")
-                self.nextUpdate = self.reactor.callLater(self.updateSeconds,
-                    self.update)
-        returnValue(False)
-
-
-
-class GroupMembershipCacherServiceMaker(LoggingMixIn):
-    """
-    Configures and returns a GroupMembershipCacherService
-    """
-    implements(IPlugin, service.IServiceMaker)
-
-    tapname = "caldav_groupcacher"
-    description = "Group Membership Cacher"
-    options = GroupMembershipCacherOptions
-
-    def makeService(self, options):
-        try:
-            from setproctitle import setproctitle
-        except ImportError:
-            pass
+        groupCacher = getattr(self.transaction, "_groupCacher", None)
+        if groupCacher is not None:
+            try:
+                yield groupCacher.updateCache()
+            except Exception, e:
+                log.error("Failed to update group membership cache (%s)" % (e,))
+            finally:
+                notBefore = (datetime.datetime.utcnow() +
+                    datetime.timedelta(seconds=groupCacher.updateSeconds))
+                log.debug("Scheduling next group cacher update: %s" % (notBefore,))
+                yield self.transaction.enqueue(GroupCacherPollingWork,
+                    notBefore=notBefore)
         else:
-            setproctitle("CalendarServer [Group Cacher]")
+            notBefore = (datetime.datetime.utcnow() +
+                datetime.timedelta(seconds=10))
+            log.debug("Rescheduling group cacher update: %s" % (notBefore,))
+            yield self.transaction.enqueue(GroupCacherPollingWork,
+                notBefore=notBefore)
 
-        # Setup the directory
-        from calendarserver.tap.util import directoryFromConfig
-        directory = directoryFromConfig(config)
 
-        # We have to set cacheNotifierFactory otherwise group cacher can't
-        # invalidate the cache tokens for principals whose membership has
-        # changed
-        if config.EnableResponseCache and config.Memcached.Pools.Default.ClientEnabled:
-            from twistedcaldav.directory.principal import DirectoryPrincipalResource
-            from twistedcaldav.cache import MemcacheChangeNotifier
-            DirectoryPrincipalResource.cacheNotifierFactory = MemcacheChangeNotifier
+ at inlineCallbacks
+def scheduleNextGroupCachingUpdate(store, seconds):
+    txn = store.newTransaction()
+    notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
+    log.debug("Scheduling next group cacher update: %s" % (notBefore,))
+    yield txn.enqueue(GroupCacherPollingWork, notBefore=notBefore)
+    yield txn.commit()
 
-        # Setup the ProxyDB Service
-        proxydbClass = namedClass(config.ProxyDBService.type)
 
-        self.log_warn("Configuring proxydb service of type: %s" % (proxydbClass,))
-
-        try:
-            proxyDB = proxydbClass(**config.ProxyDBService.params)
-        except IOError:
-            self.log_error("Could not start proxydb service")
-            raise
-
-        # Setup memcached pools
-        memcachepool.installPools(
-            config.Memcached.Pools,
-            config.Memcached.MaxClients,
-        )
-
-        cacherService = GroupMembershipCacherService(proxyDB, directory,
-            config.GroupCaching.MemcachedPool,
-            config.GroupCaching.UpdateSeconds,
-            config.GroupCaching.ExpireSeconds,
-            config.GroupCaching.LockSeconds,
-            useExternalProxies=config.GroupCaching.UseExternalProxies
-            )
-
-        return cacherService
-
-
-
 def diffAssignments(old, new):
     """
     Compare two proxy assignment lists and return their differences in the form of

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/ldapdirectory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/ldapdirectory.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/ldapdirectory.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -480,6 +480,11 @@
                 # Seen when using an empty password, treat as invalid creds
                 raise ldap.INVALID_CREDENTIALS()
 
+            except ldap.NO_SUCH_OBJECT:
+                self.log_error("LDAP Authentication error for %s: NO_SUCH_OBJECT"
+                    % (dn,))
+                # fall through to try again; could be transient
+
             except ldap.INVALID_CREDENTIALS:
                 raise
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/accounts-modified.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/accounts-modified.xml	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/accounts-modified.xml	2013-04-11 16:24:18 UTC (rev 11029)
@@ -115,6 +115,35 @@
     <name>佐藤佐藤佐藤</name>
     <email-address>nonascii at example.com</email-address>
   </user>
+  <user>
+    <uid>delegator</uid>
+    <guid>FC465590-E9E9-4746-ACE8-6C756A49FE4D</guid>
+    <password>a</password>
+    <name>Calendar Delegator</name>
+    <email-address>calendardelegator at example.com</email-address>
+  </user>
+  <user>
+    <uid>occasionaldelegate</uid>
+    <guid>EC465590-E9E9-4746-ACE8-6C756A49FE4D</guid>
+    <password>a</password>
+    <name>Occasional Delegate</name>
+    <email-address>occasional at example.com</email-address>
+  </user>
+    <user>
+    <uid>delegateviagroup</uid>
+    <guid>46D9D716-CBEE-490F-907A-66FA6C3767FF</guid>
+    <password>a</password>
+    <name>Delegate Via Group</name>
+    <email-address>delegateviagroup at example.com</email-address>
+  </user>
+  <group>
+    <uid>delegategroup</uid>
+    <guid>00599DAF-3E75-42DD-9DB7-52617E79943F</guid>
+    <name>Delegate Group</name>
+    <members>
+      <member type="users">delegateviagroup</member>
+    </members>
+  </group>
   <user repeat="2">
     <uid>user%02d</uid>
     <guid>user%02d</guid>

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/accounts.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/accounts.xml	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/accounts.xml	2013-04-11 16:24:18 UTC (rev 11029)
@@ -124,6 +124,21 @@
     <name>Occasional Delegate</name>
     <email-address>occasional at example.com</email-address>
   </user>
+    <user>
+    <uid>delegateviagroup</uid>
+    <guid>46D9D716-CBEE-490F-907A-66FA6C3767FF</guid>
+    <password>a</password>
+    <name>Delegate Via Group</name>
+    <email-address>delegateviagroup at example.com</email-address>
+  </user>
+  <group>
+    <uid>delegategroup</uid>
+    <guid>00599DAF-3E75-42DD-9DB7-52617E79943F</guid>
+    <name>Delegate Group</name>
+    <members>
+      <member type="users">delegateviagroup</member>
+    </members>
+  </group>
   <user repeat="2">
     <uid>user%02d</uid>
     <guid>user%02d</guid>

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/augments.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/augments.xml	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/augments.xml	2013-04-11 16:24:18 UTC (rev 11029)
@@ -186,4 +186,10 @@
     <enable-calendar>true</enable-calendar>
     <enable-login>true</enable-login>
   </record>
+  <record>
+    <uid>00599DAF-3E75-42DD-9DB7-52617E79943F</uid>
+    <enable>true</enable>
+    <enable-calendar>false</enable-calendar>
+    <enable-login>false</enable-login>
+  </record>
 </augments>

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/proxies.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/proxies.xml	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/proxies.xml	2013-04-11 16:24:18 UTC (rev 11029)
@@ -63,6 +63,7 @@
     <guid>FC465590-E9E9-4746-ACE8-6C756A49FE4D</guid>
     <proxies>
       <member>EC465590-E9E9-4746-ACE8-6C756A49FE4D</member>
+      <member>00599DAF-3E75-42DD-9DB7-52617E79943F</member>
     </proxies>
   </record>
 </proxies>

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_directory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_directory.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_directory.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -15,13 +15,12 @@
 ##
 
 from twisted.internet.defer import inlineCallbacks
-from twisted.internet.task import Clock
 from twisted.python.filepath import FilePath
 
 from twistedcaldav.test.util import TestCase
 from twistedcaldav.test.util import xmlFile, augmentsFile, proxiesFile, dirTest
 from twistedcaldav.config import config
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord, GroupMembershipCacherService, GroupMembershipCache, GroupMembershipCacheUpdater, diffAssignments
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord, GroupMembershipCache, GroupMembershipCacheUpdater, diffAssignments
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
 from twistedcaldav.directory import augment, calendaruserproxy
@@ -121,46 +120,7 @@
         self.count += 1
 
 
-    @inlineCallbacks
-    def test_groupMembershipCacherService(self):
-        """
-        Instantiate a GroupMembershipCacherService and make sure its update
-        method fires at the right interval, in this case 30 seconds.  The
-        updateMethod keyword arg is purely for testing purposes, so we can
-        directly detect it getting called in this test.
-        """
-        clock = Clock()
-        self.count = 0
 
-        # Deliberately set the expireSeconds lower than updateSeconds to verify
-        # expireSeconds gets set to 2 * updateSeconds in that scenario
-
-        service = GroupMembershipCacherService(
-            None, None, "Testing", 30, 20, 30, reactor=clock,
-            updateMethod=self._updateMethod)
-
-        # expireSeconds = 2 * 30 updateSeconds
-        self.assertEquals(service.updater.cache.expireSeconds, 60)
-
-        yield service.startService()
-
-        self.assertEquals(self.count, 1)
-        clock.advance(29)
-        self.assertEquals(self.count, 1)
-        clock.advance(1)
-        self.assertEquals(self.count, 2)
-
-        service.stopService()
-
-        service.updateInProgress = True
-        self.assertTrue((yield service.update()))
-        self.assertTrue(service.updateAwaiting)
-
-        service.updateInProgress = False
-        self.assertFalse((yield service.update()))
-        self.assertFalse(service.updateAwaiting)
-
-
     def test_expandedMembers(self):
         """
         Make sure expandedMembers( ) returns a complete, flattened set of
@@ -220,6 +180,8 @@
         self.assertEquals(
             groups,
             {
+                '00599DAF-3E75-42DD-9DB7-52617E79943F':
+                    set(['46D9D716-CBEE-490F-907A-66FA6C3767FF']),
                 '9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1':
                     set(['8B4288F6-CC82-491D-8EF9-642EF4F3E7D0']),
                 'admin':
@@ -250,6 +212,8 @@
         self.assertEquals(
             aliases,
             {
+                '00599DAF-3E75-42DD-9DB7-52617E79943F':
+                    '00599DAF-3E75-42DD-9DB7-52617E79943F',
                 '9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1':
                     '9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1',
                  'admin': 'admin',
@@ -275,19 +239,8 @@
             )
         )
 
-        # Prevent an update by locking the cache
-        acquiredLock = (yield cache.acquireLock())
-        self.assertTrue(acquiredLock)
-        self.assertEquals((False, 0), (yield updater.updateCache()))
+        self.assertEquals((False, 9, 9), (yield updater.updateCache()))
 
-        # You can't lock when already locked:
-        acquiredLockAgain = (yield cache.acquireLock())
-        self.assertFalse(acquiredLockAgain)
-
-        # Allow an update by unlocking the cache
-        yield cache.releaseLock()
-        self.assertEquals((False, 8, 8), (yield updater.updateCache()))
-
         # Verify cache is populated:
         self.assertTrue((yield cache.isPopulated()))
 
@@ -382,7 +335,7 @@
         # that wsanchez is only a proxy for gemini (since that assignment does not involve groups)
         self.directoryService.xmlFile = dirTest.child("accounts-modified.xml")
         self.directoryService._alwaysStat = True
-        self.assertEquals((False, 7, 1), (yield updater.updateCache()))
+        self.assertEquals((False, 8, 1), (yield updater.updateCache()))
         delegate = self._getPrincipalByShortName(DirectoryService.recordType_users, "wsanchez")
         proxyFor = (yield delegate.proxyFor(True))
         self.assertEquals(
@@ -682,20 +635,14 @@
         # time), but since the snapshot doesn't exist we fault in from the
         # directory (fast now is False), and snapshot will get created
 
-        # Note that because fast=True and isPopulated() is False, locking is
-        # ignored:
-        yield cache.acquireLock()
-
         self.assertFalse((yield cache.isPopulated()))
         fast, numMembers, numChanged = (yield updater.updateCache(fast=True))
         self.assertEquals(fast, False)
-        self.assertEquals(numMembers, 8)
-        self.assertEquals(numChanged, 8)
+        self.assertEquals(numMembers, 9)
+        self.assertEquals(numChanged, 9)
         self.assertTrue(snapshotFile.exists())
         self.assertTrue((yield cache.isPopulated()))
 
-        yield cache.releaseLock()
-
         # Try another fast update where the snapshot already exists (as in a
         # server-restart scenario), which will only read from the snapshot
         # as indicated by the return value for "fast".  Note that the cache
@@ -708,7 +655,7 @@
         # Try an update which faults in from the directory (fast=False)
         fast, numMembers, numChanged = (yield updater.updateCache(fast=False))
         self.assertEquals(fast, False)
-        self.assertEquals(numMembers, 8)
+        self.assertEquals(numMembers, 9)
         self.assertEquals(numChanged, 0)
 
         # Verify the snapshot contains the pickled dictionary we expect
@@ -716,6 +663,10 @@
         self.assertEquals(
             members,
             {
+                "46D9D716-CBEE-490F-907A-66FA6C3767FF":
+                    set([
+                        u"00599DAF-3E75-42DD-9DB7-52617E79943F",
+                    ]),
                 "5A985493-EE2C-4665-94CF-4DFEA3A89500":
                     set([
                         u"non_calendar_group",

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_opendirectory.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_opendirectory.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -19,6 +19,7 @@
 except ImportError:
     pass
 else:
+    from collections import defaultdict
     from twisted.trial.unittest import SkipTest
     from twisted.internet.defer import inlineCallbacks
     from twisted.python.runtime import platform
@@ -28,7 +29,24 @@
     from twistedcaldav.directory.directory import DirectoryService
     from twistedcaldav.directory.appleopendirectory import OpenDirectoryRecord
     from calendarserver.platform.darwin.od import dsattributes
+    from txdav.common.datastore.test.util import deriveValue, withSpecialValue
 
+    class DigestAuthModule(object):
+        """
+        Stand-in for either configurable OD module, that verifies the response
+        according to its '.response' attribute, set by the test.
+        """
+        class ODError(Exception):
+            pass
+
+        def odInit(self, node):
+            return self
+
+        def authenticateUserDigest(self, directory, node, user, challenge,
+                                   response, method):
+            val = (response == self.response)
+            return val
+
     # Wonky hack to prevent unclean reactor shutdowns
     class DummyReactor(object):
         @staticmethod
@@ -60,7 +78,8 @@
                     {
                         "node" : "/Search",
                         "augmentService": augment.AugmentXMLDB(xmlFiles=()),
-                    }
+                    },
+                    odModule=deriveValue(self, "odModule", lambda self: None)
                 )
             except ImportError, e:
                 raise SkipTest("OpenDirectory module is not available: %s" % (e,))
@@ -87,6 +106,8 @@
             )
             self.assertEquals(record.fullName, "")
 
+
+        @withSpecialValue("odModule", DigestAuthModule())
         def test_invalidODDigest(self):
             record = OpenDirectoryRecord(
                 service               = self.service(),
@@ -105,11 +126,16 @@
                 extReadOnlyProxies    = [],
             )
 
-            digestFields = {}
-            digested = DigestedCredentials("user", "GET", "example.com", digestFields, None)
+            digestFields = defaultdict(lambda: "...")
+            digested = DigestedCredentials("user", "GET", "example.com",
+                                           digestFields)
+            od = deriveValue(self, "odModule", lambda x: None)
+            od.response = "invalid"
 
             self.assertFalse(record.verifyCredentials(digested))
 
+
+        @withSpecialValue("odModule", DigestAuthModule())
         def test_validODDigest(self):
             record = OpenDirectoryRecord(
                 service               = self.service(),
@@ -136,8 +162,8 @@
                 "response":"123",
                 "algorithm":"md5",
             }
-
-            response = (
+            od = deriveValue(self, "odModule", lambda self: None)
+            od.response = (
                 'Digest username="%(username)s", '
                 'realm="%(realm)s", '
                 'nonce="%(nonce)s", '
@@ -146,9 +172,8 @@
                 'algorithm=%(algorithm)s'
             ) % digestFields
 
-            record.digestcache = {}
-            record.digestcache["/"] = response
-            digested = DigestedCredentials("user", "GET", "example.com", digestFields, None)
+            digested = DigestedCredentials("user", "GET", "example.com",
+                                           digestFields)
 
             self.assertTrue(record.verifyCredentials(digested))
 
@@ -469,5 +494,6 @@
                     "node" : "/Search",
                     "recordTypes" : (DirectoryService.recordType_users, DirectoryService.recordType_groups),
                     "augmentService" : augment.AugmentXMLDB(xmlFiles=()),
-                }
+                },
+                odModule=deriveValue(self, "odModule", lambda x: None)
             )

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -462,7 +462,17 @@
         """
         self.assertEquals(
             set((yield calendaruserproxy.ProxyDBService.getAllMembers())), #@UndefinedVariable
-            set([u'6423F94A-6B76-4A3A-815B-D52CFD77935D', u'8A985493-EE2C-4665-94CF-4DFEA3A89500', u'9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD2', u'both_coasts', u'left_coast', u'non_calendar_group', u'recursive1_coasts', u'recursive2_coasts', u'EC465590-E9E9-4746-ACE8-6C756A49FE4D'])
+            set([
+                u'00599DAF-3E75-42DD-9DB7-52617E79943F',
+                u'6423F94A-6B76-4A3A-815B-D52CFD77935D',
+                u'8A985493-EE2C-4665-94CF-4DFEA3A89500',
+                u'9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD2',
+                u'both_coasts',
+                u'left_coast',
+                u'non_calendar_group',
+                u'recursive1_coasts',
+                u'recursive2_coasts',
+                u'EC465590-E9E9-4746-ACE8-6C756A49FE4D'])
         )
 
 
@@ -470,6 +480,7 @@
     def test_hideDisabledDelegates(self):
         """
         Delegates who are not enabledForLogin are "hidden" from the delegate lists
+        (but groups *are* allowed)
         """
 
         record = self.directoryService.recordWithGUID("EC465590-E9E9-4746-ACE8-6C756A49FE4D")
@@ -477,19 +488,19 @@
         record.enabledForLogin = True
         yield self._groupMembersTest(
             DirectoryService.recordType_users, "delegator", "calendar-proxy-write",
-            ("Occasional Delegate",),
+            ("Occasional Delegate", "Delegate Via Group", "Delegate Group"),
         )
 
         # Login disabled -- no longer shown as a delegate
         record.enabledForLogin = False
         yield self._groupMembersTest(
             DirectoryService.recordType_users, "delegator", "calendar-proxy-write",
-            [],
+            ("Delegate Via Group", "Delegate Group"),
         )
 
         # Login re-enabled -- once again a delegate (it wasn't not removed from proxydb)
         record.enabledForLogin = True
         yield self._groupMembersTest(
             DirectoryService.recordType_users, "delegator", "calendar-proxy-write",
-            ("Occasional Delegate",),
+            ("Occasional Delegate", "Delegate Via Group", "Delegate Group"),
         )

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/ical.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/ical.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -57,8 +57,6 @@
 from pycalendar.timezone import PyCalendarTimezone
 from pycalendar.utcoffsetvalue import PyCalendarUTCOffsetValue
 
-import base64
-
 log = Logger()
 
 iCalendarProductID = "-//CALENDARSERVER.ORG//NONSGML Version 1//EN"
@@ -2979,8 +2977,7 @@
                 # Check that we can lookup this calendar user address - if not
                 # we cannot do anything with it
                 cuaddr = normalizeCUAddr(prop.value())
-                name, guid, cuaddrs = lookupFunction(cuaddr, principalFunction,
-                    config)
+                name, guid, cuaddrs = lookupFunction(cuaddr, principalFunction, config)
                 if guid is None:
                     continue
 
@@ -2989,27 +2986,19 @@
                 if oldemail:
                     oldemail = "mailto:%s" % (oldemail,)
 
-                if toUUID:
-                    # Store the original CUA if http(s) or /path:
-                    if config.Scheduling.Options.V1Compatibility:
-                        if cuaddr.startswith("http") or cuaddr.startswith("/"):
-                            prop.setParameter("CALENDARSERVER-OLD-CUA",
-                                "base64-%s" % (base64.b64encode(prop.value())))
+                # Get any CN parameter
+                oldCN = prop.parameterValue("CN")
 
+                cutype = prop.parameterValue("CUTYPE")
+
+                if toUUID:
                     # Always re-write value to urn:uuid
                     prop.setValue("urn:uuid:%s" % (guid,))
 
                 # If it is already a non-UUID address leave it be
                 elif cuaddr.startswith("urn:uuid:"):
 
-                    # Restore old CUA
-                    oldExternalCUA = prop.parameterValue("CALENDARSERVER-OLD-CUA")
-                    if oldExternalCUA:
-                        if oldExternalCUA.startswith("base64-"):
-                            oldExternalCUA = base64.b64decode(oldExternalCUA[7:])
-                        newaddr = oldExternalCUA
-                        prop.removeParameter("CALENDARSERVER-OLD-CUA")
-                    elif oldemail:
+                    if oldemail:
                         # Use the EMAIL parameter if it exists
                         newaddr = oldemail
                     else:
@@ -3049,9 +3038,17 @@
                     if newaddr:
                         prop.setValue(newaddr)
 
-                # Always re-write the CN parameter
+                # Re-write the CN parameter
                 if name:
-                    prop.setParameter("CN", name)
+                    if name != oldCN:
+                        prop.setParameter("CN", name)
+
+                        # Also adjust any previously matching location property
+                        if cutype == "ROOM":
+                            location = component.getProperty("LOCATION")
+                            if location is not None:
+                                if location.value() == oldCN:
+                                    location.setValue(name)
                 else:
                     prop.removeParameter("CN")
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/propfind.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/propfind.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/propfind.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -219,8 +219,11 @@
 
             # This needed for propfind cache tracking of children changes
             if depth == "1":
-                if resource != self and hasattr(resource, "url"):
-                    request.childCacheURIs.append(resource.url())
+                if resource != self:
+                    if hasattr(resource, "owner_url"):
+                        request.childCacheURIs.append(resource.owner_url())
+                    elif hasattr(resource, "url"):
+                        request.childCacheURIs.append(resource.url())
         else:
             xml_response = davxml.StatusResponse(davxml.HRef(uri), davxml.Status.fromResponseCode(responsecode.FORBIDDEN))
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -20,28 +20,29 @@
 
 __all__ = ["StoreAddressObjectResource"]
 
-import types
-
-from twisted.internet import reactor
-
-from txdav.common.icommondatastore import ReservationError
-
-from twisted.internet.defer import Deferred, inlineCallbacks
-from twisted.internet.defer import returnValue
+from twext.python.log import Logger
 from twext.web2 import responsecode
-from txdav.xml import element as davxml
 from twext.web2.dav.http import ErrorResponse
 from twext.web2.dav.util import joinURL, parentForURL
 from twext.web2.http import HTTPError
 from twext.web2.http import StatusResponse
 from twext.web2.stream import MemoryStream
 
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred, inlineCallbacks
+from twisted.internet.defer import returnValue
+from twisted.python.failure import Failure
+
+from twistedcaldav import customxml
+from twistedcaldav.carddavxml import NoUIDConflict, carddav_namespace
 from twistedcaldav.config import config
-from twistedcaldav.carddavxml import NoUIDConflict, carddav_namespace
-from twistedcaldav import customxml
 from twistedcaldav.vcard import Component
-from twext.python.log import Logger
 
+from txdav.common.icommondatastore import ReservationError
+from txdav.xml import element as davxml
+
+import types
+
 log = Logger()
 
 class StoreAddressObjectResource(object):
@@ -485,12 +486,18 @@
 
             returnValue(response)
 
-        except Exception, err:
+        except Exception:
 
+            # Grab the current exception state here so we can use it in a re-raise - we need this because
+            # an inlineCallback might be called and that raises an exception when it returns, wiping out the
+            # original exception "context".
+            ex = Failure()
+
             if reservation:
                 yield reservation.unreserve()
 
-            raise err
+            # Return the original failure (exception) state
+            ex.raiseException()
 
 
     @inlineCallbacks
@@ -576,9 +583,15 @@
 
             returnValue(response)
 
-        except Exception, err:
+        except Exception:
 
+            # Grab the current exception state here so we can use it in a re-raise - we need this because
+            # an inlineCallback might be called and that raises an exception when it returns, wiping out the
+            # original exception "context".
+            ex = Failure()
+
             if reservation:
                 yield reservation.unreserve()
 
-            raise err
+            # Return the original failure (exception) state
+            ex.raiseException()

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -21,42 +21,41 @@
 
 __all__ = ["StoreCalendarObjectResource"]
 
-import types
-import uuid
-from urlparse import urlparse, urlunparse
-
-from twisted.internet import reactor
-from twisted.internet.defer import Deferred, inlineCallbacks, succeed
-from twisted.internet.defer import returnValue
-from twisted.python import hashlib
-
+from twext.python.log import Logger
+from twext.web2 import responsecode
+from twext.web2.dav.http import ErrorResponse
 from twext.web2.dav.util import joinURL, parentForURL
-from twext.web2 import responsecode
-from txdav.xml import element as davxml
-
 from twext.web2.http import HTTPError
 from twext.web2.http import StatusResponse
 from twext.web2.iweb import IResponse
 from twext.web2.stream import MemoryStream
 
-from twext.python.log import Logger
-from twext.web2.dav.http import ErrorResponse
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred, inlineCallbacks, succeed
+from twisted.internet.defer import returnValue
+from twisted.python import hashlib
+from twisted.python.failure import Failure
 
-from txdav.caldav.icalendarstore import AttachmentStoreValidManagedID
-from txdav.common.icommondatastore import ReservationError
-
+from twistedcaldav import customxml
+from twistedcaldav.caldavxml import caldav_namespace, NoUIDConflict, MaxInstances, MaxAttendeesPerInstance
 from twistedcaldav.config import config
-from twistedcaldav.caldavxml import caldav_namespace, NoUIDConflict, MaxInstances, MaxAttendeesPerInstance
-from twistedcaldav import customxml
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
-
 from twistedcaldav.ical import Component, Property
 from twistedcaldav.instance import TooManyInstancesError, \
     InvalidOverriddenInstanceError
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 from twistedcaldav.scheduling.implicit import ImplicitScheduler
 
+from txdav.caldav.icalendarstore import AttachmentStoreValidManagedID
+from txdav.common.icommondatastore import ReservationError
+from txdav.xml import element as davxml
+
+from urlparse import urlparse, urlunparse
+
+import types
+import uuid
+
 log = Logger()
 
 class StoreCalendarObjectResource(object):
@@ -203,6 +202,7 @@
         self.access = None
         self.hasPrivateComments = False
         self.isScheduleResource = False
+        self.dataChanged = False
 
 
     @inlineCallbacks
@@ -365,6 +365,9 @@
             # Check that moves to shared calendars are OK
             yield self.validCopyMoveOperation()
 
+            # Check location/resource organizer requirement
+            yield self.validLocationResourceOrganizer()
+
             # Check access
             if self.destinationcal and config.EnablePrivateEvents:
                 result = (yield self.validAccess())
@@ -662,6 +665,53 @@
 
 
     @inlineCallbacks
+    def validLocationResourceOrganizer(self):
+        """
+        If the calendar owner is a location or resource, check whether an ORGANIZER property is required.
+        """
+
+        if not self.internal_request:
+            originatorPrincipal = (yield self.destination.ownerPrincipal(self.request))
+            cutype = originatorPrincipal.getCUType() if originatorPrincipal is not None else "INDIVIDUAL"
+            organizer = self.calendar.getOrganizer()
+
+            # Check for an allowed change
+            if organizer is None and (
+                cutype == "ROOM" and not config.Scheduling.Options.AllowLocationWithoutOrganizer or
+                cutype == "RESOURCE" and not config.Scheduling.Options.AllowResourceWithoutOrganizer):
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (calendarserver_namespace, "valid-organizer"),
+                    "Organizer required in calendar data",
+                ))
+
+            # Check for tracking the modifier
+            if organizer is None and (
+                cutype == "ROOM" and config.Scheduling.Options.TrackUnscheduledLocationData or
+                cutype == "RESOURCE" and config.Scheduling.Options.TrackUnscheduledResourceData):
+
+                # Find current principal
+                authz = None
+                authz_principal = self.destinationparent.currentPrincipal(self.request).children[0]
+                if isinstance(authz_principal, davxml.HRef):
+                    principalURL = str(authz_principal)
+                    if principalURL:
+                        authz = (yield self.request.locateResource(principalURL))
+
+                if authz is not None:
+                    prop = Property("X-CALENDARSERVER-MODIFIED-BY", "urn:uuid:%s" % (authz.record.guid,))
+                    prop.setParameter("CN", authz.displayName())
+                    for candidate in authz.calendarUserAddresses():
+                        if candidate.startswith("mailto:"):
+                            prop.setParameter("EMAIL", candidate[7:])
+                            break
+                    self.calendar.replacePropertyInAllComponents(prop)
+                else:
+                    self.calendar.removeAllPropertiesWithName("X-CALENDARSERVER-MODIFIED-BY")
+                self.dataChanged = True
+
+
+    @inlineCallbacks
     def preservePrivateComments(self):
         # Check for private comments on the old resource and the new resource and re-insert
         # ones that are lost.
@@ -769,7 +819,6 @@
         """
 
         # Only relevant if calendar is sharee collection
-        changed = False
         if self.destinationparent.isShareeCollection():
 
             # Get all X-APPLE-DROPBOX's and ATTACH's that are http URIs
@@ -813,59 +862,52 @@
                     uri = uriNormalize(xdropbox.value())
                     if uri:
                         xdropbox.setValue(uri)
-                        changed = True
+                        self.dataChanged = True
                 for attachment in attachments:
                     uri = uriNormalize(attachment.value())
                     if uri:
                         attachment.setValue(uri)
-                        changed = True
+                        self.dataChanged = True
 
-        returnValue(changed)
 
-
     def processAlarms(self):
         """
         Remove duplicate alarms. Add a default alarm if required.
-
-        @return: indicate whether a change was made
-        @rtype: C{bool}
         """
 
         # Remove duplicate alarms
-        changed = False
-        if config.RemoveDuplicateAlarms:
-            changed = self.calendar.hasDuplicateAlarms(doFix=True)
+        if config.RemoveDuplicateAlarms and self.calendar.hasDuplicateAlarms(doFix=True):
+            self.dataChanged = True
 
         # Only if feature enabled
         if not config.EnableDefaultAlarms:
-            return changed
+            return
 
         # Check that we are creating and this is not the inbox
         if not self.destinationcal or self.destination.exists() or self.isiTIP:
-            return changed
+            return
 
         # Never add default alarms to calendar data in shared calendars
         if self.destinationparent.isShareeCollection():
-            return changed
+            return
 
         # Add default alarm for VEVENT and VTODO only
         mtype = self.calendar.mainType().upper()
         if self.calendar.mainType().upper() not in ("VEVENT", "VTODO"):
-            return changed
+            return
         vevent = mtype == "VEVENT"
 
         # Check timed or all-day
         start, _ignore_end = self.calendar.mainComponent(allow_multiple=True).getEffectiveStartEnd()
         if start is None:
             # Yes VTODOs might have no DTSTART or DUE - in this case we do not add a default
-            return changed
+            return
         timed = not start.isDateOnly()
 
         # See if default exists and add using appropriate logic
         alarm = self.destinationparent.getDefaultAlarm(vevent, timed)
-        if alarm:
-            changed = self.calendar.addAlarms(alarm)
-        return changed
+        if alarm and self.calendar.addAlarms(alarm):
+            self.dataChanged = True
 
 
     @inlineCallbacks
@@ -996,11 +1038,14 @@
                 # Cannot do implicit in sharee's shared calendar
                 isShareeCollection = self.destinationparent.isShareeCollection()
                 if isShareeCollection:
-                    raise HTTPError(ErrorResponse(
-                        responsecode.FORBIDDEN,
-                        (calendarserver_namespace, "sharee-privilege-needed",),
-                        description="Sharee's cannot schedule"
-                    ))
+                    scheduler.setSchedulingNotAllowed(
+                        HTTPError,
+                        ErrorResponse(
+                            responsecode.FORBIDDEN,
+                            (calendarserver_namespace, "sharee-privilege-needed",),
+                            description="Sharee's cannot schedule",
+                        ),
+                    )
 
                 new_calendar = (yield scheduler.doImplicitScheduling(self.schedule_tag_match))
                 if new_calendar:
@@ -1221,14 +1266,14 @@
             yield self.replaceMissingToDoProperties()
 
             # Handle sharing dropbox normalization
-            dropboxChanged = (yield self.dropboxPathNormalization())
+            yield self.dropboxPathNormalization()
 
             # Pre-process managed attachments
             if not self.internal_request and not self.attachmentProcessingDone:
                 managed_copied, managed_removed = (yield self.destination.preProcessManagedAttachments(self.calendar))
 
             # Default/duplicate alarms
-            alarmChanged = self.processAlarms()
+            self.processAlarms()
 
             # Do scheduling
             implicit_result = (yield self.doImplicitScheduling())
@@ -1276,7 +1321,7 @@
                 yield self.destination.postProcessManagedAttachments(managed_copied, managed_removed)
 
             # Must not set ETag in response if data changed
-            if did_implicit_action or dropboxChanged or alarmChanged:
+            if did_implicit_action or self.dataChanged:
                 def _removeEtag(request, response):
                     response.headers.removeHeader('etag')
                     return response
@@ -1291,6 +1336,11 @@
 
         except Exception, err:
 
+            # Grab the current exception state here so we can use it in a re-raise - we need this because
+            # an inlineCallback might be called and that raises an exception when it returns, wiping out the
+            # original exception "context".
+            ex = Failure()
+
             if reservation:
                 yield reservation.unreserve()
 
@@ -1313,7 +1363,8 @@
                     "Invalid Managed-ID parameter in calendar data",
                 ))
             else:
-                raise err
+                # Return the original failure (exception) state
+                ex.raiseException()
 
 
     @inlineCallbacks
@@ -1409,6 +1460,11 @@
 
         except Exception, err:
 
+            # Grab the current exception state here so we can use it in a re-raise - we need this because
+            # an inlineCallback might be called and that raises an exception when it returns, wiping out the
+            # original exception "context".
+            ex = Failure()
+
             if reservation:
                 yield reservation.unreserve()
 
@@ -1431,4 +1487,5 @@
                     "Invalid Managed-ID parameter in calendar data",
                 ))
             else:
-                raise err
+                # Return the original failure (exception) state
+                ex.raiseException()

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/caldav/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/caldav/delivery.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/caldav/delivery.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -114,6 +114,8 @@
                     yield recipient.inbox.checkPrivileges(self.scheduler.request, (caldavxml.ScheduleDeliver(),), principal=organizerPrincipal)
                 except AccessDeniedError:
                     log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
+                    if log.willLogAtLevel("debug"):
+                        log.debug("Bare Exception: %s" % (Failure().getTraceback(),))
                     err = HTTPError(ErrorResponse(
                         responsecode.NOT_FOUND,
                         (caldav_namespace, "recipient-permissions"),
@@ -164,6 +166,8 @@
             ))
         except ImplicitProcessorException, e:
             log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
+            if log.willLogAtLevel("debug"):
+                log.debug("%s: %s" % (e, Failure().getTraceback(),))
             err = HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
                 (caldav_namespace, "recipient-permissions"),
@@ -189,6 +193,8 @@
             except:
                 # FIXME: Bare except
                 log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
+                if log.willLogAtLevel("debug"):
+                    log.debug("Bare Exception: %s" % (Failure().getTraceback(),))
                 err = HTTPError(ErrorResponse(
                     responsecode.FORBIDDEN,
                     (caldav_namespace, "recipient-permissions"),
@@ -230,6 +236,8 @@
             ))
         except:
             log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
+            if log.willLogAtLevel("debug"):
+                log.debug("Bare Exception: %s" % (Failure().getTraceback(),))
             err = HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
                 (caldav_namespace, "recipient-permissions"),

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/caldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/caldav/resource.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/caldav/resource.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -40,6 +40,11 @@
 from twistedcaldav.caldavxml import caldav_namespace, Opaque, \
     CalendarFreeBusySet, ScheduleCalendarTransp
 from twistedcaldav.config import config
+# _schedulePrivilegeSet implicitly depends on config being initilialized. The
+# following line is wrong because _schedulePrivilegeSet won't actually use the
+# config file, it will pick up stdconfig whenver it is imported, so this works
+# around that for now.
+__import__("twistedcaldav.stdconfig") # FIXME
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.ical import allowedComponents
 from twistedcaldav.resource import CalDAVResource

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/imip/inbound.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/imip/inbound.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/imip/inbound.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -21,6 +21,7 @@
 from calendarserver.tap.util import FakeRequest
 import email.utils
 from twext.enterprise.dal.record import fromTable
+from twext.enterprise.dal.syntax import Delete
 from twext.enterprise.queue import WorkItem
 from twext.python.log import Logger, LoggingMixIn
 from twisted.application import service
@@ -78,11 +79,15 @@
 
 class IMIPPollingWork(WorkItem, fromTable(schema.IMIP_POLLING_WORK)):
 
-    # FIXME: delete all other polling work items
     # FIXME: purge all old tokens here
+    group = "imip_polling"
 
     @inlineCallbacks
     def doWork(self):
+
+        # Delete all other work items
+        yield Delete(From=self.table, Where=None).on(self.transaction)
+
         mailRetriever = self.transaction._mailRetriever
         if mailRetriever is not None:
             try:
@@ -115,22 +120,25 @@
         self.point = GAIEndpoint(self.reactor, settings.Server,
             settings.Port, contextFactory=contextFactory)
 
-    def startService(self):
-        return self.scheduleNextPoll(seconds=0)
 
-
     def fetchMail(self):
         return self.point.connect(self.factory(self.settings, self.mailReceiver))
 
+
     @inlineCallbacks
     def scheduleNextPoll(self, seconds=None):
         if seconds is None:
             seconds = self.settings["PollingSeconds"]
-        txn = self.store.newTransaction()
-        notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
-        yield txn.enqueue(IMIPPollingWork, notBefore=notBefore)
-        yield txn.commit()
+        yield scheduleNextMailPoll(self.store, seconds)
+        
 
+ at inlineCallbacks
+def scheduleNextMailPoll(store, seconds):
+    txn = store.newTransaction()
+    notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
+    log.debug("Scheduling next mail poll: %s" % (notBefore,))
+    yield txn.enqueue(IMIPPollingWork, notBefore=notBefore)
+    yield txn.commit()
 
 
 class MailReceiver(object):

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/imip/test/test_inbound.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/imip/test/test_inbound.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/imip/test/test_inbound.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -15,6 +15,7 @@
 ##
 
 
+from twistedcaldav.test.util import TestCase
 import email
 from twisted.internet.defer import inlineCallbacks
 from twisted.python.modules import getModule
@@ -24,7 +25,6 @@
 from twistedcaldav.scheduling.imip.inbound import injectMessage
 from twistedcaldav.scheduling.imip.inbound import IMIPReplyWork
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.test.util import TestCase
 from twistedcaldav.test.util import xmlFile
 from txdav.common.datastore.test.util import buildStore
 from calendarserver.tap.util import getRootResource

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/implicit.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/implicit.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -35,6 +35,8 @@
 from twistedcaldav.scheduling.utils import getCalendarObjectForPrincipals
 from twistedcaldav.config import config
 
+import collections
+
 __all__ = [
     "ImplicitScheduler",
 ]
@@ -57,8 +59,34 @@
     def __init__(self):
 
         self.return_status = ImplicitScheduler.STATUS_OK
+        self.allowed_to_schedule = True
 
+    NotAllowedExceptionDetails = collections.namedtuple("NotAllowedExceptionDetails", ("type", "args", "kwargs",))
 
+    def setSchedulingNotAllowed(self, ex, *ex_args, **ex_kwargs):
+        """
+        Set indicator that scheduling is not actually allowed. Pass in exception details to raise.
+
+        @param ex: the exception class to raise
+        @type ex: C{class}
+        @param ex_args: the list of arguments for the exception
+        @type ex_args: C{list}
+        """
+
+        self.not_allowed = ImplicitScheduler.NotAllowedExceptionDetails(ex, ex_args, ex_kwargs)
+        self.allowed_to_schedule = False
+
+
+    def testSchedulingAllowed(self):
+        """
+        Called to raise an exception if scheduling is not allowed. This method should be called
+        any time a valid scheduling operation needs to occur.
+        """
+
+        if not self.allowed_to_schedule:
+            raise self.not_allowed.type(*self.not_allowed.args, **self.not_allowed.kwargs)
+
+
     @inlineCallbacks
     def testImplicitSchedulingPUT(self, request, resource, resource_uri, calendar, internal_request=False):
 
@@ -545,10 +573,6 @@
     @inlineCallbacks
     def doImplicitOrganizer(self):
 
-        # Do access control
-        if not self.internal_request:
-            yield self.doAccessControl(self.organizerPrincipal, True)
-
         self.oldcalendar = None
         self.changed_rids = None
         self.cancelledAttendees = ()
@@ -933,6 +957,13 @@
     @inlineCallbacks
     def scheduleWithAttendees(self):
 
+        # First make sure we are allowed to schedule
+        self.testSchedulingAllowed()
+
+        # Do access control
+        if not self.internal_request:
+            yield self.doAccessControl(self.organizerPrincipal, True)
+
         # First process cancelled attendees
         total = (yield self.processCancels())
 
@@ -1052,10 +1083,6 @@
     @inlineCallbacks
     def doImplicitAttendee(self):
 
-        # Do access control
-        if not self.internal_request:
-            yield self.doAccessControl(self.attendeePrincipal, False)
-
         # Check SCHEDULE-AGENT
         doScheduling = self.checkOrganizerScheduleAgent()
 
@@ -1283,8 +1310,16 @@
         return differ.attendeeMerge(self.attendee)
 
 
+    @inlineCallbacks
     def scheduleWithOrganizer(self, changedRids=None):
 
+        # First make sure we are allowed to schedule
+        self.testSchedulingAllowed()
+
+        # Do access control
+        if not self.internal_request:
+            yield self.doAccessControl(self.attendeePrincipal, False)
+
         if not hasattr(self.request, "extendedLogItems"):
             self.request.extendedLogItems = {}
         self.request.extendedLogItems["itip.reply"] = "reply"
@@ -1292,11 +1327,19 @@
         itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, changedRids=changedRids)
 
         # Send scheduling message
-        return self.sendToOrganizer("REPLY", itipmsg)
+        yield self.sendToOrganizer("REPLY", itipmsg)
 
 
+    @inlineCallbacks
     def scheduleCancelWithOrganizer(self):
 
+        # First make sure we are allowed to schedule
+        self.testSchedulingAllowed()
+
+        # Do access control
+        if not self.internal_request:
+            yield self.doAccessControl(self.attendeePrincipal, False)
+
         if not hasattr(self.request, "extendedLogItems"):
             self.request.extendedLogItems = {}
         self.request.extendedLogItems["itip.reply"] = "cancel"
@@ -1304,7 +1347,7 @@
         itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, force_decline=True)
 
         # Send scheduling message
-        return self.sendToOrganizer("CANCEL", itipmsg)
+        yield self.sendToOrganizer("CANCEL", itipmsg)
 
 
     def sendToOrganizer(self, action, itipmsg):

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/ischedule/utils.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/ischedule/utils.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling/ischedule/utils.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -41,10 +41,13 @@
     @return: a C{set} of IPs
     """
     ips = set()
-    for family in (socket.AF_INET, socket.AF_INET6):
-        results = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
-        for _ignore_family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in results:
-            ips.add(sockaddr[0])
+    # Use AF_UNSPEC rather than iterating (socket.AF_INET, socket.AF_INET6)
+    # because getaddrinfo() will raise an exception if no match is found for
+    # the specified family
+    # TODO: potentially use twext.internet.gaiendpoint instead
+    results = socket.getaddrinfo(host, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
+    for _ignore_family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in results:
+        ips.add(sockaddr[0])
 
     return ips
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/stdconfig.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/stdconfig.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -29,8 +29,8 @@
 from twext.python.log import clearLogLevels, setLogLevelForNamespace
 
 from twistedcaldav import caldavxml, customxml, carddavxml, mkcolxml
-from twistedcaldav.config import ConfigProvider, ConfigurationError
-from twistedcaldav.config import config, _mergeData, fullServerPath
+from twistedcaldav.config import ConfigProvider, ConfigurationError, ConfigDict
+from twistedcaldav.config import config, mergeData, fullServerPath
 from twistedcaldav.util import getPasswordFromKeychain
 from twistedcaldav.util import KeychainAccessError, KeychainPasswordNotFound
 
@@ -41,7 +41,7 @@
 log = Logger()
 
 if platform.isMacOSX():
-    DEFAULT_CONFIG_FILE = "/Library/Server/Calendar and Contacts/Config/caldavd.plist"
+    DEFAULT_CONFIG_FILE = "/Applications/Server.app/Contents/ServerRoot/private/etc/caldavd/caldavd-apple.plist"
 else:
     DEFAULT_CONFIG_FILE = "/etc/caldavd/caldavd.plist"
 
@@ -153,6 +153,7 @@
         },
         "resourceSchema": {
             "resourceInfoAttr": None, # contains location/resource info
+            "autoAcceptGroupAttr": None, # auto accept group
         },
         "partitionSchema": {
             "serverIdAttr": None, # maps to augments server-id
@@ -693,13 +694,16 @@
             "AllowGroupAsOrganizer"               : False, # Allow groups to be Organizers
             "AllowLocationAsOrganizer"            : False, # Allow locations to be Organizers
             "AllowResourceAsOrganizer"            : False, # Allow resources to be Organizers
+            "AllowLocationWithoutOrganizer"       : True, # Allow locations to have events without an Organizer
+            "AllowResourceWithoutOrganizer"       : True, # Allow resources to have events without an Organizer
+            "TrackUnscheduledLocationData"        : True, # Track who the last modifier of an unscheduled location event is
+            "TrackUnscheduledResourceData"        : True, # Track who the last modifier of an unscheduled resource event is
             "LimitFreeBusyAttendees"              : 30, # Maximum number of attendees to request freebusy for
             "AttendeeRefreshBatch"                : 5, # Number of attendees to do batched refreshes: 0 - no batching
             "AttendeeRefreshBatchDelaySeconds"    : 5, # Time after an iTIP REPLY for first batched attendee refresh
             "AttendeeRefreshBatchIntervalSeconds" : 5, # Time between attendee batch refreshes
             "UIDLockTimeoutSeconds"               : 60, # Time for implicit UID lock timeout
             "UIDLockExpirySeconds"                : 300, # Expiration time for UID lock,
-            "V1Compatibility"                     : False, # Allow /path-based CUAs in scheduling replies
             "PrincipalHostAliases"                : [], # Host names matched in http(s) CUAs
             "TimestampAttendeePartStatChanges"    : True, # Add a time stamp when an Attendee changes their PARTSTAT
 
@@ -926,7 +930,6 @@
         "MemcachedPool" : "Default",
         "UpdateSeconds" : 300,
         "ExpireSeconds" : 3600,
-        "LockSeconds" : 300,
         "EnableUpdater" : True,
         "UseExternalProxies" : False,
     },
@@ -974,7 +977,10 @@
     # America/Los_Angeles.
     "DefaultTimezone" : "",
 
+    # These two aren't relative to ConfigRoot:
     "Includes": [], # Other plists to parse after this one
+    "WritableConfigFile" : "", # which config file calendarserver_config should
+        # write to for changes; empty string means the main config file.
 }
 
 
@@ -1002,15 +1008,19 @@
         configDict = {}
         if self._configFileName:
             configDict = self._parseConfigFromFile(self._configFileName)
+        configDict = ConfigDict(configDict)
         # Now check for Includes and parse and add each of those
         if "Includes" in configDict:
-            configRoot = os.path.join(configDict.ServerRoot, configDict.ConfigRoot)
             for include in configDict.Includes:
-                path = _expandPath(fullServerPath(configRoot, include))
-                additionalDict = self._parseConfigFromFile(path)
-                if additionalDict:
-                    log.info("Adding configuration from file: '%s'" % (path,))
-                    configDict.update(additionalDict)
+                # Includes are not relative to ConfigRoot
+                path = _expandPath(include)
+                if os.path.exists(path):
+                    additionalDict = ConfigDict(self._parseConfigFromFile(path))
+                    if additionalDict:
+                        log.info("Adding configuration from file: '%s'" % (path,))
+                        mergeData(configDict, additionalDict)
+                else:
+                    log.warn("Missing configuration file: '%s'" % (path,))
         return configDict
 
 
@@ -1066,7 +1076,6 @@
     Post-update configuration hook for making all configured paths relative to
     their respective root directories rather than the current working directory.
     """
-
     # Remove possible trailing slash from ServerRoot
     try:
         configDict["ServerRoot"] = configDict["ServerRoot"].rstrip("/")
@@ -1129,7 +1138,7 @@
         if dsType == configDict.DirectoryService.type:
             oldParams = configDict.DirectoryService.params
             newParams = items.DirectoryService.get("params", {})
-            _mergeData(oldParams, newParams)
+            mergeData(oldParams, newParams)
         else:
             if dsType in DEFAULT_SERVICE_PARAMS:
                 configDict.DirectoryService.params = copy.deepcopy(DEFAULT_SERVICE_PARAMS[dsType])
@@ -1159,7 +1168,7 @@
         if dsType == configDict.ResourceService.type:
             oldParams = configDict.ResourceService.params
             newParams = items.ResourceService.get("params", {})
-            _mergeData(oldParams, newParams)
+            mergeData(oldParams, newParams)
         else:
             if dsType in DEFAULT_RESOURCE_PARAMS:
                 configDict.ResourceService.params = copy.deepcopy(DEFAULT_RESOURCE_PARAMS[dsType])
@@ -1191,7 +1200,7 @@
         if dsType == configDict.DirectoryAddressBook.type:
             oldParams = configDict.DirectoryAddressBook.params
             newParams = items["DirectoryAddressBook"].get("params", {})
-            _mergeData(oldParams, newParams)
+            mergeData(oldParams, newParams)
         else:
             if dsType in directoryAddressBookBackingServiceDefaultParams:
                 configDict.DirectoryAddressBook.params = copy.deepcopy(directoryAddressBookBackingServiceDefaultParams[dsType])
@@ -1202,7 +1211,7 @@
         if param not in directoryAddressBookBackingServiceDefaultParams[dsType]:
             raise ConfigurationError("Parameter %s is not supported by service %s" % (param, dsType))
 
-    _mergeData(configDict, items)
+    mergeData(configDict, items)
 
     for param in tuple(configDict.DirectoryAddressBook.params):
         if param not in directoryAddressBookBackingServiceDefaultParams[configDict.DirectoryAddressBook.type]:
@@ -1365,10 +1374,20 @@
             ):
                 if not service[protocol]["Topic"]:
                     certPath = service[protocol]["CertificatePath"]
-                    if certPath and os.path.exists(certPath):
-                        topic = getAPNTopicFromCertificate(certPath)
-                        service[protocol]["Topic"] = topic
+                    if certPath:
+                        if os.path.exists(certPath):
+                            topic = getAPNTopicFromCertificate(certPath)
+                            service[protocol]["Topic"] = topic
+                        else:
+                            log.error("APNS certificate not found: %s" %
+                                (certPath,))
+                    else:
+                        log.error("APNS certificate path not specified")
 
+                if not service[protocol]["Topic"]:
+                    log.error("APNS cannot proceed; disabling APNS")
+                    service["Enabled"] = False
+
                 # If we already have the cert passphrase, don't fetch it again
                 if service[protocol]["Passphrase"]:
                     continue
@@ -1378,13 +1397,13 @@
                 try:
                     passphrase = getPasswordFromKeychain(accountName)
                     service[protocol]["Passphrase"] = passphrase
-                    log.info("%s APN certificate passphrase retreived from keychain" % (protocol,))
+                    log.info("%s APNS certificate passphrase retreived from keychain" % (protocol,))
                 except KeychainAccessError:
                     # The system doesn't support keychain
                     pass
                 except KeychainPasswordNotFound:
                     # The password doesn't exist in the keychain.
-                    log.info("%s APN certificate passphrase not found in keychain" % (protocol,))
+                    log.info("%s APNS certificate passphrase not found in keychain" % (protocol,))
 
 
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/storebridge.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/storebridge.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -265,6 +265,13 @@
         return joinURL(self._parentResource.url(), self._name, "/")
 
 
+    def owner_url(self):
+        if self.isShareeCollection():
+            return joinURL(self._share.url(), "/")
+        else:
+            return self.url()
+
+
     def parentResource(self):
         return self._parentResource
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_config.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_config.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -17,7 +17,7 @@
 from twext.python.plistlib import writePlist #@UnresolvedImport
 from twext.python.log import logLevelForNamespace
 
-from twistedcaldav.config import config, ConfigDict
+from twistedcaldav.config import config, ConfigDict, mergeData
 from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.stdconfig import DEFAULT_CONFIG, PListConfigProvider,\
     RELATIVE_PATHS
@@ -371,6 +371,40 @@
         configDict._x = "X"
         self.assertEquals(configDict._x, "X")
 
+    def test_mergeData(self):
+        """
+        Verify we don't lose keys which are present in the old but not
+        replaced in the new.
+        """
+        old = ConfigDict({
+            "Scheduling" : ConfigDict({
+                "iMIP" : ConfigDict({
+                    "Enabled" : True,
+                    "Receiving" : ConfigDict({
+                        "Username" : "xyzzy",
+                        "Server" : "example.com",
+                    }),
+                    "Sending" : ConfigDict({
+                        "Username" : "plugh",
+                    }),
+                    "AddressPatterns" : ["mailto:.*"],
+                }),
+            }),
+        })
+        new = ConfigDict({
+            "Scheduling" : ConfigDict({
+                "iMIP" : ConfigDict({
+                    "Enabled" : False,
+                    "Receiving" : ConfigDict({
+                        "Username" : "changed",
+                    }),
+                }),
+            }),
+        })
+        mergeData(old, new)
+        self.assertEquals(old.Scheduling.iMIP.Receiving.Server, "example.com")
+        self.assertEquals(old.Scheduling.iMIP.Sending.Username, "plugh")
+
     def test_SimpleInclude(self):
 
         testConfigMaster = """<?xml version="1.0" encoding="UTF-8"?>

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_icalendar.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_icalendar.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -20,7 +20,6 @@
 
 from twisted.trial.unittest import SkipTest
 
-from twistedcaldav.config import config
 from twistedcaldav.ical import Component, Property, InvalidICalendarDataError, \
     normalizeCUAddress
 from twistedcaldav.instance import InvalidOverriddenInstanceError
@@ -7323,7 +7322,6 @@
     def test_normalizeCalendarUserAddressesFromUUID(self):
         """
         Ensure mailto is preferred, followed by path form, then http form.
-        If CALENDARSERVER-OLD-CUA parameter is present, restore that value.
         """
 
         data = """BEGIN:VCALENDAR
@@ -7335,7 +7333,6 @@
 ATTENDEE:urn:uuid:foo
 ATTENDEE:urn:uuid:bar
 ATTENDEE:urn:uuid:baz
-ATTENDEE;CALENDARSERVER-OLD-CUA="base64-aHR0cDovL2V4YW1wbGUuY29tL3ByaW5jaXBhbHMvdXNlcnMvYnV6":urn:uuid:buz
 DTSTAMP:20071114T000000Z
 END:VEVENT
 END:VCALENDAR
@@ -7361,11 +7358,6 @@
                     "baz",
                     ("urn:uuid:baz", "http://example.com/baz")
                 ),
-                "urn:uuid:buz" : (
-                    "Buz",
-                    "buz",
-                    ("urn:uuid:buz",)
-                ),
             }[cuaddr]
 
         component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=False)
@@ -7376,11 +7368,9 @@
             component.getAttendeeProperty(("/foo",)).value())
         self.assertEquals("http://example.com/baz",
             component.getAttendeeProperty(("http://example.com/baz",)).value())
-        self.assertEquals("http://example.com/principals/users/buz",
-            component.getAttendeeProperty(("http://example.com/principals/users/buz",)).value())
 
 
-    def test_normalizeCalendarUserAddressesToUUID(self):
+    def test_normalizeCalendarUserAddressesAndLocationChange(self):
         """
         Ensure http(s) and /path CUA values are tucked away into the property
         using CALENDARSERVER-OLD-CUA parameter.
@@ -7393,7 +7383,9 @@
 UID:12345-67890
 DTSTART:20071114T000000Z
 ATTENDEE:/principals/users/foo
-ATTENDEE:http://example.com/principals/users/buz
+ATTENDEE:http://example.com/principals/users/bar
+ATTENDEE;CN=Buzz;CUTYPE=ROOM:http://example.com/principals/locations/buzz
+LOCATION:Buzz
 DTSTAMP:20071114T000000Z
 END:VEVENT
 END:VCALENDAR
@@ -7409,29 +7401,134 @@
                     "foo",
                     ("urn:uuid:foo",)
                 ),
-                "http://example.com/principals/users/buz" : (
-                    "Buz",
-                    "buz",
-                    ("urn:uuid:buz",)
+                "http://example.com/principals/users/bar" : (
+                    "Bar",
+                    "bar",
+                    ("urn:uuid:bar",)
                 ),
+                "http://example.com/principals/locations/buzz" : (
+                    "{Restricted} Buzz",
+                    "buzz",
+                    ("urn:uuid:buzz",)
+                ),
             }[cuaddr]
 
-        self.patch(config.Scheduling.Options, "V1Compatibility", True)
         component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
 
-        # /principal CUAs are stored in CALENDARSERVER-OLD-CUA
-        prop = component.getAttendeeProperty(("urn:uuid:foo",))
-        self.assertEquals("urn:uuid:foo", prop.value())
-        self.assertEquals(prop.parameterValue("CALENDARSERVER-OLD-CUA"),
-            "base64-L3ByaW5jaXBhbHMvdXNlcnMvZm9v")
+        # Location value changed
+        prop = component.mainComponent().getProperty("LOCATION")
+        self.assertEquals(prop.value(), "{Restricted} Buzz")
+        prop = component.getAttendeeProperty(("urn:uuid:buzz",))
+        self.assertEquals("urn:uuid:buzz", prop.value())
+        self.assertEquals(prop.parameterValue("CN"), "{Restricted} Buzz")
 
-        # http CUAs are stored in CALENDARSERVER-OLD-CUA
-        prop = component.getAttendeeProperty(("urn:uuid:buz",))
-        self.assertEquals("urn:uuid:buz", prop.value())
-        self.assertEquals(prop.parameterValue("CALENDARSERVER-OLD-CUA"),
-            "base64-aHR0cDovL2V4YW1wbGUuY29tL3ByaW5jaXBhbHMvdXNlcnMvYnV6")
 
+    def test_normalizeCalendarUserAddressesAndLocationNoChange(self):
+        """
+        Ensure http(s) and /path CUA values are tucked away into the property
+        using CALENDARSERVER-OLD-CUA parameter.
+        """
 
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+DTSTART:20071114T000000Z
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ATTENDEE:/principals/users/foo
+ATTENDEE:http://example.com/principals/users/bar
+ATTENDEE;CN=Buzz;CUTYPE=ROOM:http://example.com/principals/locations/buzz
+LOCATION:Fuzz
+DTSTAMP:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data)
+
+
+        def lookupFunction(cuaddr, ignored1, ignored2):
+            return {
+                "/principals/users/foo" : (
+                    "Foo",
+                    "foo",
+                    ("urn:uuid:foo",)
+                ),
+                "http://example.com/principals/users/bar" : (
+                    "Bar",
+                    "bar",
+                    ("urn:uuid:bar",)
+                ),
+                "http://example.com/principals/locations/buzz" : (
+                    "{Restricted} Buzz",
+                    "buzz",
+                    ("urn:uuid:buzz",)
+                ),
+            }[cuaddr]
+
+        component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
+
+        # Location value changed
+        prop = component.mainComponent().getProperty("LOCATION")
+        self.assertEquals(prop.value(), "Fuzz")
+        prop = component.getAttendeeProperty(("urn:uuid:buzz",))
+        self.assertEquals("urn:uuid:buzz", prop.value())
+        self.assertEquals(prop.parameterValue("CN"), "{Restricted} Buzz")
+
+
+    def test_normalizeCalendarUserAddressesAndLocationNoChangeOtherCUType(self):
+        """
+        Ensure http(s) and /path CUA values are tucked away into the property
+        using CALENDARSERVER-OLD-CUA parameter.
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+DTSTART:20071114T000000Z
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ATTENDEE:/principals/users/foo
+ATTENDEE:http://example.com/principals/users/bar
+ATTENDEE;CN=Buzz;CUTYPE=RESOURCE:http://example.com/principals/locations/buzz
+LOCATION:Buzz
+DTSTAMP:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data)
+
+
+        def lookupFunction(cuaddr, ignored1, ignored2):
+            return {
+                "/principals/users/foo" : (
+                    "Foo",
+                    "foo",
+                    ("urn:uuid:foo",)
+                ),
+                "http://example.com/principals/users/bar" : (
+                    "Bar",
+                    "bar",
+                    ("urn:uuid:bar",)
+                ),
+                "http://example.com/principals/locations/buzz" : (
+                    "{Restricted} Buzz",
+                    "buzz",
+                    ("urn:uuid:buzz",)
+                ),
+            }[cuaddr]
+
+        component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
+
+        # Location value changed
+        prop = component.mainComponent().getProperty("LOCATION")
+        self.assertEquals(prop.value(), "Buzz")
+        prop = component.getAttendeeProperty(("urn:uuid:buzz",))
+        self.assertEquals("urn:uuid:buzz", prop.value())
+        self.assertEquals(prop.parameterValue("CN"), "{Restricted} Buzz")
+
+
     def test_serializationCaching(self):
 
         data = """BEGIN:VCALENDAR

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_validation.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_validation.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_validation.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -23,6 +23,7 @@
 # to address this to use system twisted.
 from twext.web2.test.test_server import SimpleRequest
 from twext.web2.http import HTTPError
+from twext.web2.resource import Resource
 
 from twistedcaldav.config import config
 from twistedcaldav.ical import Component, Property
@@ -31,16 +32,20 @@
 from twistedcaldav.resource import CalDAVResource
 
 class InMemoryCalendarObjectResource(CalDAVResource):
-    
+
     def exists(self):
         return hasattr(self, "_data") and self._data is not None
 
+
     def iCalendarForUser(self, user):
         return self._data
-    
+
+
     def setData(self, data):
         self._data = data
 
+
+
 class TestCopyMoveValidation(TestCase):
     """
     Tests for the validation code in L{twistedcaldav.method.put_common}.
@@ -52,11 +57,12 @@
         """
 
         self.destination = InMemoryCalendarObjectResource()
-        self.destination.name = lambda : '1'
+        self.destination.name = lambda : 'bar'
         self.destinationParent = CalDAVResource()
-        self.destinationParent.name = lambda : '2'
+        self.destinationParent.name = lambda : 'foo'
         self.destinationParent.isSupportedComponent = lambda x: True
 
+
     def _getSampleCalendar(self):
         return Component.fromString("""BEGIN:VCALENDAR
 VERSION:2.0
@@ -72,9 +78,13 @@
 END:VCALENDAR
 """)
 
+
     def _getStorer(self, calendar):
         self.sampleCalendar = calendar
         req = SimpleRequest(None, "COPY", "http://example.com/foo/bar")
+        req._rememberResource(self.destination, "/foo/bar")
+        req._rememberResource(self.destinationParent, "/foo/")
+        req._rememberResource(Resource(), "/")
         self.storer = StoreCalendarObjectResource(
             req,
             destination=self.destination,
@@ -83,7 +93,8 @@
             calendar=self.sampleCalendar
         )
         return self.storer
-                
+
+
     @inlineCallbacks
     def test_simpleValidRequest(self):
         """
@@ -110,7 +121,7 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
 
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
@@ -143,7 +154,7 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance - 5):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
 
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
@@ -155,7 +166,7 @@
         config.MaxAttendeesPerInstance -= 10
         eventComponent.addProperty(
             Property("ATTENDEE", "mailto:user-extra at example.com"))
-        
+
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
         except HTTPError, err:
@@ -187,7 +198,7 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance - 5):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
 
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
@@ -197,7 +208,7 @@
 
         # Now reduce the limit and try to store without any additional attendees.
         config.MaxAttendeesPerInstance -= 10
-        
+
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
         except HTTPError:
@@ -209,8 +220,8 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance + 2):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
-        
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
+
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
         except HTTPError:

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/util.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/util.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -19,7 +19,7 @@
 import os
 import xattr
 
-from calendarserver.provision.root import RootResource
+from twistedcaldav.stdconfig import config
 
 from twisted.python.failure import Failure
 from twisted.internet.base import DelayedCall
@@ -34,7 +34,6 @@
 
 from twistedcaldav import memcacher
 from twistedcaldav.bind import doBind
-from twistedcaldav.config import config
 from twistedcaldav.directory import augment
 from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
 from twistedcaldav.directory.calendar import (
@@ -48,6 +47,8 @@
 from txdav.common.datastore.test.util import deriveQuota
 from txdav.common.datastore.file import CommonDataStore
 
+from calendarserver.provision.root import RootResource
+
 from twext.python.log import Logger
 
 log = Logger()

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/upgrade.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/upgrade.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/upgrade.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -39,7 +39,9 @@
 from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
 from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
-from twistedcaldav.directory.directory import DirectoryService, GroupMembershipCacheUpdater
+from twistedcaldav.directory.directory import DirectoryService
+from twistedcaldav.directory.directory import GroupMembershipCacheUpdater
+from twistedcaldav.directory.directory import scheduleNextGroupCachingUpdate
 from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
 from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
@@ -67,6 +69,7 @@
 
 from twext.python.parallel import Parallelizer
 from twistedcaldav.scheduling.imip.mailgateway import migrateTokensToStore
+from twistedcaldav.scheduling.imip.inbound import scheduleNextMailPoll
 
 
 deadPropertyXattrPrefix = namedAny(
@@ -1069,11 +1072,14 @@
                 proxydb = proxydbClass(**self.config.ProxyDBService.params)
 
             updater = GroupMembershipCacheUpdater(proxydb,
-                directory, self.config.GroupCaching.ExpireSeconds,
-                self.config.GroupCaching.LockSeconds,
+                directory,
+                self.config.GroupCaching.UpdateSeconds,
+                self.config.GroupCaching.ExpireSeconds,
                 namespace=self.config.GroupCaching.MemcachedPool,
                 useExternalProxies=self.config.GroupCaching.UseExternalProxies)
             yield updater.updateCache(fast=True)
+            # Set in motion the work queue based updates:
+            yield scheduleNextGroupCachingUpdate(self.store, 0)
 
             uid, gid = getCalendarServerIDs(self.config)
             dbPath = os.path.join(self.config.DataRoot, "proxies.sqlite")
@@ -1087,6 +1093,9 @@
 
         # Migrate mail tokens from sqlite to store
         yield migrateTokensToStore(self.config.DataRoot, self.store)
+        # Set mail polling in motion
+        if self.config.Scheduling.iMIP.Enabled:
+            yield scheduleNextMailPoll(self.store, 0)
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/util.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/util.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -64,6 +64,7 @@
 
         return int(ncpu.value)
 
+
     def getMemorySize():
         """
         Returns the physical amount of RAM installed, in bytes
@@ -91,6 +92,7 @@
     def getNCPU():
         return libc.get_nprocs()
 
+
     def getMemorySize():
         return libc.getpagesize() * libc.get_phys_pages()
 
@@ -103,9 +105,12 @@
 
         raise NotImplementedError("getNCPU not supported on %s%s" % (sys.platform, msg))
 
+
     def getMemorySize():
         raise NotImplementedError("getMemorySize not yet supported on %s" % (sys.platform))
 
+
+
 ##
 # Module management
 ##
@@ -138,6 +143,8 @@
             raise
     return wrapper
 
+
+
 ##
 # Helpers
 ##
@@ -149,6 +156,7 @@
     def __init__(self, state=False):
         self._state = bool(state)
 
+
     def state(self):
         """
         @return: the current state
@@ -157,11 +165,15 @@
         self._state = not state
         return state
 
+
+
 def utf8String(s):
     if isinstance(s, unicode):
         s = s.encode("utf-8")
     return s
 
+
+
 ##
 # Keychain access
 ##
@@ -171,6 +183,8 @@
     Exception raised when the password does not exist
     """
 
+
+
 class KeychainAccessError(Exception):
     """
     Exception raised when not able to access keychain
@@ -205,7 +219,6 @@
 
 
 
-
 ##
 # Digest/Basic-capable HTTP GET factory
 ##
@@ -269,6 +282,8 @@
 
     return HA1.encode('hex')
 
+
+
 # DigestCalcResponse
 def calcResponse(
     HA1,
@@ -306,9 +321,13 @@
     respHash = m.digest().encode('hex')
     return respHash
 
+
+
 class Unauthorized(Exception):
     pass
 
+
+
 class AuthorizedHTTPGetter(client.HTTPPageGetter, LoggingMixIn):
 
     def handleStatus_401(self):
@@ -414,8 +433,8 @@
 
         elif basicAvailable:
             basicauth = "%s:%s" % (user, pswd)
-            basicauth = "Basic " + base64.encodestring( basicauth )
-            basicauth = basicauth.replace( "\n", "" )
+            basicauth = "Basic " + base64.encodestring(basicauth)
+            basicauth = basicauth.replace("\n", "")
 
             self.factory.headers['Authorization'] = basicauth
 
@@ -432,7 +451,6 @@
 
             return self.factory.deferred
 
-
         else:
             self.factory.deferred.errback(failure.Failure(Unauthorized("Mail gateway not able to process reply; calendar server returned 401 and doesn't support basic or digest")))
             return self.factory.deferred
@@ -464,11 +482,6 @@
         # to single-quotes.
         fullName = rec.fullName.replace('"', "'")
 
-        # TODO: remove V1Compatibility when V1 migration is complete
-        if config.Scheduling.Options.V1Compatibility:
-            # Allow /principals-form CUA
-            cuas = principal.calendarUserAddresses()
-        else:
-            cuas = principal.record.calendarUserAddresses
+        cuas = principal.record.calendarUserAddresses
 
         return (fullName, rec.guid, cuas)

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/base/datastore/dbapiclient.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/base/datastore/dbapiclient.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/base/datastore/dbapiclient.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -39,7 +39,7 @@
     # however, process-global; after the first call to connect(), all
     # subsequent connections inherit this encoding even if the environment
     # variable changes.) -glyph
-    os.environ['NLS_LANG'] = '.UTF8'
+    os.environ['NLS_LANG'] = '.AL32UTF8'
     import cx_Oracle
 except ImportError:
     cx_Oracle = None

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/base/propertystore/xattr.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/base/propertystore/xattr.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/base/propertystore/xattr.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -47,7 +47,7 @@
 # expose.  Its value is 93.
 #
 
-if sys.platform in ("darwin", "freebsd8"):
+if sys.platform in ("darwin", "freebsd8", "freebsd9"):
     _ERRNO_NO_ATTR = getattr(errno, "ENOATTR", 93)
 else:
     _ERRNO_NO_ATTR = errno.ENODATA

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/delivery.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/delivery.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -136,6 +136,8 @@
             ))
         except ImplicitProcessorException, e:
             log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
+            if log.willLogAtLevel("debug"):
+                log.debug("%s: %s" % (e, Failure().getTraceback(),))
             err = HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
                 (caldav_namespace, "recipient-permissions"),
@@ -151,6 +153,8 @@
             except Exception as e:
                 # FIXME: Bare except
                 log.err("Could not store data in Inbox : %s %s" % (recipient.inbox, e,))
+                if log.willLogAtLevel("debug"):
+                    log.debug("Bare Exception: %s" % (Failure().getTraceback(),))
                 err = HTTPError(ErrorResponse(
                     responsecode.FORBIDDEN,
                     (caldav_namespace, "recipient-permissions"),
@@ -192,6 +196,8 @@
             ))
         except:
             log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
+            if log.willLogAtLevel("debug"):
+                log.debug("Bare Exception: %s" % (Failure().getTraceback(),))
             err = HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
                 (caldav_namespace, "recipient-permissions"),

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/resource.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/resource.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -40,6 +40,11 @@
 from twistedcaldav.caldavxml import caldav_namespace, Opaque, \
     CalendarFreeBusySet, ScheduleCalendarTransp
 from twistedcaldav.config import config
+# _schedulePrivilegeSet implicitly depends on config being initialized. The
+# following line is wrong because _schedulePrivilegeSet won't actually use the
+# config file, it will pick up stdconfig whenever it is imported, so this works
+# around that for now.
+__import__("twistedcaldav.stdconfig") # FIXME
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.ical import allowedComponents
 from twistedcaldav.resource import CalDAVResource

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/imip/inbound.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/imip/inbound.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/imip/inbound.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -21,6 +21,7 @@
 from calendarserver.tap.util import FakeRequest
 import email.utils
 from twext.enterprise.dal.record import fromTable
+from twext.enterprise.dal.syntax import Delete
 from twext.enterprise.queue import WorkItem
 from twext.python.log import Logger, LoggingMixIn
 from twisted.application import service
@@ -80,11 +81,15 @@
 
 class IMIPPollingWork(WorkItem, fromTable(schema.IMIP_POLLING_WORK)):
 
-    # FIXME: delete all other polling work items
     # FIXME: purge all old tokens here
+    group = "imip_polling"
 
     @inlineCallbacks
     def doWork(self):
+
+        # Delete all other work items
+        yield Delete(From=self.table, Where=None).on(self.transaction)
+
         mailRetriever = self.transaction._mailRetriever
         if mailRetriever is not None:
             try:
@@ -119,10 +124,6 @@
             settings.Port, contextFactory=contextFactory)
 
 
-    def startService(self):
-        return self.scheduleNextPoll(seconds=0)
-
-
     def fetchMail(self):
         return self.point.connect(self.factory(self.settings, self.mailReceiver))
 
@@ -131,13 +132,20 @@
     def scheduleNextPoll(self, seconds=None):
         if seconds is None:
             seconds = self.settings["PollingSeconds"]
-        txn = self.store.newTransaction()
-        notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
-        yield txn.enqueue(IMIPPollingWork, notBefore=notBefore)
-        yield txn.commit()
+        yield scheduleNextMailPoll(self.store, seconds)
 
 
 
+ at inlineCallbacks
+def scheduleNextMailPoll(store, seconds):
+    txn = store.newTransaction()
+    notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
+    log.debug("Scheduling next mail poll: %s" % (notBefore,))
+    yield txn.enqueue(IMIPPollingWork, notBefore=notBefore)
+    yield txn.commit()
+
+
+
 class MailReceiver(object):
 
     NO_TOKEN = 0

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -15,6 +15,7 @@
 ##
 
 
+from twistedcaldav.test.util import TestCase
 import email
 from twisted.internet.defer import inlineCallbacks
 from twisted.python.modules import getModule
@@ -24,7 +25,6 @@
 from twistedcaldav.scheduling.imip.inbound import injectMessage
 from twistedcaldav.scheduling.imip.inbound import IMIPReplyWork
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.test.util import TestCase
 from twistedcaldav.test.util import xmlFile
 from txdav.common.datastore.test.util import buildStore
 from calendarserver.tap.util import getRootResource

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/implicit.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/implicit.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -35,6 +35,8 @@
 from txdav.caldav.datastore.scheduling.itip import iTipGenerator, iTIPRequestStatus
 from txdav.caldav.datastore.scheduling.utils import getCalendarObjectForPrincipals
 
+import collections
+
 __all__ = [
     "ImplicitScheduler",
 ]
@@ -58,8 +60,34 @@
 
         self.return_status = ImplicitScheduler.STATUS_OK
         self.logItems = {}
+        self.allowed_to_schedule = True
 
+    NotAllowedExceptionDetails = collections.namedtuple("NotAllowedExceptionDetails", ("type", "args", "kwargs",))
 
+    def setSchedulingNotAllowed(self, ex, *ex_args, **ex_kwargs):
+        """
+        Set indicator that scheduling is not actually allowed. Pass in exception details to raise.
+
+        @param ex: the exception class to raise
+        @type ex: C{class}
+        @param ex_args: the list of arguments for the exception
+        @type ex_args: C{list}
+        """
+
+        self.not_allowed = ImplicitScheduler.NotAllowedExceptionDetails(ex, ex_args, ex_kwargs)
+        self.allowed_to_schedule = False
+
+
+    def testSchedulingAllowed(self):
+        """
+        Called to raise an exception if scheduling is not allowed. This method should be called
+        any time a valid scheduling operation needs to occur.
+        """
+
+        if not self.allowed_to_schedule:
+            raise self.not_allowed.type(*self.not_allowed.args, **self.not_allowed.kwargs)
+
+
     @inlineCallbacks
     def testImplicitSchedulingPUT(self, parent, resource, calendar, internal_request=False):
         """
@@ -862,6 +890,9 @@
     @inlineCallbacks
     def scheduleWithAttendees(self):
 
+        # First make sure we are allowed to schedule
+        self.testSchedulingAllowed()
+
         # First process cancelled attendees
         total = (yield self.processCancels())
 
@@ -1208,6 +1239,9 @@
 
     def scheduleWithOrganizer(self, changedRids=None):
 
+        # First make sure we are allowed to schedule
+        self.testSchedulingAllowed()
+
         self.logItems["itip.reply"] = "reply"
 
         itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, changedRids=changedRids)
@@ -1218,6 +1252,9 @@
 
     def scheduleCancelWithOrganizer(self):
 
+        # First make sure we are allowed to schedule
+        self.testSchedulingAllowed()
+
         self.logItems["itip.reply"] = "cancel"
 
         itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, force_decline=True)

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/ischedule/utils.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/ischedule/utils.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/ischedule/utils.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -41,10 +41,13 @@
     @return: a C{set} of IPs
     """
     ips = set()
-    for family in (socket.AF_INET, socket.AF_INET6):
-        results = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
-        for _ignore_family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in results:
-            ips.add(sockaddr[0])
+    # Use AF_UNSPEC rather than iterating (socket.AF_INET, socket.AF_INET6)
+    # because getaddrinfo() will raise an exception if no match is found for
+    # the specified family
+    # TODO: potentially use twext.internet.gaiendpoint instead
+    results = socket.getaddrinfo(host, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
+    for _ignore_family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in results:
+        ips.add(sockaddr[0])
 
     return ips
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -70,7 +70,8 @@
     AttachmentMigrationFailed, AttachmentDropboxNotAllowed, \
     TooManyAttendeesError, InvalidComponentTypeError, InvalidCalendarAccessError, \
     InvalidUIDError, UIDExistsError, ResourceDeletedError, \
-    AttendeeAllowedError, InvalidPerUserDataMerge, ComponentUpdateState
+    AttendeeAllowedError, InvalidPerUserDataMerge, ComponentUpdateState, \
+    ValidOrganizerError, ShareeAllowedError
 from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, ECALENDARTYPE
@@ -322,10 +323,19 @@
         return self.principal_uid
 
 
+    def shortNames(self):
+        return [self.principal_uid, ]
+
+
     def fullName(self):
         return "%s %s" % (self.principal_uid[:4], self.principal_uid[4:])
 
 
+    def displayName(self):
+        fullName = self.fullName()
+        return fullName if fullName else self.shortNames()[0]
+
+
     def calendarUserAddresses(self):
         return self.cuaddrs
 
@@ -1392,8 +1402,12 @@
             # calendar data
             component.normalizeCalendarUserAddresses(normalizationLookup, self.calendar().viewerHome().principalForCalendarUserAddress)
 
+        # Check location/resource organizer requirement
+        yield self.validLocationResourceOrganizer(component, inserting, update_state)
+
         # Check access
-        yield self.validAccess(component, inserting, update_state)
+        if config.EnablePrivateEvents:
+            yield self.validAccess(component, inserting, update_state)
 
 
     def validIfScheduleMatch(self, etag_match, schedule_tag, update_state):
@@ -1471,6 +1485,43 @@
                     raise TooManyAttendeesError("Attendee list size %d is larger than allowed limit %d" % (attendeeListLength, config.MaxAttendeesPerInstance))
 
 
+    @inlineCallbacks
+    def validLocationResourceOrganizer(self, component, inserting, update_state):
+        """
+        If the calendar owner is a location or resource, check whether an ORGANIZER property is required.
+        """
+
+        if update_state == ComponentUpdateState.NORMAL:
+            originatorPrincipal = (yield self.calendar().ownerHome().principal())
+            cutype = originatorPrincipal.getCUType() if originatorPrincipal is not None else "INDIVIDUAL"
+            organizer = component.getOrganizer()
+
+            # Check for an allowed change
+            if organizer is None and (
+                cutype == "ROOM" and not config.Scheduling.Options.AllowLocationWithoutOrganizer or
+                cutype == "RESOURCE" and not config.Scheduling.Options.AllowResourceWithoutOrganizer):
+                raise ValidOrganizerError("Organizer required in calendar data for a %s" % (cutype.lower(),))
+
+            # Check for tracking the modifier
+            if organizer is None and (
+                cutype == "ROOM" and config.Scheduling.Options.TrackUnscheduledLocationData or
+                cutype == "RESOURCE" and config.Scheduling.Options.TrackUnscheduledResourceData):
+
+                # Find current principal and update modified by details
+                if hasattr(self._txn, "_authz_uid"):
+                    authz = (yield self.calendar().ownerHome().principalForUID(self._txn._authz_uid))
+                    prop = Property("X-CALENDARSERVER-MODIFIED-BY", authz.canonicalCalendarUserAddress())
+                    prop.setParameter("CN", authz.displayName())
+                    for candidate in authz.calendarUserAddresses():
+                        if candidate.startswith("mailto:"):
+                            prop.setParameter("EMAIL", candidate[7:])
+                            break
+                    component.replacePropertyInAllComponents(prop)
+                else:
+                    component.removeAllPropertiesWithName("X-CALENDARSERVER-MODIFIED-BY")
+                self._componentChanged = True
+
+
     def validAccess(self, component, inserting, update_state):
         """
         Make sure that the X-CALENDARSERVER-ACCESS property is properly dealt with.
@@ -1724,11 +1775,10 @@
                 # Cannot do implicit in sharee's shared calendar
                 isShareeCollection = self.destinationparent.isShareeCollection()
                 if isShareeCollection:
-                    raise HTTPError(ErrorResponse(
-                        responsecode.FORBIDDEN,
-                        (calendarserver_namespace, "sharee-privilege-needed",),
-                        description="Sharee's cannot schedule"
-                    ))
+                    scheduler.setSchedulingNotAllowed(
+                        ShareeAllowedError,
+                        "Sharee's cannot schedule",
+                    )
 
                 new_calendar = (yield scheduler.doImplicitScheduling(self.schedule_tag_match))
                 if new_calendar:
@@ -2050,7 +2100,7 @@
             # Determine attachment mode (ignore inbox's) - NB we have to do this
             # after setting up other properties as UID at least is needed
             self._attachment = _ATTACHMENTS_MODE_NONE
-            if self._dropboxID is None:
+            if not self._dropboxID:
                 if not isInboxItem:
                     if component.hasPropertyInAnyComponent("X-APPLE-DROPBOX"):
                         self._attachment = _ATTACHMENTS_MODE_WRITE
@@ -2500,7 +2550,7 @@
         for managed_id in added:
             changed[managed_id] = newattached[managed_id]
 
-        if self._dropboxID is None:
+        if not self._dropboxID:
             self._dropboxID = str(uuid.uuid4())
         changes = yield self._addingManagedIDs(self._txn, self._parentCollection, self._dropboxID, changed, component.resourceUID())
 
@@ -2632,7 +2682,7 @@
             raise AttachmentStoreFailed
         yield t.loseConnection()
 
-        if self._dropboxID is None:
+        if not self._dropboxID:
             self._dropboxID = str(uuid.uuid4())
         attachment._objectDropboxID = self._dropboxID
 
@@ -3037,7 +3087,7 @@
         @return: C{True} if this attachment exists, C{False} otherwise.
         """
         att = schema.ATTACHMENT
-        if self._dropboxID is not None:
+        if self._dropboxID:
             where = (att.DROPBOX_ID == self._dropboxID).And(
                    att.PATH == self._name)
         else:

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/common.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/common.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -1118,6 +1118,26 @@
 
 
     @inlineCallbacks
+    def test_sharedNotifierID(self):
+        yield self.test_shareWith()
+        yield self.commit()
+
+        home = yield self.homeUnderTest()
+        self.assertEquals(home.notifierID(), "CalDAV|home1")
+        calendar = yield home.calendarWithName("calendar_1")
+        self.assertEquals(calendar.notifierID(), "CalDAV|home1")
+        self.assertEquals(calendar.notifierID(label="collection"), "CalDAV|home1/calendar_1")
+        yield self.commit()
+
+        home = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        self.assertEquals(home.notifierID(), "CalDAV|%s" % (OTHER_HOME_UID,))
+        calendar = yield home.calendarWithName(self.sharedName)
+        self.assertEquals(calendar.notifierID(), "CalDAV|home1")
+        self.assertEquals(calendar.notifierID(label="collection"), "CalDAV|home1/calendar_1")
+        yield self.commit()
+
+
+    @inlineCallbacks
     def test_hasCalendarResourceUIDSomewhereElse(self):
         """
         L{ICalendarHome.hasCalendarResourceUIDSomewhereElse} will determine if

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_file.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_file.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -490,6 +490,7 @@
     test_asShared = test_shareWith
     test_unshareSharerSide = test_shareWith
     test_unshareShareeSide = test_shareWith
+    test_sharedNotifierID = test_shareWith
 
 
     def test_init(self):

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -132,6 +132,13 @@
 
 
 
+class ValidOrganizerError(CommonStoreError):
+    """
+    Specified organizer is not valid.
+    """
+
+
+
 class AttendeeAllowedError(CommonStoreError):
     """
     Attendee is not allowed to make an implicit scheduling change.
@@ -139,6 +146,13 @@
 
 
 
+class ShareeAllowedError(CommonStoreError):
+    """
+    Sharee is not allowed to make an implicit scheduling change.
+    """
+
+
+
 class InvalidPerUserDataMerge(CommonStoreError):
     """
     Per-user data merge failed.

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/file.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/file.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -77,6 +77,14 @@
 )
 UIDPATH = "__uids__"
 
+
+
+class _StubQueuer(object):
+    def transferProposalCallbacks(self, otherQueuer):
+        return otherQueuer
+
+
+
 class CommonDataStore(DataStore):
     """
     Shared logic for SQL-based data stores, between calendar and addressbook
@@ -87,7 +95,6 @@
 
     @ivar quota: the amount of space granted to each calendar home (in bytes)
         for storing attachments, or C{None} if quota should not be enforced.
-
     @type quota: C{int} or C{NoneType}
 
     @ivar _propertyStoreClass: The class (or callable object / factory) that
@@ -95,6 +102,10 @@
         signature of the L{XattrPropertyStore} type: take 2 arguments
         C{(default-user-uid, path-factory)}, return an L{IPropertyStore}
         provider.
+
+    @ivar queuer: For compatibility with SQL-based store; currently a
+        non-functional implementation just for tests, but could be fixed to be
+        backed by SQLite or something.
     """
     implements(ICalendarStore)
 
@@ -118,7 +129,10 @@
         self._migrating = False
         self._enableNotifications = True
         self._newTransactionCallbacks = set()
+        # FIXME: see '@ivar queuer' above.
+        self.queuer = _StubQueuer()
 
+
     def callWithNewTransactions(self, callback):
         """
         Registers a method to be called whenever a new transaction is

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py	2013-04-11 16:24:18 UTC (rev 11029)
@@ -353,7 +353,7 @@
             toFile.write("\n")
             toFile.write("SQL: %s\n" % (sql,))
             toFile.write("Rows: %s\n" % (rows,))
-            toFile.write("Time (ms): %.3f\n" % (t,))
+            toFile.write("Time (ms): %.3f\n" % (t * 1000.0,))
         toFile.write("***\n\n")
 
         if self.logFileName:
@@ -2505,15 +2505,8 @@
     _objectTable = None
 
 
-    def __init__(self, home, name, resourceID, mode, status, message=None, ownerHome=None):
+    def __init__(self, home, name, resourceID, mode, status, message=None, ownerHome=None, ownerName=None):
 
-        if home._notifiers:
-            childID = "%s/%s" % (home.uid(), name)
-            notifiers = [notifier.clone(label="collection", id=childID)
-                         for notifier in home._notifiers]
-        else:
-            notifiers = None
-
         self._home = home
         self._name = name
         self._resourceID = resourceID
@@ -2521,12 +2514,24 @@
         self._bindStatus = status
         self._bindMessage = message
         self._ownerHome = home if ownerHome is None else ownerHome
+        self._ownerName = name if ownerName is None else ownerName
         self._created = None
         self._modified = None
         self._objects = {}
         self._objectNames = None
         self._syncTokenRevision = None
-        self._notifiers = notifiers
+
+        # Always use notifiers based off the owner home so that shared collections use tokens common
+        # to the owner - and thus will be the same for each sharee. Without that, each sharee would have
+        # a different token to subscribe to and thus would each need a separate push - whereas a common
+        # token only requires one push (to multiple subscribers).
+        if self._ownerHome._notifiers:
+            childID = "%s/%s" % (self._ownerHome.uid(), self._ownerName)
+            self._notifiers = [notifier.clone(label="collection", id=childID)
+                         for notifier in self._ownerHome._notifiers]
+        else:
+            self._notifiers = None
+
         self._index = None  # Derived classes need to set this
 
 
@@ -3038,17 +3043,18 @@
 
             if bindStatus == _BIND_MODE_OWN:
                 ownerHome = home
+                ownerName = resourceName
             else:
                 #TODO: get all ownerHomeIDs at once
-                ownerHomeID = (yield cls._ownerHomeWithResourceID.on(
-                                home._txn, resourceID=resourceID))[0][0]
+                ownerHomeID, ownerName = (yield cls._ownerHomeWithResourceID.on(home._txn, resourceID=resourceID))[0]
                 ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerHomeID)
 
             child = cls(
                 home=home,
                 name=resourceName, resourceID=resourceID,
                 mode=bindMode, status=bindStatus,
-                message=bindMessage, ownerHome=ownerHome
+                message=bindMessage,
+                ownerHome=ownerHome, ownerName=ownerName
             )
             for attr, value in zip(cls.metadataAttributes(), metadata):
                 setattr(child, attr, value)
@@ -3097,15 +3103,15 @@
         bindMode, homeID, resourceID, resourceName, bindStatus, bindMessage = rows[0] #@UnusedVariable
 
         #TODO:  combine with _invitedBindForNameAndHomeID and sort results
-        ownerHomeID = (yield cls._ownerHomeWithResourceID.on(
-                        home._txn, resourceID=resourceID))[0][0]
+        ownerHomeID, ownerName = (yield cls._ownerHomeWithResourceID.on(home._txn, resourceID=resourceID))[0]
         ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerHomeID)
 
         child = cls(
             home=home,
             name=resourceName, resourceID=resourceID,
             mode=bindMode, status=bindStatus,
-            message=bindMessage, ownerHome=ownerHome,
+            message=bindMessage,
+            ownerHome=ownerHome, ownerName=ownerName,
         )
         yield child.initFromStore()
         returnValue(child)
@@ -3152,10 +3158,12 @@
                 # get ownerHomeID
                 if bindMode == _BIND_MODE_OWN:
                     ownerHomeID = homeID
+                    ownerName = resourceName
                 else:
-                    ownerHomeID = (yield cls._ownerHomeWithResourceID.on(
-                                    home._txn, resourceID=resourceID))[0][0]
+                    ownerHomeID, ownerName = (yield cls._ownerHomeWithResourceID.on(
+                                    home._txn, resourceID=resourceID))[0]
                 rows[0].append(ownerHomeID)
+                rows[0].append(ownerName)
 
             if rows and queryCacher:
                 # Cache the result
@@ -3164,7 +3172,7 @@
         if not rows:
             returnValue(None)
 
-        bindMode, homeID, resourceID, resourceName, bindStatus, bindMessage, ownerHomeID = rows[0] #@UnusedVariable
+        bindMode, homeID, resourceID, resourceName, bindStatus, bindMessage, ownerHomeID, ownerName = rows[0] #@UnusedVariable
 
         if bindMode == _BIND_MODE_OWN:
             ownerHome = home
@@ -3175,7 +3183,8 @@
             home=home,
             name=name, resourceID=resourceID,
             mode=bindMode, status=bindStatus,
-            message=bindMessage, ownerHome=ownerHome,
+            message=bindMessage,
+            ownerHome=ownerHome, ownerName=ownerName,
         )
         yield child.initFromStore()
         returnValue(child)
@@ -3214,15 +3223,16 @@
 
         if bindMode == _BIND_MODE_OWN:
             ownerHome = home
+            ownerName = resourceName
         else:
-            ownerHomeID = (yield cls._ownerHomeWithResourceID.on(
-                            home._txn, resourceID=resourceID))[0][0]
+            ownerHomeID, ownerName = (yield cls._ownerHomeWithResourceID.on(home._txn, resourceID=resourceID))[0]
             ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerHomeID)
         child = cls(
             home=home,
             name=resourceName, resourceID=resourceID,
             mode=bindMode, status=bindStatus,
-            message=bindMessage, ownerHome=ownerHome,
+            message=bindMessage,
+            ownerHome=ownerHome, ownerName=ownerName,
         )
         yield child.initFromStore()
         returnValue(child)
@@ -3481,15 +3491,16 @@
     @classproperty
     def _ownerHomeWithResourceID(cls): #@NoSelf
         """
-        DAL query to retrieve the home resource ID of the owner from the bound
+        DAL query to retrieve the home resource ID and resource name of the owner from the bound
         home-child ID.
         """
         bind = cls._bindSchema
-        return Select([bind.HOME_RESOURCE_ID],
-                     From=bind,
-                     Where=(bind.RESOURCE_ID ==
-                            Parameter("resourceID")).And(
-                                bind.BIND_MODE == _BIND_MODE_OWN))
+        return Select(
+            [bind.HOME_RESOURCE_ID, bind.RESOURCE_NAME, ],
+            From=bind,
+            Where=(bind.RESOURCE_ID == Parameter("resourceID")).And(
+                bind.BIND_MODE == _BIND_MODE_OWN)
+        )
 
 
     @inlineCallbacks
@@ -3505,8 +3516,7 @@
             # we already know who the owner is.
             returnValue(self._home._resourceID)
         else:
-            rid = (yield self._ownerHomeWithResourceID.on(
-                self._txn, resourceID=self._resourceID))[0][0]
+            rid, _ignore_rname = (yield self._ownerHomeWithResourceID.on(self._txn, resourceID=self._resourceID))[0]
             returnValue(rid)
 
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-04-11 16:24:18 UTC (rev 11029)
@@ -300,12 +300,17 @@
     "PUSH_ID" nvarchar2(255)
 );
 
+create table GROUP_CACHER_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
 create table CALENDARSERVER (
     "NAME" nvarchar2(255) primary key,
     "VALUE" nvarchar2(255)
 );
 
-insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '17');
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '18');
 insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '3');
 insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '1');
 create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/current.sql	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/current.sql	2013-04-11 16:24:18 UTC (rev 11029)
@@ -559,7 +559,16 @@
   PUSH_ID                       varchar(255) not null
 );
 
+-----------------
+-- GroupCacher --
+-----------------
 
+create table GROUP_CACHER_POLLING_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
 --------------------
 -- Schema Version --
 --------------------
@@ -569,6 +578,6 @@
   VALUE                         varchar(255)
 );
 
-insert into CALENDARSERVER values ('VERSION', '17');
+insert into CALENDARSERVER values ('VERSION', '18');
 insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '3');
 insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '1');

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v14.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v14.sql	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v14.sql	2013-04-11 16:24:18 UTC (rev 11029)
@@ -54,7 +54,7 @@
 create table CALENDAR_BIND (
     "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
     "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
-    "CALENDAR_RESOURCE_NAME" nvarchar2(255) not null,
+    "CALENDAR_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
     "MESSAGE" nclob, 
@@ -203,7 +203,7 @@
 create table ADDRESSBOOK_BIND (
     "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
     "ADDRESSBOOK_RESOURCE_ID" integer not null references ADDRESSBOOK on delete cascade,
-    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255) not null,
+    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
     "MESSAGE" nclob, 
@@ -268,7 +268,6 @@
 insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '14');
 insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '3');
 insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '1');
-
 create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (
     NOTIFICATION_HOME_RESOURCE_ID
 );

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v15.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v15.sql	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v15.sql	2013-04-11 16:24:18 UTC (rev 11029)
@@ -58,7 +58,7 @@
 create table CALENDAR_BIND (
     "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
     "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
-    "CALENDAR_RESOURCE_NAME" nvarchar2(255) not null,
+    "CALENDAR_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
     "MESSAGE" nclob, 
@@ -207,7 +207,7 @@
 create table ADDRESSBOOK_BIND (
     "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
     "ADDRESSBOOK_RESOURCE_ID" integer not null references ADDRESSBOOK on delete cascade,
-    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255) not null,
+    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
     "MESSAGE" nclob, 
@@ -272,7 +272,6 @@
 insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '15');
 insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '3');
 insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '1');
-
 create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (
     NOTIFICATION_HOME_RESOURCE_ID
 );

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v17.sql (from rev 11028, CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v17.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v17.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/oracle-dialect/v17.sql	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,399 @@
+create sequence RESOURCE_ID_SEQ;
+create sequence INSTANCE_ID_SEQ;
+create sequence ATTACHMENT_ID_SEQ;
+create sequence REVISION_SEQ;
+create sequence WORKITEM_SEQ;
+create table NODE_INFO (
+    "HOSTNAME" nvarchar2(255),
+    "PID" integer not null,
+    "PORT" integer not null,
+    "TIME" timestamp default CURRENT_TIMESTAMP at time zone 'UTC' not null, 
+    primary key("HOSTNAME", "PORT")
+);
+
+create table NAMED_LOCK (
+    "LOCK_NAME" nvarchar2(255) primary key
+);
+
+create table CALENDAR_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique,
+    "DATAVERSION" integer default 0 not null
+);
+
+create table CALENDAR_HOME_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR_HOME on delete cascade,
+    "QUOTA_USED_BYTES" integer default 0 not null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDAR (
+    "RESOURCE_ID" integer primary key
+);
+
+create table CALENDAR_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR on delete cascade,
+    "SUPPORTED_COMPONENTS" nvarchar2(255) default null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table NOTIFICATION_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique
+);
+
+create table NOTIFICATION (
+    "RESOURCE_ID" integer primary key,
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME,
+    "NOTIFICATION_UID" nvarchar2(255),
+    "XML_TYPE" nvarchar2(255),
+    "XML_DATA" nclob,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique("NOTIFICATION_UID", "NOTIFICATION_HOME_RESOURCE_ID")
+);
+
+create table CALENDAR_BIND (
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "CALENDAR_RESOURCE_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "MESSAGE" nclob, 
+    primary key("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_ID"), 
+    unique("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_NAME")
+);
+
+create table CALENDAR_BIND_MODE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('own', 0);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('write', 2);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('direct', 3);
+create table CALENDAR_BIND_STATUS (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invited', 0);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('accepted', 1);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('declined', 2);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invalid', 3);
+create table CALENDAR_OBJECT (
+    "RESOURCE_ID" integer primary key,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob,
+    "ICALENDAR_UID" nvarchar2(255),
+    "ICALENDAR_TYPE" nvarchar2(255),
+    "ATTACHMENTS_MODE" integer default 0 not null,
+    "DROPBOX_ID" nvarchar2(255),
+    "ORGANIZER" nvarchar2(255),
+    "RECURRANCE_MIN" date,
+    "RECURRANCE_MAX" date,
+    "ACCESS" integer default 0 not null,
+    "SCHEDULE_OBJECT" integer default 0,
+    "SCHEDULE_TAG" nvarchar2(36) default null,
+    "SCHEDULE_ETAGS" nclob default null,
+    "PRIVATE_COMMENTS" integer default 0 not null,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique("CALENDAR_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MO (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('none', 0);
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('write', 2);
+create table CALENDAR_ACCESS_TYPE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(32) unique
+);
+
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('', 0);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('public', 1);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('private', 2);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('confidential', 3);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('restricted', 4);
+create table TIME_RANGE (
+    "INSTANCE_ID" integer primary key,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "CALENDAR_OBJECT_RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "FLOATING" integer not null,
+    "START_DATE" timestamp not null,
+    "END_DATE" timestamp not null,
+    "FBTYPE" integer not null,
+    "TRANSPARENT" integer not null
+);
+
+create table FREE_BUSY_TYPE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('unknown', 0);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('free', 1);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy', 2);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-unavailable', 3);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-tentative', 4);
+create table TRANSPARENCY (
+    "TIME_RANGE_INSTANCE_ID" integer not null references TIME_RANGE on delete cascade,
+    "USER_ID" nvarchar2(255),
+    "TRANSPARENT" integer not null
+);
+
+create table ATTACHMENT (
+    "ATTACHMENT_ID" integer primary key,
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "DROPBOX_ID" nvarchar2(255),
+    "CONTENT_TYPE" nvarchar2(255),
+    "SIZE" integer not null,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "PATH" nvarchar2(1024)
+);
+
+create table ATTACHMENT_CALENDAR_OBJECT (
+    "ATTACHMENT_ID" integer not null references ATTACHMENT on delete cascade,
+    "MANAGED_ID" nvarchar2(255),
+    "CALENDAR_OBJECT_RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade, 
+    primary key("ATTACHMENT_ID", "CALENDAR_OBJECT_RESOURCE_ID"), 
+    unique("MANAGED_ID", "CALENDAR_OBJECT_RESOURCE_ID")
+);
+
+create table RESOURCE_PROPERTY (
+    "RESOURCE_ID" integer not null,
+    "NAME" nvarchar2(255),
+    "VALUE" nclob,
+    "VIEWER_UID" nvarchar2(255), 
+    primary key("RESOURCE_ID", "NAME", "VIEWER_UID")
+);
+
+create table ADDRESSBOOK_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique,
+    "DATAVERSION" integer default 0 not null
+);
+
+create table ADDRESSBOOK_HOME_METADATA (
+    "RESOURCE_ID" integer primary key references ADDRESSBOOK_HOME on delete cascade,
+    "QUOTA_USED_BYTES" integer default 0 not null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table ADDRESSBOOK (
+    "RESOURCE_ID" integer primary key
+);
+
+create table ADDRESSBOOK_METADATA (
+    "RESOURCE_ID" integer primary key references ADDRESSBOOK on delete cascade,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table ADDRESSBOOK_BIND (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "ADDRESSBOOK_RESOURCE_ID" integer not null references ADDRESSBOOK on delete cascade,
+    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "MESSAGE" nclob, 
+    primary key("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_ID"), 
+    unique("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_NAME")
+);
+
+create table ADDRESSBOOK_OBJECT (
+    "RESOURCE_ID" integer primary key,
+    "ADDRESSBOOK_RESOURCE_ID" integer not null references ADDRESSBOOK on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "VCARD_TEXT" nclob,
+    "VCARD_UID" nvarchar2(255),
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique("ADDRESSBOOK_RESOURCE_ID", "RESOURCE_NAME"), 
+    unique("ADDRESSBOOK_RESOURCE_ID", "VCARD_UID")
+);
+
+create table CALENDAR_OBJECT_REVISIONS (
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "CALENDAR_RESOURCE_ID" integer references CALENDAR,
+    "CALENDAR_NAME" nvarchar2(255) default null,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null
+);
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "ADDRESSBOOK_RESOURCE_ID" integer references ADDRESSBOOK,
+    "ADDRESSBOOK_NAME" nvarchar2(255) default null,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null
+);
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null, 
+    unique("NOTIFICATION_HOME_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table APN_SUBSCRIPTIONS (
+    "TOKEN" nvarchar2(255),
+    "RESOURCE_KEY" nvarchar2(255),
+    "MODIFIED" integer not null,
+    "SUBSCRIBER_GUID" nvarchar2(255),
+    "USER_AGENT" nvarchar2(255) default null,
+    "IP_ADDR" nvarchar2(255) default null, 
+    primary key("TOKEN", "RESOURCE_KEY")
+);
+
+create table IMIP_TOKENS (
+    "TOKEN" nvarchar2(255),
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALUID" nvarchar2(255),
+    "ACCESSED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    primary key("ORGANIZER", "ATTENDEE", "ICALUID")
+);
+
+create table IMIP_INVITATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "FROM_ADDR" nvarchar2(255),
+    "TO_ADDR" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table IMIP_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table IMIP_REPLY_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table PUSH_NOTIFICATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "PUSH_ID" nvarchar2(255)
+);
+
+create table CALENDARSERVER (
+    "NAME" nvarchar2(255) primary key,
+    "VALUE" nvarchar2(255)
+);
+
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '17');
+insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '3');
+insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '1');
+create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (
+    NOTIFICATION_HOME_RESOURCE_ID
+);
+
+create index CALENDAR_BIND_RESOURC_e57964d4 on CALENDAR_BIND (
+    CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_CALEN_a9a453a9 on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_CALEN_96e83b73 on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    RECURRANCE_MAX
+);
+
+create index CALENDAR_OBJECT_ICALE_82e731d5 on CALENDAR_OBJECT (
+    ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_DROPB_de041d80 on CALENDAR_OBJECT (
+    DROPBOX_ID
+);
+
+create index TIME_RANGE_CALENDAR_R_beb6e7eb on TIME_RANGE (
+    CALENDAR_RESOURCE_ID
+);
+
+create index TIME_RANGE_CALENDAR_O_acf37bd1 on TIME_RANGE (
+    CALENDAR_OBJECT_RESOURCE_ID
+);
+
+create index TRANSPARENCY_TIME_RAN_5f34467f on TRANSPARENCY (
+    TIME_RANGE_INSTANCE_ID
+);
+
+create index ATTACHMENT_CALENDAR_H_0078845c on ATTACHMENT (
+    CALENDAR_HOME_RESOURCE_ID
+);
+
+create index ADDRESSBOOK_BIND_RESO_205aa75c on ADDRESSBOOK_BIND (
+    ADDRESSBOOK_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_3a3956c4 on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_HOME_RESOURCE_ID,
+    CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_2643d556 on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    RESOURCE_NAME
+);
+
+create index CALENDAR_OBJECT_REVIS_265c8acf on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_f460d62d on ADDRESSBOOK_OBJECT_REVISIONS (
+    ADDRESSBOOK_HOME_RESOURCE_ID,
+    ADDRESSBOOK_RESOURCE_ID
+);
+
+create index ADDRESSBOOK_OBJECT_RE_9a848f39 on ADDRESSBOOK_OBJECT_REVISIONS (
+    ADDRESSBOOK_RESOURCE_ID,
+    RESOURCE_NAME
+);
+
+create index ADDRESSBOOK_OBJECT_RE_cb101e6b on ADDRESSBOOK_OBJECT_REVISIONS (
+    ADDRESSBOOK_RESOURCE_ID,
+    REVISION
+);
+
+create index NOTIFICATION_OBJECT_R_036a9cee on NOTIFICATION_OBJECT_REVISIONS (
+    NOTIFICATION_HOME_RESOURCE_ID,
+    REVISION
+);
+
+create index APN_SUBSCRIPTIONS_RES_9610d78e on APN_SUBSCRIPTIONS (
+    RESOURCE_KEY
+);
+
+create index IMIP_TOKENS_TOKEN_e94b918f on IMIP_TOKENS (
+    TOKEN
+);
+

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/postgres-dialect/v17.sql (from rev 11028, CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v17.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/postgres-dialect/v17.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/old/postgres-dialect/v17.sql	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,572 @@
+-- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+
+----
+-- Copyright (c) 2010-2013 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.
+----
+
+-----------------
+-- Resource ID --
+-----------------
+
+create sequence RESOURCE_ID_SEQ;
+
+-------------------------
+-- Cluster Bookkeeping --
+-------------------------
+
+-- Information about a process connected to this database.
+
+-- Note that this must match the node info schema in twext.enterprise.queue.
+create table NODE_INFO (
+  HOSTNAME  varchar(255) not null,
+  PID       integer not null,
+  PORT      integer not null,
+  TIME      timestamp not null default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (HOSTNAME, PORT)
+);
+
+-- Unique named locks.  This table should always be empty, but rows are
+-- temporarily created in order to prevent undesirable concurrency.
+create table NAMED_LOCK (
+    LOCK_NAME varchar(255) primary key
+);
+
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null unique,                                 -- implicit index
+  DATAVERSION      integer      default 0 not null
+);
+
+----------------------------
+-- Calendar Home Metadata --
+----------------------------
+
+create table CALENDAR_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references CALENDAR_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+
+-----------------------
+-- Calendar Metadata --
+-----------------------
+
+create table CALENDAR_METADATA (
+  RESOURCE_ID           integer   primary key references CALENDAR on delete cascade, -- implicit index
+  SUPPORTED_COMPONENTS  varchar(255) default null,
+  CREATED               timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED              timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID   varchar(255) not null unique                                 -- implicit index
+);
+
+create table NOTIFICATION (
+  RESOURCE_ID                   integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME,
+  NOTIFICATION_UID              varchar(255) not null,
+  XML_TYPE                      varchar(255) not null,
+  XML_DATA                      text         not null,
+  MD5                           char(32)     not null,
+  CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID) -- implicit index
+);
+
+create index NOTIFICATION_NOTIFICATION_HOME_RESOURCE_ID on
+  NOTIFICATION(NOTIFICATION_HOME_RESOURCE_ID);
+
+-------------------
+-- Calendar Bind --
+-------------------
+
+-- Joins CALENDAR_HOME and CALENDAR
+
+create table CALENDAR_BIND (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      not null references CALENDAR on delete cascade,
+  CALENDAR_RESOURCE_NAME    varchar(255) not null,
+  BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
+  MESSAGE                   text,
+
+  primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID), -- implicit index
+  unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)     -- implicit index
+);
+
+create index CALENDAR_BIND_RESOURCE_ID on CALENDAR_BIND(CALENDAR_RESOURCE_ID);
+
+-- Enumeration of calendar bind modes
+
+create table CALENDAR_BIND_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_MODE values (0, 'own'  );
+insert into CALENDAR_BIND_MODE values (1, 'read' );
+insert into CALENDAR_BIND_MODE values (2, 'write');
+insert into CALENDAR_BIND_MODE values (3, 'direct');
+
+-- Enumeration of statuses
+
+create table CALENDAR_BIND_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_STATUS values (0, 'invited' );
+insert into CALENDAR_BIND_STATUS values (1, 'accepted');
+insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+  RESOURCE_ID          integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID integer      not null references CALENDAR on delete cascade,
+  RESOURCE_NAME        varchar(255) not null,
+  ICALENDAR_TEXT       text         not null,
+  ICALENDAR_UID        varchar(255) not null,
+  ICALENDAR_TYPE       varchar(255) not null,
+  ATTACHMENTS_MODE     integer      default 0 not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
+  DROPBOX_ID           varchar(255),
+  ORGANIZER            varchar(255),
+  RECURRANCE_MIN       date,        -- minimum date that recurrences have been expanded to.
+  RECURRANCE_MAX       date,        -- maximum date that recurrences have been expanded to.
+  ACCESS               integer      default 0 not null,
+  SCHEDULE_OBJECT      boolean      default false,
+  SCHEDULE_TAG         varchar(36)  default null,
+  SCHEDULE_ETAGS       text         default null,
+  PRIVATE_COMMENTS     boolean      default false not null,
+  MD5                  char(32)     not null,
+  CREATED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED             timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (CALENDAR_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+
+  -- since the 'inbox' is a 'calendar resource' for the purpose of storing
+  -- calendar objects, this constraint has to be selectively enforced by the
+  -- application layer.
+
+  -- unique(CALENDAR_RESOURCE_ID, ICALENDAR_UID)
+);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_AND_ICALENDAR_UID on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_RECURRANCE_MAX on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, RECURRANCE_MAX);
+
+create index CALENDAR_OBJECT_ICALENDAR_UID on
+  CALENDAR_OBJECT(ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_DROPBOX_ID on
+  CALENDAR_OBJECT(DROPBOX_ID);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (0, 'none' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (1, 'read' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (2, 'write');
+
+
+-- Enumeration of calendar access types
+
+create table CALENDAR_ACCESS_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(32) not null unique
+);
+
+insert into CALENDAR_ACCESS_TYPE values (0, ''             );
+insert into CALENDAR_ACCESS_TYPE values (1, 'public'       );
+insert into CALENDAR_ACCESS_TYPE values (2, 'private'      );
+insert into CALENDAR_ACCESS_TYPE values (3, 'confidential' );
+insert into CALENDAR_ACCESS_TYPE values (4, 'restricted'   );
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+  INSTANCE_ID                 integer        primary key default nextval('INSTANCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID        integer        not null references CALENDAR on delete cascade,
+  CALENDAR_OBJECT_RESOURCE_ID integer        not null references CALENDAR_OBJECT on delete cascade,
+  FLOATING                    boolean        not null,
+  START_DATE                  timestamp      not null,
+  END_DATE                    timestamp      not null,
+  FBTYPE                      integer        not null,
+  TRANSPARENT                 boolean        not null
+);
+
+create index TIME_RANGE_CALENDAR_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_RESOURCE_ID);
+create index TIME_RANGE_CALENDAR_OBJECT_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_OBJECT_RESOURCE_ID);
+
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown'         );
+insert into FREE_BUSY_TYPE values (1, 'free'            );
+insert into FREE_BUSY_TYPE values (2, 'busy'            );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative'  );
+
+
+------------------
+-- Transparency --
+------------------
+
+create table TRANSPARENCY (
+  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
+  USER_ID                     varchar(255) not null,
+  TRANSPARENT                 boolean      not null
+);
+
+create index TRANSPARENCY_TIME_RANGE_INSTANCE_ID on
+  TRANSPARENCY(TIME_RANGE_INSTANCE_ID);
+
+
+----------------
+-- Attachment --
+----------------
+
+create sequence ATTACHMENT_ID_SEQ;
+
+create table ATTACHMENT (
+  ATTACHMENT_ID               integer           primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
+  CALENDAR_HOME_RESOURCE_ID   integer           not null references CALENDAR_HOME,
+  DROPBOX_ID                  varchar(255),
+  CONTENT_TYPE                varchar(255)      not null,
+  SIZE                        integer           not null,
+  MD5                         char(32)          not null,
+  CREATED                     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                    timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  PATH                        varchar(1024)     not null
+);
+
+create index ATTACHMENT_CALENDAR_HOME_RESOURCE_ID on
+  ATTACHMENT(CALENDAR_HOME_RESOURCE_ID);
+
+-- Many-to-many relationship between attachments and calendar objects
+create table ATTACHMENT_CALENDAR_OBJECT (
+  ATTACHMENT_ID                  integer      not null references ATTACHMENT on delete cascade,
+  MANAGED_ID                     varchar(255) not null,
+  CALENDAR_OBJECT_RESOURCE_ID    integer      not null references CALENDAR_OBJECT on delete cascade,
+
+  primary key (ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID), -- implicit index
+  unique (MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
+);
+
+
+-----------------------
+-- Resource Property --
+-----------------------
+
+create table RESOURCE_PROPERTY (
+  RESOURCE_ID integer      not null, -- foreign key: *.RESOURCE_ID
+  NAME        varchar(255) not null,
+  VALUE       text         not null, -- FIXME: xml?
+  VIEWER_UID  varchar(255),
+
+  primary key (RESOURCE_ID, NAME, VIEWER_UID) -- implicit index
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null unique,                                -- implicit index
+  DATAVERSION      integer      default 0 not null
+);
+
+-------------------------------
+-- AddressBook Home Metadata --
+-------------------------------
+
+create table ADDRESSBOOK_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references ADDRESSBOOK_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+-----------------
+-- AddressBook --
+-----------------
+
+create table ADDRESSBOOK (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+
+--------------------------
+-- AddressBook Metadata --
+--------------------------
+
+create table ADDRESSBOOK_METADATA (
+  RESOURCE_ID integer   primary key references ADDRESSBOOK on delete cascade, -- implicit index
+  CREATED     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED    timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+----------------------
+-- AddressBook Bind --
+----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK
+
+create table ADDRESSBOOK_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID integer      not null references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_RESOURCE_ID      integer      not null references ADDRESSBOOK on delete cascade,
+  ADDRESSBOOK_RESOURCE_NAME    varchar(255) not null,
+  BIND_MODE                    integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                  integer      not null, -- enum CALENDAR_BIND_STATUS
+  MESSAGE                      text,                  -- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)     -- implicit index
+);
+
+create index ADDRESSBOOK_BIND_RESOURCE_ID on
+  ADDRESSBOOK_BIND(ADDRESSBOOK_RESOURCE_ID);
+
+create table ADDRESSBOOK_OBJECT (
+  RESOURCE_ID             integer      primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  ADDRESSBOOK_RESOURCE_ID integer      not null references ADDRESSBOOK on delete cascade,
+  RESOURCE_NAME           varchar(255) not null,
+  VCARD_TEXT              text         not null,
+  VCARD_UID               varchar(255) not null,
+  MD5                     char(32)     not null,
+  CREATED                 timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME), -- implicit index
+  unique (ADDRESSBOOK_RESOURCE_ID, VCARD_UID)      -- implicit index
+);
+
+---------------
+-- Revisions --
+---------------
+
+create sequence REVISION_SEQ;
+
+
+---------------
+-- Revisions --
+---------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      references CALENDAR,
+  CALENDAR_NAME             varchar(255) default null,
+  RESOURCE_NAME             varchar(255),
+  REVISION                  integer      default nextval('REVISION_SEQ') not null,
+  DELETED                   boolean      not null
+);
+
+create index CALENDAR_OBJECT_REVISIONS_HOME_RESOURCE_ID_CALENDAR_RESOURCE_ID
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, RESOURCE_NAME);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, REVISION);
+
+-------------------------------
+-- AddressBook Object Revisions --
+-------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+  ADDRESSBOOK_HOME_RESOURCE_ID integer      not null references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_RESOURCE_ID      integer      references ADDRESSBOOK,
+  ADDRESSBOOK_NAME             varchar(255) default null,
+  RESOURCE_NAME                varchar(255),
+  REVISION                     integer      default nextval('REVISION_SEQ') not null,
+  DELETED                      boolean      not null
+);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_HOME_RESOURCE_ID_ADDRESSBOOK_RESOURCE_ID
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_RESOURCE_ID, REVISION);
+
+-----------------------------------
+-- Notification Object Revisions --
+-----------------------------------
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+
+  unique(NOTIFICATION_HOME_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+);
+
+create index NOTIFICATION_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on NOTIFICATION_OBJECT_REVISIONS(NOTIFICATION_HOME_RESOURCE_ID, REVISION);
+
+-------------------------------------------
+-- Apple Push Notification Subscriptions --
+-------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  USER_AGENT                    varchar(255) default null,
+  IP_ADDR                       varchar(255) default null,
+
+  primary key (TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+   on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+-----------------
+-- IMIP Tokens --
+-----------------
+
+create table IMIP_TOKENS (
+  TOKEN                         varchar(255) not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALUID                       varchar(255) not null,
+  ACCESSED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (ORGANIZER, ATTENDEE, ICALUID) -- implicit index
+);
+
+create index IMIP_TOKENS_TOKEN
+   on IMIP_TOKENS(TOKEN);
+
+----------------
+-- Work Items --
+----------------
+
+create sequence WORKITEM_SEQ;
+
+---------------------------
+-- IMIP Inivitation Work --
+---------------------------
+
+create table IMIP_INVITATION_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  FROM_ADDR                     varchar(255) not null,
+  TO_ADDR                       varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+create table IMIP_POLLING_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  PUSH_ID                       varchar(255) not null
+);
+
+
+--------------------
+-- Schema Version --
+--------------------
+
+create table CALENDARSERVER (
+  NAME                          varchar(255) primary key, -- implicit index
+  VALUE                         varchar(255)
+);
+
+insert into CALENDARSERVER values ('VERSION', '17');
+insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '3');
+insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '1');

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_12_to_13.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_12_to_13.sql	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_12_to_13.sql	2013-04-11 16:24:18 UTC (rev 11029)
@@ -22,13 +22,24 @@
 
 create sequence ATTACHMENT_ID_SEQ;
 
+alter table ATTACHMENT
+ drop primary key;
 
+-- not needed; DROPBOX_ID becomes nullable after dropping primary key above
+-- alter table ATTACHMENT
+--  modify (DROPBOX_ID null);
+
+-- We want ATTACHMENT_ID as a pkey, but can't yet since it needs unique values,
+-- and oracle can't set a default column value from a sequence
 alter table ATTACHMENT
- drop primary key ("DROPBOX_ID", "PATH");
+ add ("ATTACHMENT_ID" integer);
+
+-- fill in ATTACHMENT_ID with unique values from the sequence
+update ATTACHMENT set ATTACHMENT_ID = ATTACHMENT_ID_SEQ.nextval;
+
+-- now set ATTACHMENT_ID as primary key, which implies unique and not null
 alter table ATTACHMENT
- modify (DROPBOX_ID null);
-alter table ATTACHMENT
- add ("ATTACHMENT_ID" integer primary key);
+ add primary key(ATTACHMENT_ID);
 
 create table ATTACHMENT_CALENDAR_OBJECT (
     "ATTACHMENT_ID" integer not null references ATTACHMENT on delete cascade,

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql	2013-04-11 13:46:55 UTC (rev 11028)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql	2013-04-11 16:24:18 UTC (rev 11029)
@@ -26,15 +26,11 @@
  drop column SEEN_BY_OWNER;
 alter table CALENDAR_BIND
  drop column SEEN_BY_SHAREE;
-alter table CALENDAR_BIND
- modify (ADDRESSBOOK_RESOURCE_NAME not null);
  
 alter table ADDRESSBOOK_BIND
  drop column SEEN_BY_OWNER;
 alter table ADDRESSBOOK_BIND
  drop column SEEN_BY_SHAREE;
-alter table ADDRESSBOOK_BIND
- modify (ADDRESSBOOK_RESOURCE_NAME not null);
 
 -- Now update the version
 -- No data upgrades

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_17_to_18.sql (from rev 11028, CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_17_to_18.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_17_to_18.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_17_to_18.sql	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,35 @@
+----
+-- Copyright (c) 2011-2013 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.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 17 to 18 --
+---------------------------------------------------
+
+
+-----------------
+-- GroupCacher --
+-----------------
+
+
+
+create table GROUP_CACHER_POLLING_WORK (
+  "WORK_ID" integer primary key not null,
+  "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+
+-- Now update the version
+update CALENDARSERVER set VALUE = '18' where NAME = 'VERSION';

Copied: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_17_to_18.sql (from rev 11028, CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_17_to_18.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_17_to_18.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_17_to_18.sql	2013-04-11 16:24:18 UTC (rev 11029)
@@ -0,0 +1,31 @@
+----
+-- Copyright (c) 2011-2013 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.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 17 to 18 --
+---------------------------------------------------
+
+-----------------
+-- GroupCacher --
+-----------------
+
+create table GROUP_CACHER_POLLING_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+-- Now update the version
+update CALENDARSERVER set VALUE = '18' where NAME = 'VERSION';
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130411/9401d668/attachment-0001.html>


More information about the calendarserver-changes mailing list