[CalendarServer-changes] [15011] CalendarServer/branches/users/cdaboo/cfod

source_changes at macosforge.org source_changes at macosforge.org
Mon Jul 27 19:43:19 PDT 2015


Revision: 15011
          http://trac.calendarserver.org//changeset/15011
Author:   cdaboo at apple.com
Date:     2015-07-27 19:43:19 -0700 (Mon, 27 Jul 2015)
Log Message:
-----------
Merge from trunk.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/cfod/.project
    CalendarServer/branches/users/cdaboo/cfod/.pydevproject
    CalendarServer/branches/users/cdaboo/cfod/bin/package
    CalendarServer/branches/users/cdaboo/cfod/calendarserver/accesslog.py
    CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/config.py
    CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/gateway.py
    CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/purge.py
    CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_config.py
    CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_gateway.py
    CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_purge_old_events.py
    CalendarServer/branches/users/cdaboo/cfod/conf/caldavd-apple.plist
    CalendarServer/branches/users/cdaboo/cfod/contrib/performance/loadtest/sim.py
    CalendarServer/branches/users/cdaboo/cfod/doc/Extensions/caldav-recursplit.txt
    CalendarServer/branches/users/cdaboo/cfod/doc/Extensions/caldav-recursplit.xml
    CalendarServer/branches/users/cdaboo/cfod/requirements-dev.txt
    CalendarServer/branches/users/cdaboo/cfod/requirements-stable.txt
    CalendarServer/branches/users/cdaboo/cfod/setup.py
    CalendarServer/branches/users/cdaboo/cfod/support/Apple.make
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/controlapi.py
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/customxml.py
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/extensions.py
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/ical.py
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/storebridge.py
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_config.py
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_icalendar.py
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_sharing.py
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_upgrade.py
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Africa/Casablanca.ics
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Africa/El_Aaiun.ics
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/America/Cayman.ics
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Morocco Standard Time.ics
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/links.txt
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/timezones.xml
    CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/version.txt
    CalendarServer/branches/users/cdaboo/cfod/txdav/base/datastore/subpostgres.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/icalsplitter.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/inbound.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/outbound.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/smtpsender.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/test/test_outbound.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/processing.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/sql_external.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/calendar_store/ho/me/home_bad/calendar_bad/1.ics
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_attachments.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_file.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_queue_scheduling.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/util.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/carddav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/carddav/datastore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/file.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/podding/attachments.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/podding/test/test_conduit.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_sharing.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_util.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/test/accounts/accounts.xml
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/test/util.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/upgrade/sql/test/test_upgrade_with_data.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/upgrade/test/test_migrate.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/revision_cleanup.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/test/test_revision_cleanup.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/dps/server.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/who/directory.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/who/groups.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/accounts/groupAccounts.xml
    CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_directory.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_group_attendees.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_group_sharees.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_util.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_wiki.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/who/util.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/who/wiki.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/cfod/contrib/performance/jobqueue/
    CalendarServer/branches/users/cdaboo/cfod/contrib/tools/flow.d
    CalendarServer/branches/users/cdaboo/cfod/contrib/tools/lldb_utils.py
    CalendarServer/branches/users/cdaboo/cfod/support/python-wrapper.c
    CalendarServer/branches/users/cdaboo/cfod/support/undo-virtualenv
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/oracle-dialect/v55.sql
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/oracle-dialect/v56.sql
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/postgres-dialect/v55.sql
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/postgres-dialect/v56.sql
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_55_to_56.sql
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_56_to_57.sql
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_55_to_56.sql
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_56_to_57.sql
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/load_work.py
    CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/test/test_load_work.py

Property Changed:
----------------
    CalendarServer/branches/users/cdaboo/cfod/


Property changes on: CalendarServer/branches/users/cdaboo/cfod
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalDAVTester/trunk:11193-11198
/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/release/CalendarServer-5.1-dev:11846
/CalendarServer/branches/release/CalendarServer-5.2-dev:11972,12357-12358,12794,12814
/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/cross-pod-sharing:12038-12191
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/fix-no-ischedule:11607-11871
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/json:11622-11912
/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/performance-tweaks:11824-11836
/CalendarServer/branches/users/cdaboo/pod2pod-migration:14338-14520
/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/reverse-proxy-pods:11875-11900
/CalendarServer/branches/users/cdaboo/scheduling-queue-refresh:11783-12557
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/sharing-in-the-store:11935-12016
/CalendarServer/branches/users/cdaboo/store-scheduling:10876-11129
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/gaya/cleanrevisions:12152-12334
/CalendarServer/branches/users/gaya/groupsharee2:13669-13773
/CalendarServer/branches/users/gaya/sharedgroupfixes:12120-12142
/CalendarServer/branches/users/gaya/sharedgroups-3:11088-11204
/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/enforce-max-requests:11640-11643
/CalendarServer/branches/users/glyph/hang-fix:11465-11491
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/launchd-wrapper-bis:11413-11436
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/log-cleanups:11691-11731
/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/start-service-start-loop:11060-11065
/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/warning-cleanups:11347-11357
/CalendarServer/branches/users/glyph/whenNotProposed:11881-11897
/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/move2who:12819-12860
/CalendarServer/branches/users/sagen/move2who-2:12861-12898
/CalendarServer/branches/users/sagen/move2who-3:12899-12913
/CalendarServer/branches/users/sagen/move2who-4:12914-13157
/CalendarServer/branches/users/sagen/move2who-5:13158-13163
/CalendarServer/branches/users/sagen/newcua:13309-13327
/CalendarServer/branches/users/sagen/newcua-1:13328-13330
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/recordtypes:13648-13656
/CalendarServer/branches/users/sagen/recordtypes-2:13657
/CalendarServer/branches/users/sagen/request-socket:14748-14767
/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/sagen/trashcan:14185-14269
/CalendarServer/branches/users/sagen/trashcan-2:14270-14324
/CalendarServer/branches/users/sagen/trashcan-3:14325-14450
/CalendarServer/branches/users/sagen/trashcan-4:14451-14471
/CalendarServer/branches/users/sagen/trashcan-5:14471-14555
/CalendarServer/branches/users/wsanchez/psycopg2cffi:14427-14439
/CalendarServer/branches/users/wsanchez/transations:5515-5593
   + /CalDAVTester/trunk:11193-11198
/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/release/CalendarServer-5.1-dev:11846
/CalendarServer/branches/release/CalendarServer-5.2-dev:11972,12357-12358,12794,12814
/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/cross-pod-sharing:12038-12191
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/fix-no-ischedule:11607-11871
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/json:11622-11912
/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/performance-tweaks:11824-11836
/CalendarServer/branches/users/cdaboo/pod2pod-migration:14338-14520
/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/reverse-proxy-pods:11875-11900
/CalendarServer/branches/users/cdaboo/scheduling-queue-refresh:11783-12557
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/sharing-in-the-store:11935-12016
/CalendarServer/branches/users/cdaboo/store-scheduling:10876-11129
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/gaya/cleanrevisions:12152-12334
/CalendarServer/branches/users/gaya/groupsharee2:13669-13773
/CalendarServer/branches/users/gaya/sharedgroupfixes:12120-12142
/CalendarServer/branches/users/gaya/sharedgroups-3:11088-11204
/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/enforce-max-requests:11640-11643
/CalendarServer/branches/users/glyph/hang-fix:11465-11491
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/launchd-wrapper-bis:11413-11436
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/log-cleanups:11691-11731
/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/start-service-start-loop:11060-11065
/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/warning-cleanups:11347-11357
/CalendarServer/branches/users/glyph/whenNotProposed:11881-11897
/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/move2who:12819-12860
/CalendarServer/branches/users/sagen/move2who-2:12861-12898
/CalendarServer/branches/users/sagen/move2who-3:12899-12913
/CalendarServer/branches/users/sagen/move2who-4:12914-13157
/CalendarServer/branches/users/sagen/move2who-5:13158-13163
/CalendarServer/branches/users/sagen/newcua:13309-13327
/CalendarServer/branches/users/sagen/newcua-1:13328-13330
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/recordtypes:13648-13656
/CalendarServer/branches/users/sagen/recordtypes-2:13657
/CalendarServer/branches/users/sagen/request-socket:14748-14767
/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/sagen/trashcan:14185-14269
/CalendarServer/branches/users/sagen/trashcan-2:14270-14324
/CalendarServer/branches/users/sagen/trashcan-3:14325-14450
/CalendarServer/branches/users/sagen/trashcan-4:14451-14471
/CalendarServer/branches/users/sagen/trashcan-5:14471-14555
/CalendarServer/branches/users/wsanchez/psycopg2cffi:14427-14439
/CalendarServer/branches/users/wsanchez/transations:5515-5593
/CalendarServer/trunk:14901-15009

Modified: CalendarServer/branches/users/cdaboo/cfod/.project
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/.project	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/.project	2015-07-28 02:43:19 UTC (rev 15011)
@@ -1,19 +1,16 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <projectDescription>
-	<name>CalendarServer</name>
+	<name>CalendarServer-4</name>
 	<comment></comment>
 	<projects>
 		<project>CalDAVClientLibrary</project>
 		<project>cffi</project>
-		<project>cx_Oracle</project>
-		<project>kerberos</project>
 		<project>pg8000</project>
 		<project>psutil</project>
 		<project>pycalendar</project>
-		<project>pycparser</project>
 		<project>pycrypto</project>
-		<project>pyOpenSSL</project>
-		<project>service_identity</project>
+		<project>PyKerberos</project>
+		<project>PyOpenDirectory</project>
 		<project>twextpy</project>
 		<project>Twisted</project>
 		<project>zope.interface-4.0.3</project>
@@ -28,24 +25,4 @@
 	<natures>
 		<nature>org.python.pydev.pythonNature</nature>
 	</natures>
-	<filteredResources>
-		<filter>
-			<id>1396668930421</id>
-			<name></name>
-			<type>10</type>
-			<matcher>
-				<id>org.eclipse.ui.ide.multiFilter</id>
-				<arguments>1.0-projectRelativePath-matches-true-false-.develop</arguments>
-			</matcher>
-		</filter>
-		<filter>
-			<id>1396668930422</id>
-			<name></name>
-			<type>10</type>
-			<matcher>
-				<id>org.eclipse.ui.ide.multiFilter</id>
-				<arguments>1.0-name-matches-true-false-subprojects</arguments>
-			</matcher>
-		</filter>
-	</filteredResources>
 </projectDescription>

Modified: CalendarServer/branches/users/cdaboo/cfod/.pydevproject
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/.pydevproject	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/.pydevproject	2015-07-28 02:43:19 UTC (rev 15011)
@@ -1,9 +1,8 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?eclipse-pydev version="1.0"?><pydev_project>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">PyPy-2.6.0</pydev_property>
 <pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
 <pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
-<path>/${PROJECT_DIR_NAME}</path>
+<path>/CalendarServer-4</path>
 </pydev_pathproperty>
-
-<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
 </pydev_project>

Modified: CalendarServer/branches/users/cdaboo/cfod/bin/package
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/bin/package	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/bin/package	2015-07-28 02:43:19 UTC (rev 15011)
@@ -115,6 +115,7 @@
     bootstrap_virtualenv;
     "${bootstrap_python}" -m virtualenv  \
       --always-copy                      \
+      --system-site-packages             \
       --no-setuptools                    \
       "${py_virtualenv}";
   fi;
@@ -149,13 +150,29 @@
 
   cd "${destination}/lib";
 
-  find "../roots" -type f  \
+  find "../roots" "(" -type f -o -type l ")"  \
     "("                    \
       -name '*.so'     -o  \
       -name '*.so.*'   -o  \
       -name '*.dylib'      \
     ")" -print0            \
     | xargs -0 -I % ln -s % .;
+
+  # Write out environment.sh
+  local dst="${destination}";
+  cat > "${dst}/environment.sh" << __EOF__
+export              PATH="${dst}/bin:\${PATH}";
+export    C_INCLUDE_PATH="${dst}/include:\${C_INCLUDE_PATH:-}";
+export   LD_LIBRARY_PATH="${dst}/lib:${dst}/lib64:\${LD_LIBRARY_PATH:-}:\$ORACLE_HOME";
+export          CPPFLAGS="-I${dst}/include \${CPPFLAGS:-} ";
+export           LDFLAGS="-L${dst}/lib -L${dst}/lib64 \${LDFLAGS:-} ";
+export DYLD_LIBRARY_PATH="${dst}/lib:${dst}/lib64:\${DYLD_LIBRARY_PATH:-}:\$ORACLE_HOME";
+__EOF__
+
+  # Install CalendarServer into venv
+  cd ${wd}
+  ${python} setup.py install
+
 }
 
 

Modified: CalendarServer/branches/users/cdaboo/cfod/calendarserver/accesslog.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/calendarserver/accesslog.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/calendarserver/accesslog.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -181,6 +181,13 @@
                     format += " fwd=%(fwd)s"
                     formatArgs["fwd"] = forwardedFor
 
+            if formatArgs["host"] == "0.0.0.0":
+                fwdHeaders = request.headers.getRawHeaders("x-forwarded-for", "")
+                if fwdHeaders:
+                    formatArgs["host"] = fwdHeaders[-1].split(",")[-1].strip()
+                    format += " unix=%(unix)s"
+                    formatArgs["unix"] = "true"
+
         elif "overloaded" in eventDict:
             overloaded = eventDict.get("overloaded")
 

Modified: CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/config.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/config.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -59,12 +59,6 @@
     "EnableSSL",
     "HTTPPort",
     "LogLevels",
-    "Notifications.Services.APNS.CalDAV.AuthorityChainPath",
-    "Notifications.Services.APNS.CalDAV.CertificatePath",
-    "Notifications.Services.APNS.CalDAV.PrivateKeyPath",
-    "Notifications.Services.APNS.CardDAV.AuthorityChainPath",
-    "Notifications.Services.APNS.CardDAV.CertificatePath",
-    "Notifications.Services.APNS.CardDAV.PrivateKeyPath",
     "Notifications.Services.APNS.Enabled",
     "RedirectHTTPToHTTPS",
     "Scheduling.iMIP.Enabled",

Modified: CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/gateway.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/gateway.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/gateway.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -577,7 +577,7 @@
         cutoff = DateTime.getToday()
         cutoff.setDateOnly(False)
         cutoff.offsetDay(-retainDays)
-        eventCount = (yield PurgeOldEventsService.purgeOldEvents(self.store, cutoff, DEFAULT_BATCH_SIZE))
+        eventCount = (yield PurgeOldEventsService.purgeOldEvents(self.store, None, cutoff, DEFAULT_BATCH_SIZE))
         self.respond(command, {'EventsRemoved': eventCount, "RetainDays": retainDays})
 
 

Modified: CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/purge.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/purge.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -29,7 +29,7 @@
 from pycalendar.datetime import DateTime
 
 from twext.enterprise.dal.record import fromTable
-from twext.enterprise.dal.syntax import Delete, Select, Union
+from twext.enterprise.dal.syntax import Delete, Select, Union, Parameter, Max
 from twext.enterprise.jobqueue import WorkItem, RegeneratingWorkItem
 from twext.python.log import Logger
 
@@ -37,9 +37,11 @@
 
 from twistedcaldav import caldavxml
 from twistedcaldav.config import config
+from twistedcaldav.dateops import parseSQLDateToPyCalendar, pyCalendarToSQLTimestamp
+from twistedcaldav.ical import Component, InvalidICalendarDataError
 
 from txdav.caldav.datastore.query.filter import Filter
-from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_NORMAL
+from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_NORMAL, _BIND_MODE_OWN
 
 log = Logger()
 
@@ -277,10 +279,11 @@
 
 class PurgeOldEventsService(WorkerService):
 
+    uuid = None
     cutoff = None
     batchSize = None
     dryrun = False
-    verbose = False
+    debug = False
 
     @classmethod
     def usage(cls, e=None):
@@ -293,8 +296,8 @@
         print("options:")
         print("  -h --help: print this help and exit")
         print("  -f --config <path>: Specify caldavd.plist configuration path")
+        print("  -u --uuid <uuid>: Only process this user(s) [REQUIRED]")
         print("  -d --days <number>: specify how many days in the past to retain (default=%d)" % (DEFAULT_RETAIN_DAYS,))
-        # print("  -b --batch <number>: number of events to remove in each transaction (default=%d)" % (DEFAULT_BATCH_SIZE,))
         print("  -n --dry-run: calculate how many events to purge, but do not purge data")
         print("  -v --verbose: print progress information")
         print("  -D --debug: debug logging")
@@ -312,11 +315,12 @@
 
         try:
             (optargs, args) = getopt(
-                sys.argv[1:], "Dd:b:f:hnv", [
+                sys.argv[1:], "Dd:b:f:hnu:v", [
                     "days=",
                     "batch=",
                     "dry-run",
                     "config=",
+                    "uuid=",
                     "help",
                     "verbose",
                     "debug",
@@ -329,6 +333,7 @@
         # Get configuration
         #
         configFileName = None
+        uuid = None
         days = DEFAULT_RETAIN_DAYS
         batchSize = DEFAULT_BATCH_SIZE
         dryrun = False
@@ -365,12 +370,19 @@
             elif opt in ("-f", "--config"):
                 configFileName = arg
 
+            elif opt in ("-u", "--uuid"):
+                uuid = arg
+
             else:
                 raise NotImplementedError(opt)
 
         if args:
             cls.usage("Too many arguments: %s" % (args,))
 
+        if uuid is None:
+            cls.usage("uuid must be specified")
+        cls.uuid = uuid
+
         if dryrun:
             verbose = True
 
@@ -380,68 +392,321 @@
         cls.cutoff = cutoff
         cls.batchSize = batchSize
         cls.dryrun = dryrun
-        cls.verbose = verbose
+        cls.debug = debug
 
         utilityMain(
             configFileName,
             cls,
-            verbose=debug,
+            verbose=verbose,
         )
 
 
     @classmethod
     @inlineCallbacks
-    def purgeOldEvents(cls, store, cutoff, batchSize, verbose=False, dryrun=False):
+    def purgeOldEvents(cls, store, uuid, cutoff, batchSize, debug=False, dryrun=False):
 
         service = cls(store)
+        service.uuid = uuid
         service.cutoff = cutoff
         service.batchSize = batchSize
         service.dryrun = dryrun
-        service.verbose = verbose
+        service.debug = debug
         result = yield service.doWork()
         returnValue(result)
 
 
     @inlineCallbacks
+    def getMatchingHomeUIDs(self):
+        """
+        Find all the calendar homes that match the uuid cli argument.
+        """
+        log.debug("Searching for calendar homes matching: '{}'".format(self.uuid))
+        txn = self.store.newTransaction(label="Find matching homes")
+        ch = schema.CALENDAR_HOME
+        if self.uuid:
+            kwds = {"uuid": self.uuid}
+            rows = (yield Select(
+                [ch.RESOURCE_ID, ch.OWNER_UID, ],
+                From=ch,
+                Where=(ch.OWNER_UID.StartsWith(Parameter("uuid"))),
+            ).on(txn, **kwds))
+        else:
+            rows = (yield Select(
+                [ch.RESOURCE_ID, ch.OWNER_UID, ],
+                From=ch,
+            ).on(txn))
+
+        yield txn.commit()
+        log.debug("  Found {} calendar homes".format(len(rows)))
+        returnValue(sorted(rows, key=lambda x: x[1]))
+
+
+    @inlineCallbacks
+    def getMatchingCalendarIDs(self, home_id, owner_uid):
+        """
+        Find all the owned calendars for the specified calendar home.
+
+        @param home_id: resource-id of calendar home to check
+        @type home_id: L{int}
+        @param owner_uid: owner UUID of home to check
+        @type owner_uid: L{str}
+        """
+        log.debug("Checking calendar home: {} '{}'".format(home_id, owner_uid))
+        txn = self.store.newTransaction(label="Find matching calendars")
+        cb = schema.CALENDAR_BIND
+        kwds = {"home_id": home_id}
+        rows = (yield Select(
+            [cb.CALENDAR_RESOURCE_ID, cb.CALENDAR_RESOURCE_NAME, ],
+            From=cb,
+            Where=(cb.CALENDAR_HOME_RESOURCE_ID == Parameter("home_id")).And(
+                cb.BIND_MODE == _BIND_MODE_OWN
+            ),
+        ).on(txn, **kwds))
+        yield txn.commit()
+        log.debug("  Found {} calendars".format(len(rows)))
+        returnValue(rows)
+
+
+    PurgeEvent = collections.namedtuple("PurgeEvent", ("home", "calendar", "resource",))
+
+    @inlineCallbacks
+    def getResourceIDsToPurge(self, home_id, calendar_id, calendar_name):
+        """
+        For the given calendar find which calendar objects are older than the cut-off and return the
+        resource-ids of those.
+
+        @param home_id: resource-id of calendar home
+        @type home_id: L{int}
+        @param calendar_id: resource-id of the calendar to check
+        @type calendar_id: L{int}
+        @param calendar_name: name of the calendar to check
+        @type calendar_name: L{str}
+        """
+
+        log.debug("  Checking calendar: {} '{}'".format(calendar_id, calendar_name))
+        purge = set()
+        txn = self.store.newTransaction(label="Find matching resources")
+        co = schema.CALENDAR_OBJECT
+        tr = schema.TIME_RANGE
+        kwds = {"calendar_id": calendar_id}
+        rows = (yield Select(
+            [co.RESOURCE_ID, co.RECURRANCE_MAX, co.RECURRANCE_MIN, Max(tr.END_DATE)],
+            From=co.join(tr, on=(co.RESOURCE_ID == tr.CALENDAR_OBJECT_RESOURCE_ID)),
+            Where=(co.CALENDAR_RESOURCE_ID == Parameter("calendar_id")).And(
+                co.ICALENDAR_TYPE == "VEVENT"
+            ),
+            GroupBy=(co.RESOURCE_ID, co.RECURRANCE_MAX, co.RECURRANCE_MIN,),
+            Having=(
+                (co.RECURRANCE_MAX == None).And(Max(tr.END_DATE) < pyCalendarToSQLTimestamp(self.cutoff))
+            ).Or(
+                (co.RECURRANCE_MAX != None).And(co.RECURRANCE_MAX < pyCalendarToSQLTimestamp(self.cutoff))
+            ),
+        ).on(txn, **kwds))
+
+        log.debug("    Found {} resources to check".format(len(rows)))
+        for resource_id, recurrence_max, recurrence_min, max_end_date in rows:
+
+            recurrence_max = parseSQLDateToPyCalendar(recurrence_max) if recurrence_max else None
+            recurrence_min = parseSQLDateToPyCalendar(recurrence_min) if recurrence_min else None
+            max_end_date = parseSQLDateToPyCalendar(max_end_date) if max_end_date else None
+
+            # Find events where we know the max(end_date) represents a valid,
+            # untruncated expansion
+            if recurrence_min is None or recurrence_min < self.cutoff:
+                if recurrence_max is None:
+                    # Here we know max_end_date is the fully expand final instance
+                    if max_end_date < self.cutoff:
+                        purge.add(self.PurgeEvent(home_id, calendar_id, resource_id,))
+                    continue
+                elif recurrence_max > self.cutoff:
+                    # Here we know that there are instances newer than the cut-off
+                    # but they have not yet been indexed out that far
+                    continue
+
+            # Manually detect the max_end_date from the actual calendar data
+            calendar = yield self.getCalendar(txn, resource_id)
+            if calendar is not None:
+                if self.checkLastInstance(calendar):
+                    purge.add(self.PurgeEvent(home_id, calendar_id, resource_id,))
+
+        yield txn.commit()
+        log.debug("    Found {} resources to purge".format(len(purge)))
+        returnValue(purge)
+
+
+    @inlineCallbacks
+    def getCalendar(self, txn, resid):
+        """
+        Get the calendar data for a calendar object resource.
+
+        @param resid: resource-id of the calendar object resource to load
+        @type resid: L{int}
+        """
+        co = schema.CALENDAR_OBJECT
+        kwds = {"ResourceID" : resid}
+        rows = (yield Select(
+            [co.ICALENDAR_TEXT],
+            From=co,
+            Where=(
+                co.RESOURCE_ID == Parameter("ResourceID")
+            ),
+        ).on(txn, **kwds))
+        try:
+            caldata = Component.fromString(rows[0][0]) if rows else None
+        except InvalidICalendarDataError:
+            returnValue(None)
+
+        returnValue(caldata)
+
+
+    def checkLastInstance(self, calendar):
+        """
+        Determine the last instance of a calendar event. Try a "static" analysis of the data first,
+        and only if needed, do an instance expansion.
+
+        @param calendar: the calendar object to examine
+        @type calendar: L{Component}
+        """
+
+        # Is it recurring
+        master = calendar.masterComponent()
+        if not calendar.isRecurring() or master is None:
+            # Just check the end date
+            for comp in calendar.subcomponents():
+                if comp.name() == "VEVENT":
+                    if comp.getEndDateUTC() > self.cutoff:
+                        return False
+            else:
+                return True
+        elif calendar.isRecurringUnbounded():
+            return False
+        else:
+            # First test all sub-components
+            # Just check the end date
+            for comp in calendar.subcomponents():
+                if comp.name() == "VEVENT":
+                    if comp.getEndDateUTC() > self.cutoff:
+                        return False
+
+            # If we get here we need to test the RRULE - if there is an until use
+            # that as the end point, if a count, we have to expand
+            rrules = tuple(master.properties("RRULE"))
+            if len(rrules):
+                if rrules[0].value().getUseUntil():
+                    return rrules[0].value().getUntil() < self.cutoff
+                else:
+                    return not calendar.hasInstancesAfter(self.cutoff)
+
+        return True
+
+
+    @inlineCallbacks
+    def getResourcesToPurge(self, home_id, owner_uid):
+        """
+        Find all the resource-ids of calendar object resources that need to be purged in the specified home.
+
+        @param home_id: resource-id of calendar home to check
+        @type home_id: L{int}
+        @param owner_uid: owner UUID of home to check
+        @type owner_uid: L{str}
+        """
+
+        purge = set()
+        calendars = yield self.getMatchingCalendarIDs(home_id, owner_uid)
+        for calendar_id, calendar_name in calendars:
+            purge.update((yield self.getResourceIDsToPurge(home_id, calendar_id, calendar_name)))
+
+        returnValue(purge)
+
+
+    @inlineCallbacks
+    def purgeResources(self, events):
+        """
+        Remove up to batchSize events and return how
+        many were removed.
+        """
+
+        txn = self.store.newTransaction(label="Remove old events")
+        count = 0
+        last_home = None
+        last_calendar = None
+        for event in events:
+            if event.home != last_home:
+                home = (yield txn.calendarHomeWithResourceID(event.home))
+                last_home = event.home
+            if event.calendar != last_calendar:
+                calendar = (yield home.childWithID(event.calendar))
+                last_calendar = event.calendar
+            resource = (yield calendar.objectResourceWithID(event.resource))
+            yield resource.purge(implicitly=False)
+            log.debug("Removed resource {} '{}' from calendar {} '{}' of calendar home '{}'".format(
+                resource.id(),
+                resource.name(),
+                resource.parentCollection().id(),
+                resource.parentCollection().name(),
+                resource.parentCollection().ownerHome().uid()
+            ))
+            count += 1
+        yield txn.commit()
+        returnValue(count)
+
+
+    @inlineCallbacks
     def doWork(self):
 
+        if self.debug:
+            # Turn on debug logging for this module
+            config.LogLevels[__name__] = "debug"
+        else:
+            config.LogLevels[__name__] = "info"
+        config.update()
+
+        homes = yield self.getMatchingHomeUIDs()
+        if not homes:
+            log.info("No homes to process")
+            returnValue(0)
+
         if self.dryrun:
-            if self.verbose:
-                print("(Dry run) Searching for old events...")
-            txn = self.store.newTransaction(label="Find old events")
-            oldEvents = yield txn.eventsOlderThan(self.cutoff)
-            eventCount = len(oldEvents)
-            if self.verbose:
-                if eventCount == 0:
-                    print("No events are older than %s" % (self.cutoff,))
-                elif eventCount == 1:
-                    print("1 event is older than %s" % (self.cutoff,))
-                else:
-                    print("%d events are older than %s" % (eventCount, self.cutoff))
+            log.info("Purge dry run only")
+
+        log.info("Searching for old events...")
+
+        purge = set()
+        homes = yield self.getMatchingHomeUIDs()
+        for home_id, owner_uid in homes:
+            purge.update((yield self.getResourcesToPurge(home_id, owner_uid)))
+
+        if self.dryrun:
+            eventCount = len(purge)
+            if eventCount == 0:
+                log.info("No events are older than %s" % (self.cutoff,))
+            elif eventCount == 1:
+                log.info("1 event is older than %s" % (self.cutoff,))
+            else:
+                log.info("%d events are older than %s" % (eventCount, self.cutoff))
             returnValue(eventCount)
 
-        if self.verbose:
-            print("Removing events older than %s..." % (self.cutoff,))
+        purge = list(purge)
+        purge.sort()
+        totalEvents = len(purge)
 
+        log.info("Removing {} events older than {}...".format(len(purge), self.cutoff,))
+
         numEventsRemoved = -1
         totalRemoved = 0
         while numEventsRemoved:
-            txn = self.store.newTransaction(label="Remove old events")
-            numEventsRemoved = yield txn.removeOldEvents(self.cutoff, batchSize=self.batchSize)
-            yield txn.commit()
+            numEventsRemoved = (yield self.purgeResources(purge[:self.batchSize]))
             if numEventsRemoved:
                 totalRemoved += numEventsRemoved
-                if self.verbose:
-                    print("%d," % (totalRemoved,),)
+                log.debug("  Removed {} of {} events...".format(totalRemoved, totalEvents))
+                purge = purge[numEventsRemoved:]
 
-        if self.verbose:
-            print("")
-            if totalRemoved == 0:
-                print("No events were removed")
-            elif totalRemoved == 1:
-                print("1 event was removed in total")
-            else:
-                print("%d events were removed in total" % (totalRemoved,))
+        if totalRemoved == 0:
+            log.info("No events were removed")
+        elif totalRemoved == 1:
+            log.info("1 event was removed in total")
+        else:
+            log.info("%d events were removed in total" % (totalRemoved,))
 
         returnValue(totalRemoved)
 
@@ -671,20 +936,18 @@
             # Print table of results
             table = tables.Table()
             table.addHeader(("User", "Current Quota", "Orphan Size", "Orphan Count", "Dropbox Size", "Dropbox Count", "Managed Size", "Managed Count", "Total Size", "Total Count"))
-            table.setDefaultColumnFormats(
-                (
-                    tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY),
-                    tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-                    tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-                    tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-                    tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-                    tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-                    tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-                    tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-                    tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-                    tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-                )
-            )
+            table.setDefaultColumnFormats((
+                tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY),
+                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            ))
 
             totals = [0] * 8
             for user, data in sorted(byuser.items(), key=lambda x: x[0]):

Modified: CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_config.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_config.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -131,7 +131,6 @@
         self.assertEquals(results["result"]["DefaultLogLevel"], "warn")
 
         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 umask which is not accessible
         self.assertFalse("umask" in results["result"])
@@ -150,7 +149,6 @@
         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")
         hostName = "hostname_%s_%s" % (unichr(208), u"\ud83d\udca3")
         self.assertTrue(results["result"]["ServerHostName"].endswith(hostName))
 
@@ -242,8 +240,6 @@
             <true/>
             <key>Notifications.Services.APNS.Enabled</key>
             <true/>
-            <key>Notifications.Services.APNS.CalDAV.CertificatePath</key>
-            <string>/example/changed.cer</string>
             <key>ServerRoot</key>
             <string>/You/Shall/Not/Pass</string>
             <key>ServerHostName</key>

Modified: CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_gateway.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_gateway.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_gateway.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -466,7 +466,6 @@
         self.assertEquals(results["result"]["DefaultLogLevel"], "warn")
 
         self.assertEquals(results["result"]["Notifications"]["Services"]["APNS"]["Enabled"], False)
-        self.assertEquals(results["result"]["Notifications"]["Services"]["APNS"]["CalDAV"]["CertificatePath"], "/example/calendar.cer")
 
         # This is a read only key that is returned
         self.assertEquals(results["result"]["ServerRoot"], self.absoluteServerRoot)
@@ -489,7 +488,6 @@
         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")
         hostName = "hostname_%s_%s" % (unichr(208), u"\ud83d\udca3")
         self.assertTrue(results["result"]["ServerHostName"].endswith(hostName))
 
@@ -972,8 +970,6 @@
             <true/>
             <key>Notifications.Services.APNS.Enabled</key>
             <true/>
-            <key>Notifications.Services.APNS.CalDAV.CertificatePath</key>
-            <string>/example/changed.cer</string>
             <key>ServerHostName</key>
             <string>hostname_%s_%s</string>
         </dict>

Modified: CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_purge_old_events.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_purge_old_events.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/calendarserver/tools/test/test_purge_old_events.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -642,33 +642,123 @@
         # Dry run
         total = (yield PurgeOldEventsService.purgeOldEvents(
             self._sqlCalendarStore,
+            None,
             DateTime(now, 4, 1, 0, 0, 0),
             2,
             dryrun=True,
-            verbose=False
+            debug=True
         ))
         self.assertEquals(total, 13)
 
         # Actually remove
         total = (yield PurgeOldEventsService.purgeOldEvents(
             self._sqlCalendarStore,
+            None,
             DateTime(now, 4, 1, 0, 0, 0),
             2,
-            verbose=False
+            debug=True
         ))
         self.assertEquals(total, 13)
 
         # There should be no more left
         total = (yield PurgeOldEventsService.purgeOldEvents(
             self._sqlCalendarStore,
+            None,
             DateTime(now, 4, 1, 0, 0, 0),
             2,
-            verbose=False
+            debug=True
         ))
         self.assertEquals(total, 0)
 
 
     @inlineCallbacks
+    def test_purgeOldEvents_home_filtering(self):
+
+        # Dry run
+        total = (yield PurgeOldEventsService.purgeOldEvents(
+            self._sqlCalendarStore,
+            "ho",
+            DateTime(now, 4, 1, 0, 0, 0),
+            2,
+            dryrun=True,
+            debug=True
+        ))
+        self.assertEquals(total, 13)
+
+        # Dry run
+        total = (yield PurgeOldEventsService.purgeOldEvents(
+            self._sqlCalendarStore,
+            "home",
+            DateTime(now, 4, 1, 0, 0, 0),
+            2,
+            dryrun=True,
+            debug=True
+        ))
+        self.assertEquals(total, 13)
+
+        # Dry run
+        total = (yield PurgeOldEventsService.purgeOldEvents(
+            self._sqlCalendarStore,
+            "home1",
+            DateTime(now, 4, 1, 0, 0, 0),
+            2,
+            dryrun=True,
+            debug=True
+        ))
+        self.assertEquals(total, 5)
+
+        # Dry run
+        total = (yield PurgeOldEventsService.purgeOldEvents(
+            self._sqlCalendarStore,
+            "home2",
+            DateTime(now, 4, 1, 0, 0, 0),
+            2,
+            dryrun=True,
+            debug=True
+        ))
+        self.assertEquals(total, 8)
+
+
+    @inlineCallbacks
+    def test_purgeOldEvents_old_cutoff(self):
+
+        # Dry run
+        cutoff = DateTime.getToday()
+        cutoff.setDateOnly(False)
+        cutoff.offsetDay(-400)
+
+        total = (yield PurgeOldEventsService.purgeOldEvents(
+            self._sqlCalendarStore,
+            "ho",
+            cutoff,
+            2,
+            dryrun=True,
+            debug=True
+        ))
+        self.assertEquals(total, 12)
+
+        # Actually remove
+        total = (yield PurgeOldEventsService.purgeOldEvents(
+            self._sqlCalendarStore,
+            None,
+            cutoff,
+            2,
+            debug=True
+        ))
+        self.assertEquals(total, 12)
+
+        total = (yield PurgeOldEventsService.purgeOldEvents(
+            self._sqlCalendarStore,
+            "ho",
+            cutoff,
+            2,
+            dryrun=True,
+            debug=True
+        ))
+        self.assertEquals(total, 0)
+
+
+    @inlineCallbacks
     def test_purgeUID(self):
         txn = self._sqlCalendarStore.newTransaction()
 
@@ -753,9 +843,10 @@
         # Remove old events first
         total = (yield PurgeOldEventsService.purgeOldEvents(
             self._sqlCalendarStore,
+            None,
             DateTime(now, 4, 1, 0, 0, 0),
             2,
-            verbose=False
+            debug=False
         ))
         self.assertEquals(total, 13)
 

Modified: CalendarServer/branches/users/cdaboo/cfod/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/conf/caldavd-apple.plist	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/conf/caldavd-apple.plist	2015-07-28 02:43:19 UTC (rev 15011)
@@ -230,6 +230,9 @@
       </dict>
     </dict>
 
+    <key>DirectoryFilterStartsWith</key>
+    <true/>
+
     <!--
         Special principals
 

Modified: CalendarServer/branches/users/cdaboo/cfod/contrib/performance/loadtest/sim.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/contrib/performance/loadtest/sim.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/contrib/performance/loadtest/sim.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -97,20 +97,6 @@
 
 
 
-def recordsFromCount(count, uid=u"user%02d", password=u"user%02d",
-                     commonName=u"User %02d", email=u"user%02d at example.com",
-                     guid="10000000-0000-0000-0000-000000000%03d"):
-    for i in range(1, count + 1):
-        yield _DirectoryRecord(
-            uid % i,
-            password % i,
-            commonName % i,
-            email % i,
-            guid % i,
-        )
-
-
-
 class LagTrackingReactor(object):
     """
     This reactor wraps another reactor and proxies all attribute
@@ -292,16 +278,10 @@
             workerID = config.get("workerID", 0)
             workerCount = config.get("workerCount", 1)
             configTemplate = None
-            server = 'http://127.0.0.1:8008'
-            principalPathTemplate = "/principals/users/%s/"
+            server = config.get('server', 'http://127.0.0.1:8008')
+            principalPathTemplate = config.get('principalPathTemplate', '/principals/users/%s/')
             serializationPath = None
 
-            if 'server' in config:
-                server = config['server']
-
-            if 'principalPathTemplate' in config:
-                principalPathTemplate = config['principalPathTemplate']
-
             if 'clientDataSerialization' in config:
                 serializationPath = config['clientDataSerialization']['Path']
                 if not config['clientDataSerialization']['UseOldData']:

Copied: CalendarServer/branches/users/cdaboo/cfod/contrib/tools/flow.d (from rev 15009, CalendarServer/trunk/contrib/tools/flow.d)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/contrib/tools/flow.d	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/contrib/tools/flow.d	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,74 @@
+#!/usr/sbin/dtrace -Zs
+/*
+ * py_flow.d - snoop Python execution showing function flow.
+ *             Written for the Python DTrace provider.
+ *
+ * $Id: py_flow.d 51 2007-09-24 00:55:23Z brendan $
+ *
+ * This traces Python activity from all Python programs on the system
+ * running with Python provider support.
+ *
+ * USAGE: flow.d <PID>			# hit Ctrl-C to end
+ *
+ * This watches Python function entries and returns, and indents child
+ * function calls.
+ *
+ * FIELDS:
+ *		C			CPU-id
+ *		TIME(us)	Time since boot, us
+ *		FILE		Filename that this function belongs to
+ *		FUNC		Function name
+ *
+ * LEGEND:
+ *		->		function entry
+ *		<-		function return
+ *
+ * WARNING: Watch the first column carefully, it prints the CPU-id. If it
+ * changes, then it is very likely that the output has been shuffled.
+ *
+ * COPYRIGHT: Copyright (c) 2007 Brendan Gregg.
+ *
+ * CDDL HEADER START
+ *
+ *  The contents of this file are subject to the terms of the
+ *  Common Development and Distribution License, Version 1.0 only
+ *  (the "License").  You may not use this file except in compliance
+ *  with the License.
+ *
+ *  You can obtain a copy of the license at Docs/cddl1.txt
+ *  or http://www.opensolaris.org/os/licensing.
+ *  See the License for the specific language governing permissions
+ *  and limitations under the License.
+ *
+ * CDDL HEADER END
+ *
+ * 09-Sep-2007	Brendan Gregg	Created this.
+ *
+ * This is a variation of py_flow.d that takes a pid.
+ */
+
+#pragma D option quiet
+#pragma D option switchrate=10
+
+self int depth;
+
+dtrace:::BEGIN
+{
+	printf("%3s %-16s %-16s -- %s\n", "C", "TIME(us)", "FILE", "FUNC");
+}
+
+python*:::function-entry
+/pid == $1/
+{
+	printf("%3d %-16d %-16s %*s-> %s\n", cpu, timestamp / 1000,
+	    basename(copyinstr(arg0)), self->depth * 2, "", copyinstr(arg1));
+	self->depth++;
+}
+
+python*:::function-return
+/pid == $1/
+{
+	self->depth -= self->depth > 0 ? 1 : 0;
+	printf("%3d %-16d %-16s %*s<- %s\n", cpu, timestamp / 1000,
+	    basename(copyinstr(arg0)), self->depth * 2, "", copyinstr(arg1));
+}
\ No newline at end of file

Copied: CalendarServer/branches/users/cdaboo/cfod/contrib/tools/lldb_utils.py (from rev 15009, CalendarServer/trunk/contrib/tools/lldb_utils.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/contrib/tools/lldb_utils.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/contrib/tools/lldb_utils.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,184 @@
+##
+# Copyright (c) 2015 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.
+##
+
+"""
+Below are a bunch of useful functions that can be used inside of lldb to debug a Python
+process (this applies to cPython only).
+
+The steps are as follows:
+
+    > lldb
+    (lldb) target symbols add <path to Python.framework.dSYM>
+    (lldb) command script import contrib/tools/lldb_utils.py
+
+    Run commands using:
+
+    (lldb) pybt
+        ...
+    (lldb) pybtall
+        ...
+    (lldb) pylocals
+        ...
+
+    or inside the python shell:
+
+    (lldb) script
+    Python Interactive Interpreter. To exit, type 'quit()', 'exit()' or Ctrl-D.
+    >>> lldb_utils.pybt()
+        ...
+    >>> lldb_utils.pybtall()
+        ...
+    >>> lldb_utils.pylocals()
+        ...
+
+pybt - generate a python function call back trace of the currently selected thread
+pybtall - generate a python function call back trace of all threads
+pylocals - generate a list of the name and values of all locals in the current
+    python frame (only works when the currently selected frame is a Python call
+    frame as found by the pybt command).
+"""
+
+import lldb #@UnresolvedImport
+
+def _toStr(obj, pystring_t):
+    return obj.Cast(pystring_t).GetValueForExpressionPath("->ob_sval").summary
+
+
+
+def pybt(debugger=None, command=None, result=None, dict=None, thread=None):
+    """
+    An lldb command that prints a Python call back trace for the specified
+    thread or the currently selected thread.
+
+    @param debugger: debugger to use
+    @type debugger: L{lldb.SBDebugger}
+    @param command: ignored
+    @type command: ignored
+    @param result: ignored
+    @type result: ignored
+    @param dict: ignored
+    @type dict: ignored
+    @param thread: the specific thread to target
+    @type thread: L{lldb.SBThread}
+    """
+
+    if debugger is None:
+        debugger = lldb.debugger
+    target = debugger.GetSelectedTarget()
+    if not isinstance(thread, lldb.SBThread):
+        thread = target.GetProcess().GetSelectedThread()
+
+    pystring_t = target.FindFirstType("PyStringObject").GetPointerType()
+
+    num_frames = thread.GetNumFrames()
+    for i in range(num_frames - 1):
+        fr = thread.GetFrameAtIndex(i)
+        if fr.GetFunctionName() == "PyEval_EvalFrameEx":
+            fr_next = thread.GetFrameAtIndex(i + 1)
+            if fr_next.GetFunctionName() == "PyEval_EvalCodeEx":
+                f = fr.GetValueForVariablePath("f")
+                filename = _toStr(f.GetValueForExpressionPath("->f_code->co_filename"), pystring_t)
+                name = _toStr(f.GetValueForExpressionPath("->f_code->co_name"), pystring_t)
+                lineno = f.GetValueForExpressionPath("->f_lineno").GetValue()
+                print("#{}: {} - {}:{}".format(
+                    fr.GetFrameID(),
+                    filename[1:-1] if filename else ".",
+                    name[1:-1] if name else ".",
+                    lineno if lineno else ".",
+                ))
+
+
+
+def pybtall(debugger=None, command=None, result=None, dict=None):
+    """
+    An lldb command that prints a Python call back trace for all threads.
+
+    @param debugger: debugger to use
+    @type debugger: L{lldb.SBDebugger}
+    @param command: ignored
+    @type command: ignored
+    @param result: ignored
+    @type result: ignored
+    @param dict: ignored
+    @type dict: ignored
+    """
+    if debugger is None:
+        debugger = lldb.debugger
+    process = debugger.GetSelectedTarget().GetProcess()
+    numthreads = process.GetNumThreads()
+    for i in range(numthreads):
+        thread = process.GetThreadAtIndex(i)
+        print("----- Thread: {} -----".format(i + 1))
+        pybt(debugger=debugger, thread=thread)
+
+
+
+def pylocals(debugger=None, command=None, result=None, dict=None):
+    """
+    An lldb command that prints a list of Python local variables for the
+    currently selected frame.
+
+    @param debugger: debugger to use
+    @type debugger: L{lldb.SBDebugger}
+    @param command: ignored
+    @type command: ignored
+    @param result: ignored
+    @type result: ignored
+    @param dict: ignored
+    @type dict: ignored
+    """
+    if debugger is None:
+        debugger = lldb.debugger
+    target = debugger.GetSelectedTarget()
+    frame = target.GetProcess().GetSelectedThread().GetSelectedFrame()
+
+    pystring_t = target.FindFirstType("PyStringObject").GetPointerType()
+    pytuple_t = target.FindFirstType("PyTupleObject").GetPointerType()
+
+    f = frame.GetValueForVariablePath("f")
+    try:
+        numlocals = int(f.GetValueForExpressionPath("->f_code->co_nlocals").GetValue())
+    except TypeError:
+        print("Current frame is not a Python function")
+        return
+    print("Locals in frame #{}".format(frame.GetFrameID()))
+    names = f.GetValueForExpressionPath("->f_code->co_varnames").Cast(pytuple_t)
+    for i in range(numlocals):
+        localname = _toStr(names.GetValueForExpressionPath("->ob_item[{}]".format(i)), pystring_t)
+        local = frame.EvaluateExpression("PyString_AsString(PyObject_Repr(f->f_localsplus[{}]))".format(i)).summary
+        print("    {} = {}".format(
+            localname[1:-1] if localname else ".",
+            local[1:-1] if local else ".",
+        ))
+
+
+CMDS = ("pybt", "pybtall", "pylocals",)
+
+
+def __lldb_init_module(debugger, dict):
+    """
+    Register each command with lldb so they are available directly within lldb as
+    well as within its Python script shell.
+
+    @param debugger: debugger to use
+    @type debugger: L{lldb.SBDebugger}
+    @param dict: ignored
+    @type dict: ignored
+    """
+    for cmd in CMDS:
+        debugger.HandleCommand(
+            "command script add -f lldb_utils.{cmd} {cmd}".format(cmd=cmd)
+        )

Modified: CalendarServer/branches/users/cdaboo/cfod/doc/Extensions/caldav-recursplit.txt
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/doc/Extensions/caldav-recursplit.txt	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/doc/Extensions/caldav-recursplit.txt	2015-07-28 02:43:19 UTC (rev 15011)
@@ -4,11 +4,11 @@
 
                                                                 C. Daboo
                                                                    Apple
-                                                        October 30, 2014
+                                                           June 26, 2015
 
 
              Smart Splitting of Recurring Events in CalDAV
-                          caldav-recursplit-01
+                          caldav-recursplit-02
 
 Abstract
 
@@ -22,7 +22,7 @@
    1.  Introduction  . . . . . . . . . . . . . . . . . . . . . . . .   1
    2.  Conventions Used in This Document . . . . . . . . . . . . . .   2
    3.  New behavior  . . . . . . . . . . . . . . . . . . . . . . . .   3
-     3.1.  Example . . . . . . . . . . . . . . . . . . . . . . . . .   5
+     3.1.  Example . . . . . . . . . . . . . . . . . . . . . . . . .   6
    4.  Server Initiated Splitting  . . . . . . . . . . . . . . . . .   8
    5.  Security Considerations . . . . . . . . . . . . . . . . . . .   8
    6.  IANA Considerations . . . . . . . . . . . . . . . . . . . . .   8
@@ -53,9 +53,9 @@
 
 
 
-Daboo                      Expires May 3, 2015                  [Page 1]
+Daboo                   Expires December 28, 2015               [Page 1]
 
-                       CalDAV Recurrence Splitting          October 2014
+                       CalDAV Recurrence Splitting             June 2015
 
 
    instances from that point onwards.  Typically this is done by
@@ -109,9 +109,9 @@
 
 
 
-Daboo                      Expires May 3, 2015                  [Page 2]
+Daboo                   Expires December 28, 2015               [Page 2]
 
-                       CalDAV Recurrence Splitting          October 2014
+                       CalDAV Recurrence Splitting             June 2015
 
 
 3.  New behavior
@@ -128,16 +128,30 @@
 
    1.  "action" set to the value "split" - REQUIRED
 
-   2.  "rid" set to an iCalendar format DATE-TIME value in UTC
-       ("YYYYMMDDTHHMMSSZ" style format) - REQUIRED
+   2.  "rid" set to an iCalendar format DATE or DATE-TIME value (with
+       format choice determined as per description below) - REQUIRED
 
    3.  "uid" set to an iCalendar UID property value - OPTIONAL
 
    The "action" parameter is used to distinguish this operation from
    others that might be defined in the future for calendar object
-   resources.  The "rid" parameter specified the UTC date-time where the
-   split is to occur.  The actual split occurs at the next recurrence
-   instance on or after the "rid" parameter value - the "split point".
+   resources.
+
+   The "rid" parameter specifies the date or date-time where the split
+   is to occur.  The actual split occurs at the next recurrence instance
+   on or after the "rid" parameter value - the "split point".  The "rid"
+   parameter value is an iCalendar DATE or DATE-TIME value that follows
+   that matched the event's DTSTART value as follows:
+
+   +---------------------------+--------------------+------------------+
+   | DTSTART value             | rid value          | rid example      |
+   +---------------------------+--------------------+------------------+
+   | DATE                      | DATE               | 20150626         |
+   | DATE-TIME UTC             | DATE-TIME UTC      | 20150626T120000Z |
+   | DATE-TIME local+time zone | DATE-TIME UTC      | 20150626T120000Z |
+   | DATE-TIME floating        | DATE-TIME floating | 20150626T120000  |
+   +---------------------------+--------------------+------------------+
+
    The optional "uid" parameter can be used by the client to specify the
    iCalendar UID property value used in the new calendar object resource
    created by the server.  In the absence of the "uid" parameter, the
@@ -145,9 +159,17 @@
    resource.
 
    Clients MUST include both "action" and "rid" parameters in the POST
-   request and MUST ensure a valid date-time value is used.  The date-
-   time value MUST NOT be earlier than the start time of the first
+   request and MUST ensure a valid date-time value is used.  The "rid"
+   parameter value MUST NOT be earlier than the start time of the first
    instance of the recurrence set, and it MUST NOT be later than the
+
+
+
+Daboo                   Expires December 28, 2015               [Page 3]
+
+                       CalDAV Recurrence Splitting             June 2015
+
+
    start time of the last instance of the recurrence set.  If the "rid"
    parameter value is not of the correct format or missing, the server
    MUST return a DAV:error response with the CALDAV:valid-rid-parameter
@@ -162,14 +184,6 @@
    too long as per reasonable requirements), then it MUST return a
    DAV:error response with the CS:invalid-split pre-condition code.
 
-
-
-
-Daboo                      Expires May 3, 2015                  [Page 3]
-
-                       CalDAV Recurrence Splitting          October 2014
-
-
    Clients MAY include an HTTP "Prefer" request header including the
    value "return=representation" (see [RFC7240]).  That instructs the
    server to return a WebDAV multistatus response containing two
@@ -203,6 +217,15 @@
            by subtracting the number of instances prior to the split
            point.
 
+
+
+
+
+Daboo                   Expires December 28, 2015               [Page 4]
+
+                       CalDAV Recurrence Splitting             June 2015
+
+
        E.  The "DTSTART" property of the master instance is adjusted to
            the value of the first instance of the "RRULE" on or after
            the split point, or, in the absence of an "RRULE", to the
@@ -219,16 +242,10 @@
        C.  Any "RRULE" property that only generates instances on or
            after the split point is removed.
 
-
-
-Daboo                      Expires May 3, 2015                  [Page 4]
-
-                       CalDAV Recurrence Splitting          October 2014
-
-
        D.  Any remaining "RRULE" property has an "UNTIL" value applied,
-           with the until value being one second less than the split
-           point.
+           with the until value being one second (for a "rid" DATE-TIME
+           value) or one day (for a "rid" DATE value) less than the
+           split point.
 
        E.  The "UID" property of all components is changed to (the same)
            new value.
@@ -256,6 +273,15 @@
        attendee with both the modified (old) resource data and the new
        resource.  This will likely result in loss of per-attendee data
        such as alarms (though the participation status might be
+
+
+
+
+Daboo                   Expires December 28, 2015               [Page 5]
+
+                       CalDAV Recurrence Splitting             June 2015
+
+
        preserved if the calendar user agent processing the new iTIP
        message for the new resource allows it).
 
@@ -272,16 +298,6 @@
    Assume the following iCalendar data is stored in the resource with
    URI "/event.ics":
 
-
-
-
-
-
-Daboo                      Expires May 3, 2015                  [Page 5]
-
-                       CalDAV Recurrence Splitting          October 2014
-
-
    BEGIN:VCALENDAR
    PRODID:-//Example Inc.//Example Calendar//EN
    VERSION:2.0
@@ -314,6 +330,14 @@
    Content-Type: text/xml
    Content-Length: xxxx
 
+
+
+
+Daboo                   Expires December 28, 2015               [Page 6]
+
+                       CalDAV Recurrence Splitting             June 2015
+
+
    <?xml version='1.0' encoding='UTF-8'?>
    <multistatus xmlns='DAV:'>
      <response>
@@ -330,14 +354,6 @@
    DTSTART:20140110T120000Z
    DURATION:PT1H
    DTSTAMP:20140110T135358Z
-
-
-
-Daboo                      Expires May 3, 2015                  [Page 6]
-
-                       CalDAV Recurrence Splitting          October 2014
-
-
    RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:8DE45ECB-8145-
     4AEC-B3E1-11A9DB22A578
    RRULE:FREQ=DAILY;COUNT=11
@@ -370,6 +386,14 @@
    END:VEVENT
    END:VCALENDAR
    </calendar-data>
+
+
+
+Daboo                   Expires December 28, 2015               [Page 7]
+
+                       CalDAV Recurrence Splitting             June 2015
+
+
          </prop>
          <status>HTTP/1.1 200 OK</status>
        </propstat>
@@ -386,14 +410,6 @@
    "RELTYPE" parameter set to "X-CALENDARSERVER-RECURRENCE-SET" and a
    value set to a new "UID" value.
 
-
-
-
-Daboo                      Expires May 3, 2015                  [Page 7]
-
-                       CalDAV Recurrence Splitting          October 2014
-
-
 4.  Server Initiated Splitting
 
    Servers can automatically split events that have already been stored
@@ -426,6 +442,14 @@
               "Calendaring Extensions to WebDAV (CalDAV)", RFC 4791,
               March 2007.
 
+
+
+
+Daboo                   Expires December 28, 2015               [Page 8]
+
+                       CalDAV Recurrence Splitting             June 2015
+
+
    [RFC5545]  Desruisseaux, B., "Internet Calendaring and Scheduling
               Core Object Specification (iCalendar)", RFC 5545,
               September 2009.
@@ -439,17 +463,6 @@
 
    [RFC7240]  Snell, J., "Prefer Header for HTTP", RFC 7240, June 2014.
 
-
-
-
-
-
-
-Daboo                      Expires May 3, 2015                  [Page 8]
-
-                       CalDAV Recurrence Splitting          October 2014
-
-
 Appendix A.  Acknowledgments
 
    This specification is the result of discussions between the Apple
@@ -457,6 +470,14 @@
 
 Appendix B.  Change History
 
+   Changes since -02
+
+   1.  Clarify that rid query parameter value matches the event DTSTART
+       value type using the UNTIL "rules"
+
+   2.  Clarify that earlier event uses an UNTIL one second or one day
+       less that the "rid" value depending on the value type.
+
    Changes since -01
 
    1.  Added "uid" query parameter to POST request
@@ -480,25 +501,4 @@
 
 
 
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Daboo                      Expires May 3, 2015                  [Page 9]
+Daboo                   Expires December 28, 2015               [Page 9]

Modified: CalendarServer/branches/users/cdaboo/cfod/doc/Extensions/caldav-recursplit.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/doc/Extensions/caldav-recursplit.xml	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/doc/Extensions/caldav-recursplit.xml	2015-07-28 02:43:19 UTC (rev 15011)
@@ -18,7 +18,7 @@
 <?rfc compact="yes"?>
 <?rfc subcompact="no"?>
 <?rfc private="Calendar Server Extension"?>
-<rfc ipr="none" docName='caldav-recursplit-01'>
+<rfc ipr="none" docName='caldav-recursplit-02'>
     <front>
         <title abbrev="CalDAV Recurrence Splitting">Smart Splitting of Recurring Events in CalDAV</title> 
         <author initials="C." surname="Daboo" fullname="Cyrus Daboo">
@@ -79,12 +79,37 @@
           <t>To split an existing calendar object resource containing a recurring event, a client issues an HTTP POST resource with the request-uri set to the URI of the calendar object resource. The client also includes the following two URI query parameters, and one optional one:
             <list style='numbers'>
               <t>"action" set to the value "split" - REQUIRED</t>
-              <t>"rid" set to an iCalendar format DATE-TIME value in UTC ("YYYYMMDDTHHMMSSZ" style format) - REQUIRED</t>
+              <t>"rid" set to an iCalendar format DATE or DATE-TIME value (with format choice determined as per description below) - REQUIRED</t>
               <t>"uid" set to an iCalendar UID property value - OPTIONAL</t>
             </list>
-            The "action" parameter is used to distinguish this operation from others that might be defined in the future for calendar object resources. The "rid" parameter specified the UTC date-time where the split is to occur. The actual split occurs at the next recurrence instance on or after the "rid" parameter value - the "split point". The optional "uid" parameter can be used by the client to specify the iCalendar UID property value used in the new calendar object resource created by the server. In the absence of the "uid" parameter, the server will itself generate the UID value for the new calendar object resource.</t>
+            </t>
+            <t>The "action" parameter is used to distinguish this operation from others that might be defined in the future for calendar object resources.</t>
+            <t>The "rid" parameter specifies the date or date-time where the split is to occur. The actual split occurs at the next recurrence instance on or after the "rid" parameter value - the "split point". The "rid" parameter value is an iCalendar DATE or DATE-TIME value that follows that matched the event's DTSTART value as follows:</t>
+            <texttable>
+              <ttcol>DTSTART value</ttcol>
+              <ttcol>rid value</ttcol>
+              <ttcol>rid example</ttcol>
+
+              <c>DATE</c>
+              <c>DATE</c>
+              <c>20150626</c>
+
+              <c>DATE-TIME UTC</c>
+              <c>DATE-TIME UTC</c>
+              <c>20150626T120000Z</c>
+
+              <c>DATE-TIME local+time zone</c>
+              <c>DATE-TIME UTC</c>
+              <c>20150626T120000Z</c>
+
+              <c>DATE-TIME floating</c>
+              <c>DATE-TIME floating</c>
+              <c>20150626T120000</c>
+
+            </texttable>
+            <t>The optional "uid" parameter can be used by the client to specify the iCalendar UID property value used in the new calendar object resource created by the server. In the absence of the "uid" parameter, the server will itself generate the UID value for the new calendar object resource.</t>
             <t>
-            Clients MUST include both "action" and "rid" parameters in the POST request and MUST ensure a valid date-time value is used. The date-time value MUST NOT be earlier than the start time of the first instance of the recurrence set, and it MUST NOT be later than the start time of the last instance of the recurrence set. If the "rid" parameter value is not of the correct format or missing, the server MUST return a DAV:error response with the CALDAV:valid-rid-parameter pre-condition code. If the "rid" parameter is valid, but outside of the allowed range, or the targeted calendar object resource is not recurring, then the server MUST return a DAV:error response with the CS:invalid-split pre-condition code. The server MUST reject any attempt by an attendee to split their copy of a scheduled calendar object resource - only organizers are allowed to split events. If the optional "uid" parameter is used by the client, and the server determines that the specified value is i
 nvalid (e.g., too short or too long as per reasonable requirements), then it MUST return a DAV:error response with the CS:invalid-split pre-condition code.
+            Clients MUST include both "action" and "rid" parameters in the POST request and MUST ensure a valid date-time value is used. The "rid" parameter value MUST NOT be earlier than the start time of the first instance of the recurrence set, and it MUST NOT be later than the start time of the last instance of the recurrence set. If the "rid" parameter value is not of the correct format or missing, the server MUST return a DAV:error response with the CALDAV:valid-rid-parameter pre-condition code. If the "rid" parameter is valid, but outside of the allowed range, or the targeted calendar object resource is not recurring, then the server MUST return a DAV:error response with the CS:invalid-split pre-condition code. The server MUST reject any attempt by an attendee to split their copy of a scheduled calendar object resource - only organizers are allowed to split events. If the optional "uid" parameter is used by the client, and the server determines that the specified valu
 e is invalid (e.g., too short or too long as per reasonable requirements), then it MUST return a DAV:error response with the CS:invalid-split pre-condition code.
           </t>
           <t>
             Clients MAY include an HTTP "Prefer" request header including the value "return=representation" (see <xref target='RFC7240'/>). That instructs the server to return a WebDAV multistatus response containing two responses: one for the targeted resource and one for the new resource created as a result of the split. The multistatus response MUST include the DAV:getetag and CALDAV:calendar-data properties for each resource. In the absence of the "Prefer:return=representation" request header, the server MUST return an HTTP "Split-Component-URL" response header whose value is the URI of the new resource created as a result of the split.
@@ -107,7 +132,7 @@
                   <t>Any overridden components with a "RECURRENCE-ID" property value on or after the split point are removed.</t>
                   <t>Any "RDATE" or "EXDATE" property values on or after the split point are removed.</t>
                   <t>Any "RRULE" property that only generates instances on or after the split point is removed.</t>
-                  <t>Any remaining "RRULE" property has an "UNTIL" value applied, with the until value being one second less than the split point.</t>
+                  <t>Any remaining "RRULE" property has an "UNTIL" value applied, with the until value being one second (for a "rid" DATE-TIME value) or one day (for a "rid" DATE value) less than the split point.</t>
                   <t>The "UID" property of all components is changed to (the same) new value.</t>
                   <t>Attendee participation status MUST NOT be changed.</t>
                 </list>
@@ -259,6 +284,12 @@
             </t>
         </section>
         <section title='Change History'>
+          <t>Changes since -02
+            <list style='numbers'>
+              <t>Clarify that rid query parameter value matches the event DTSTART value type using the UNTIL "rules"</t>
+              <t>Clarify that earlier event uses an UNTIL one second or one day less that the "rid" value depending on the value type.</t>
+            </list>
+          </t>
           <t>Changes since -01
             <list style='numbers'>
               <t>Added "uid" query parameter to POST request</t>

Modified: CalendarServer/branches/users/cdaboo/cfod/requirements-dev.txt
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/requirements-dev.txt	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/requirements-dev.txt	2015-07-28 02:43:19 UTC (rev 15011)
@@ -8,4 +8,4 @@
 q
 tl.eggdeps
 --editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVClientLibrary/trunk@14856#egg=CalDAVClientLibrary
---editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVTester/trunk@14892#egg=CalDAVTester
+--editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVTester/trunk@14955#egg=CalDAVTester

Modified: CalendarServer/branches/users/cdaboo/cfod/requirements-stable.txt
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/requirements-stable.txt	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/requirements-stable.txt	2015-07-28 02:43:19 UTC (rev 15011)
@@ -36,7 +36,7 @@
             #pyOpenSSL
         pycrypto==2.6.1
 
-    --editable svn+http://svn.calendarserver.org/repository/calendarserver/twext/branches/users/cdaboo/cfod@14909#egg=twextpy
+    --editable svn+http://svn.calendarserver.org/repository/calendarserver/twext/branches/users/cdaboo/cfod@15010#egg=twextpy
         cffi==1.1.0
             pycparser==2.13
         #twisted
@@ -56,16 +56,18 @@
 
     pyOpenSSL==0.14
         cryptography==0.9
+        	idna
             #pyasn1
             #cffi
             enum34==1.0.4
+            ipaddress
             setuptools==17.0
             #six
         six==1.9.0
 
     --editable svn+http://svn.calendarserver.org/repository/calendarserver/PyKerberos/trunk@13420#egg=kerberos
 
-    --editable svn+http://svn.calendarserver.org/repository/calendarserver/PyCalendar/trunk@14695#egg=pycalendar
+    --editable svn+http://svn.calendarserver.org/repository/calendarserver/PyCalendar/trunk@14915#egg=pycalendar
     python-dateutil==1.5  # Note: v2.0+ is for Python 3
     pytz==2015.4
 

Modified: CalendarServer/branches/users/cdaboo/cfod/setup.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/setup.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/setup.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -322,11 +322,11 @@
 install_requirements = [
     # Core frameworks
     "zope.interface",
-    "Twisted>=13.2.0",
+    "Twisted>=15.2.1",
     "twextpy",
 
     # Security frameworks
-    "pyOpenSSL>=0.13.1",  # also for Twisted
+    "pyOpenSSL>=0.14",    # also for Twisted
     "service_identity",   # for Twisted
     "pycrypto",           # for Twisted
     "kerberos",

Modified: CalendarServer/branches/users/cdaboo/cfod/support/Apple.make
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/support/Apple.make	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/support/Apple.make	2015-07-28 02:43:19 UTC (rev 15011)
@@ -58,6 +58,12 @@
 
 build:: $(BuildDirectory)/$(Project)
 
+build:: build-wrapper
+build-wrapper: $(BuildDirectory)/python-wrapper
+
+$(BuildDirectory)/python-wrapper: $(Sources)/support/python-wrapper.c
+	$(CC) $(Sources)/support/python-wrapper.c -o $(BuildDirectory)/python-wrapper
+
 install:: install-python
 install-python:: build
 	@#
@@ -85,18 +91,15 @@
 	@#
 	@# Set up a virtual environment in Server.app; we'll install into that.
 	@# That creates a self-contained environment which has specific version of
-	@# (almost) all of our dependancies in it.
+	@# (almost) all of our dependencies in it.
 	@# Use --system-site-packages so that we use the packages provided by the
 	@# OS, such as PyObjC.
-	@# Use --always-copy because we want copies of, not links to, the system
-	@# python, as Server.app is an independent product train.
 	@#
 	@echo "Creating virtual environment...";
 	$(_v) $(RMDIR) "$(DSTROOT)$(CS_VIRTUALENV)";
 	$(_v) PYTHONPATH="$(BuildDirectory)/pytools/lib" \
 	          "$(PYTHON)" -m virtualenv              \
 		          --system-site-packages             \
-		          --always-copy                      \
 		          "$(DSTROOT)$(CS_VIRTUALENV)";
 	@#
 	@# Use the pip in the virtual environment (as opposed to pip in the OS) to
@@ -131,7 +134,7 @@
 	              --ignore-installed                                      \
 	              Twisted;
 
-	@echo "Installing CalendarServer and remaining dependancies...";
+	@echo "Installing CalendarServer and remaining dependencies...";
 	$(_v) $(Environment)                                                  \
 	          "$(DSTROOT)$(CS_VIRTUALENV)/bin/pip" install                \
 	              --disable-pip-version-check                             \
@@ -153,14 +156,18 @@
 	$(_v) perl -i -pe "s|#PATH|export PYTHON=$(CS_VIRTUALENV)/bin/python;|" "$(DSTROOT)$(CS_VIRTUALENV)/bin/caldavd";
 	@echo "Stripping binaries...";
 	$(_v) find "$(DSTROOT)$(CS_VIRTUALENV)" -type f -name "*.so" -print0 | xargs -0 $(STRIP) -Sx;
-	@echo "Updating install location of Python library...";
-	$(_v) find "$(DSTROOT)$(CS_VIRTUALENV)/bin" -type f -name "python*" -print0 | \
-	          xargs -0 -n 1 install_name_tool -change "@executable_path/../.Python" "$(CS_VIRTUALENV)/.Python";
-	$(_v) install_name_tool -id "$(CS_VIRTUALENV)/.Python" "$(DSTROOT)$(CS_VIRTUALENV)/.Python";
 	@echo "Putting comments into empty files...";
 	$(_v) find "$(DSTROOT)$(CS_VIRTUALENV)" -type f -size 0 -name "*.py" -exec sh -c 'printf "# empty\n" > {}' ";";
 	$(_v) find "$(DSTROOT)$(CS_VIRTUALENV)" -type f -size 0 -name "*.h" -exec sh -c 'printf "/* empty */\n" > {}' ";";
 
+	@#
+	@# Undo virtualenv so we use the python-wrapper
+	@#
+	@echo "Undo virtualenv...";
+	$(_v) cp "$(BuildDirectory)/python-wrapper" "$(DSTROOT)$(CS_VIRTUALENV)/bin"
+	$(_v) $(STRIP) -Sx "$(DSTROOT)$(CS_VIRTUALENV)/bin/python-wrapper"
+	$(_v) "$(Sources)/support/undo-virtualenv" "$(DSTROOT)$(CS_VIRTUALENV)"
+
 install:: install-config
 install-config::
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(ETCDIR)$(CALDAVDSUBDIR)";
@@ -216,12 +223,14 @@
 	@echo "Installing CalDAVTester package...";
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)/AppleInternal/ServerTools";
 	$(_v) tar -C "$(DSTROOT)/AppleInternal/ServerTools" -xvzf "$(Sources)/CalDAVTester.tgz";
+	$(_v) chown -R root:wheel "$(DSTROOT)/AppleInternal/ServerTools/CalDAVTester";
 
 install:: install-caldavsim
 install-caldavsim::
 	@echo "Installing caldavsim...";
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)/AppleInternal/ServerTools";
 	$(_v) cp -a "$(Sources)/contrib/performance/" "$(DSTROOT)/AppleInternal/ServerTools/CalDAVSim/";
+	$(_v) chown -R root:wheel "$(DSTROOT)/AppleInternal/ServerTools/CalDAVSim";
 
 #
 # Automatic Extract

Copied: CalendarServer/branches/users/cdaboo/cfod/support/python-wrapper.c (from rev 15009, CalendarServer/trunk/support/python-wrapper.c)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/support/python-wrapper.c	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/support/python-wrapper.c	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,47 @@
+//
+//  python-wrapper.c
+//
+//  Copyright © 2015 Apple Inc. All rights reserved.
+//
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+const char* python = "/usr/bin/python2.7";
+const char* bin = "/Applications/Server.app/Contents/ServerRoot/Library/CalendarServer/bin";
+const char* site = "/Applications/Server.app/Contents/ServerRoot/Library/CalendarServer/lib/python2.7/site-packages";
+
+// Prepend a path to the named environment variable
+int prependToPath(const char* name, const char* prepend) {
+    const char* old_value = getenv(name);
+    char* new_value = NULL;
+    if (old_value == NULL) {
+        // No existing value - set to the prepend value
+        size_t max_length = strlen(prepend) + 1;
+        new_value = malloc(max_length);
+        strlcpy(new_value, prepend, max_length);
+    } else {
+        // Existing value - so prepend with a ":" in between
+        size_t max_length = strlen(old_value) + strlen(prepend) + 2;
+        new_value = malloc(max_length);
+        strlcpy(new_value, prepend, max_length);
+        strlcat(new_value, ":", max_length);
+        strlcat(new_value, old_value, max_length);
+    }
+    setenv(name, new_value, 1);
+    free(new_value);
+    return 0;
+}
+
+int main(int argc, const char * argv[]) {
+    
+    // Update PATH and PYTHONPATH
+    prependToPath("PATH", bin);
+    prependToPath("PYTHONPATH", site);
+    
+    // Launch real python
+    argv[0] = python;
+    return execvp(python, (char* const*)argv);
+}

Copied: CalendarServer/branches/users/cdaboo/cfod/support/undo-virtualenv (from rev 15009, CalendarServer/trunk/support/undo-virtualenv)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/support/undo-virtualenv	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/support/undo-virtualenv	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,48 @@
+#!/bin/sh
+
+##
+# Copyright (c) 2015 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.
+##
+
+set -e
+set -u
+
+base="${1}";
+if [ $# == 1 ]; then
+  appbase="/Applications/Server.app/Contents/ServerRoot/Library/CalendarServer";
+else
+  appbase="${2}";
+fi;
+
+# Remove python binaries
+rm -f "${base}/bin/python";
+rm -f "${base}/bin/python2";
+rm -f "${base}/bin/python2.7";
+
+# Remove unused virtualenv files
+rm -f "${base}/.Python";
+rm -f "${base}/include/python2.7";
+rm -f "${base}/lib/python2.7/"*.py*;
+rm -f "${base}/lib/python2.7/config";
+rm -rf "${base}/lib/python2.7/distutils";
+rm -f "${base}/lib/python2.7/encodings";
+rm -f "${base}/lib/python2.7/lib-dynload";
+rm -f "${base}/lib/python2.7/orig-prefix.txt";
+
+# Create links
+cd "${base}/bin"
+ln -s "python-wrapper" "python";
+ln -s "python-wrapper" "python2";
+ln -s "python-wrapper" "python2.7";

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/controlapi.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/controlapi.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/controlapi.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -28,7 +28,7 @@
 
 from calendarserver.tools.util import recordForPrincipalID
 
-from twext.enterprise.jobqueue import JobItem
+from twext.enterprise.jobqueue import JobItem, WORK_PRIORITY_HIGH, WORK_WEIGHT_1
 from twext.python.log import Logger
 
 from twisted.internet import reactor
@@ -42,6 +42,7 @@
 from txdav.caldav.datastore.scheduling.work import ScheduleOrganizerWork, \
     ScheduleOrganizerSendWork, ScheduleReplyWork, ScheduleRefreshWork, \
     ScheduleAutoReplyWork
+from txdav.common.datastore.work.load_work import TestWork
 from txdav.who.groups import GroupCacherPollingWork, GroupRefreshWork, \
     GroupAttendeeReconciliationWork, GroupDelegateChangesWork, \
     GroupShareeReconciliationWork
@@ -355,6 +356,45 @@
 
 
     @inlineCallbacks
+    def action_testwork(self, j):
+        """
+        Wait for all schedule queue items to complete.
+        """
+
+        try:
+            when = j["when"]
+        except KeyError:
+            when = 0
+        try:
+            priority = j["priority"]
+        except KeyError:
+            priority = WORK_PRIORITY_HIGH
+        try:
+            weight = j["weight"]
+        except KeyError:
+            weight = WORK_WEIGHT_1
+        try:
+            delay = j["delay"]
+        except KeyError:
+            delay = 0
+        try:
+            jobs = j["jobs"]
+        except KeyError:
+            jobs = 1
+
+        for _ in range(jobs):
+            yield TestWork.schedule(
+                self._store,
+                when,
+                priority,
+                weight,
+                delay,
+            )
+
+        returnValue(self._ok("ok", "Test work scheduled"))
+
+
+    @inlineCallbacks
     def action_revisioncleanup(self, j):
         """
         Wait for all schedule queue items to complete.

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/customxml.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/customxml.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -68,10 +68,18 @@
     "calendarserver-sharing-no-scheduling",
 )
 
+calendarserver_group_sharee_compliance = (
+    "calendarserver-group-sharee",
+)
+
 calendarserver_partstat_changes_compliance = (
     "calendarserver-partstat-changes",
 )
 
+calendarserver_group_attendee_compliance = (
+    "calendarserver-group-attendee",
+)
+
 calendarserver_home_sync_compliance = (
     "calendarserver-home-sync",
 )

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/extensions.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/extensions.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -1041,7 +1041,9 @@
             applyTo = True
 
         elif child.qname() == (calendarserver_namespace, "search-token"):
-            tokens.append(child.toString())
+            tokenValue = child.toString().strip()
+            if tokenValue:
+                tokens.append(tokenValue)
 
         elif child.qname() == (calendarserver_namespace, "limit"):
             try:

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/ical.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/ical.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -1255,7 +1255,10 @@
                     rrule.setUseUntil(True)
                     rrule.setUseCount(False)
                     until = rid.duplicate()
-                    until.offsetSeconds(-1)
+                    if until.isDateOnly():
+                        until.offsetDay(-1)
+                    else:
+                        until.offsetSeconds(-1)
                     rrule.setUntil(until)
 
             # Remove any RDATEs or EXDATEs in the future
@@ -3541,13 +3544,6 @@
                 if 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")
 
@@ -3586,7 +3582,19 @@
 
 
     def _reconcileGroupAttendee(self, groupCUA, memberAttendeeProps):
+        """
+        Make sure there are attendee properties for every member of the group, and no
+        other attendee properties marked as a member of the group. Note that attendee
+        properties already present with a MEMBER parameter are not given a MEMBER
+        parameter if they are in the group. This ensures that manually added attendees
+        are not automatically removed when they dissappear from a group.
 
+        @param groupCUA: calendar user address of the group
+        @type groupCUA: L{str}
+        @param memberAttendeeProps: list of member properties
+        @type memberAttendeeProps: L{tuple}
+        """
+
         changed = False
         for component in self.subcomponents(ignore=True):
             oldAttendeeProps = tuple(component.properties("ATTENDEE"))
@@ -3622,7 +3630,15 @@
 
 
     def reconcileGroupAttendees(self, groupCUAToAttendeeMemberPropMap):
+        """
+        Reconcile the attendee properties in this L{Component}.
 
+        @param groupCUAToAttendeeMemberPropMap: map of group to potential attendees
+        @type groupCUAToAttendeeMemberPropMap: L{dict}
+        """
+
+        # Reconcile the member ship list of each group attendee, keeping track of which
+        # groups are actually used
         changed = False
         allMemberCUAs = set()
         nonemptyGroupCUAs = set()
@@ -3632,7 +3648,8 @@
             if memberAttendeeProps:
                 nonemptyGroupCUAs.add(groupCUA)
 
-        # remove orphans
+        # Remove attendee properties that have a MEMBER value that contains only groups no longer
+        # used in this component
         for component in self.subcomponents(ignore=True):
             for attendeeProp in tuple(component.properties("ATTENDEE")):
                 if attendeeProp.hasParameter("MEMBER"):

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/stdconfig.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/stdconfig.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -316,6 +316,10 @@
 
     "DirectoryRealmName": "",
 
+    # Apply an additional filter for attendee lookups where names must start
+    # with the search tokens rather than just contain them.
+    "DirectoryFilterStartsWith": False,
+
     #
     # Locations and Resources service
     #
@@ -817,16 +821,16 @@
                 "EnableStaggering" : False,
                 "StaggerSeconds" : 3,
                 "CalDAV" : {
-                    "CertificatePath" : "",
-                    "PrivateKeyPath" : "",
-                    "AuthorityChainPath" : "",
+                    "CertificatePath" : "Certificates/apns:com.apple.calendar.cert.pem",
+                    "PrivateKeyPath" : "Certificates/apns:com.apple.calendar.key.pem",
+                    "AuthorityChainPath" : "Certificates/apns:com.apple.calendar.chain.pem",
                     "Passphrase" : "",
                     "Topic" : "",
                 },
                 "CardDAV" : {
-                    "CertificatePath" : "",
-                    "PrivateKeyPath" : "",
-                    "AuthorityChainPath" : "",
+                    "CertificatePath" : "Certificates/apns:com.apple.contact.cert.pem",
+                    "PrivateKeyPath" : "Certificates/apns:com.apple.contact.key.pem",
+                    "AuthorityChainPath" : "Certificates/apns:com.apple.contact.chain.pem",
                     "Passphrase" : "",
                     "Topic" : "",
                 },
@@ -1218,6 +1222,12 @@
     ("ConfigRoot", ("Scheduling", "iSchedule", "DKIM", "PublicKeyFile",)),
     ("ConfigRoot", ("Scheduling", "iSchedule", "DKIM", "PrivateExchanges",)),
     ("ConfigRoot", "WritableConfigFile"),
+    ("ConfigRoot", ("Notifications", "Services", "APNS", "CalDAV", "AuthorityChainPath",)),
+    ("ConfigRoot", ("Notifications", "Services", "APNS", "CalDAV", "CertificatePath",)),
+    ("ConfigRoot", ("Notifications", "Services", "APNS", "CalDAV", "PrivateKeyPath",)),
+    ("ConfigRoot", ("Notifications", "Services", "APNS", "CardDAV", "AuthorityChainPath",)),
+    ("ConfigRoot", ("Notifications", "Services", "APNS", "CardDAV", "CertificatePath",)),
+    ("ConfigRoot", ("Notifications", "Services", "APNS", "CardDAV", "PrivateKeyPath",)),
     ("LogRoot", "AccessLogFile"),
     ("LogRoot", "ErrorLogFile"),
     ("LogRoot", "AgentLogFile"),
@@ -1717,6 +1727,8 @@
             compliance += customxml.calendarserver_sharing_compliance
             # TODO: This is only needed whilst we do not support scheduling in shared calendars
             compliance += customxml.calendarserver_sharing_no_scheduling_compliance
+            if config.Sharing.Calendars.Enabled and config.Sharing.Calendars.Groups.Enabled:
+                compliance += customxml.calendarserver_group_sharee_compliance
         if configDict.EnableCalendarQueryExtended:
             compliance += caldavxml.caldav_query_extended_compliance
         if configDict.EnableDefaultAlarms:
@@ -1725,6 +1737,8 @@
             compliance += caldavxml.caldav_managed_attachments_compliance
         if configDict.Scheduling.Options.TimestampAttendeePartStatChanges:
             compliance += customxml.calendarserver_partstat_changes_compliance
+        if config.GroupAttendees.Enabled:
+            compliance += customxml.calendarserver_group_attendee_compliance
         if configDict.EnableTimezonesByReference:
             compliance += caldavxml.caldav_timezones_by_reference_compliance
         compliance += customxml.calendarserver_recurrence_split

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/storebridge.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/storebridge.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -2011,34 +2011,35 @@
                 userprivs.extend(privileges)
 
             principal = yield self.principalForUID(invite.shareeUID)
-            aces += (
-                # Inheritable specific access for the resource's associated principal.
-                davxml.ACE(
-                    davxml.Principal(davxml.HRef(principal.principalURL())),
-                    davxml.Grant(*userprivs),
-                    davxml.Protected(),
-                    TwistedACLInheritable(),
-                ),
-            )
-
-            if config.EnableProxyPrincipals:
+            if principal is not None:
                 aces += (
-                    # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
+                    # Inheritable specific access for the resource's associated principal.
                     davxml.ACE(
-                        davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-read/"))),
+                        davxml.Principal(davxml.HRef(principal.principalURL())),
                         davxml.Grant(*userprivs),
                         davxml.Protected(),
                         TwistedACLInheritable(),
                     ),
-                    # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
-                    davxml.ACE(
-                        davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-write/"))),
-                        davxml.Grant(*userprivs),
-                        davxml.Protected(),
-                        TwistedACLInheritable(),
-                    ),
                 )
 
+                if config.EnableProxyPrincipals:
+                    aces += (
+                        # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
+                        davxml.ACE(
+                            davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-read/"))),
+                            davxml.Grant(*userprivs),
+                            davxml.Protected(),
+                            TwistedACLInheritable(),
+                        ),
+                        # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
+                        davxml.ACE(
+                            davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-write/"))),
+                            davxml.Grant(*userprivs),
+                            davxml.Protected(),
+                            TwistedACLInheritable(),
+                        ),
+                    )
+
         returnValue(aces)
 
 
@@ -3162,12 +3163,18 @@
                 content_type, filename = _getContentInfo()
                 attachment, location = (yield self._newStoreObject.addAttachment(rids, content_type, filename, request.stream))
                 post_result = Response(CREATED)
+                if not hasattr(request, "extendedLogItems"):
+                    request.extendedLogItems = {}
+                request.extendedLogItems["cl"] = str(attachment.size())
 
             elif action == "attachment-update":
                 mid = _getMID()
                 content_type, filename = _getContentInfo()
                 attachment, location = (yield self._newStoreObject.updateAttachment(mid, content_type, filename, request.stream))
                 post_result = Response(NO_CONTENT)
+                if not hasattr(request, "extendedLogItems"):
+                    request.extendedLogItems = {}
+                request.extendedLogItems["cl"] = str(attachment.size())
 
             elif action == "attachment-remove":
                 rids = _getRIDs()

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_config.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_config.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -385,7 +385,21 @@
         config.EnableProxyPrincipals = False
         self.assertTrue("calendar-proxy" not in resource.davComplianceClasses())
 
+        self.assertTrue("calendarserver-group-sharee" in resource.davComplianceClasses())
+        config.Sharing.Calendars.Groups.Enabled = False
+        config.update()
+        self.assertTrue("calendarserver-group-sharee" not in resource.davComplianceClasses())
+        config.Sharing.Calendars.Groups.Enabled = True
+        config.update()
 
+        self.assertTrue("calendarserver-group-attendee" in resource.davComplianceClasses())
+        config.GroupAttendees.Enabled = False
+        config.update()
+        self.assertTrue("calendarserver-group-attendee" not in resource.davComplianceClasses())
+        config.GroupAttendees.Enabled = True
+        config.update()
+
+
     def test_logging(self):
         """
         Logging module configures properly.

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_icalendar.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_icalendar.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -8241,9 +8241,6 @@
 
         yield component.normalizeCalendarUserAddresses(lookupFunction, None, toCanonical=True)
 
-        # Location value changed
-        prop = component.mainComponent().getProperty("LOCATION")
-        self.assertEquals(prop.value(), "{Restricted} Buzz")
         prop = component.getAttendeeProperty(("urn:x-uid:buzz",))
         self.assertEquals("urn:x-uid:buzz", prop.value())
         self.assertEquals(prop.parameterValue("CN"), "{Restricted} Buzz")

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_sharing.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_sharing.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -18,25 +18,28 @@
 from txweb2.dav.util import allDataFromStream
 from txweb2.http_headers import MimeType
 from txweb2.iweb import IResponse
+from txweb2.stream import MemoryStream
 
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
 from twistedcaldav import customxml
 from twistedcaldav.config import config
+from twistedcaldav.ical import Component
 from twistedcaldav.test.test_cache import StubResponseCacheResource
 from twistedcaldav.test.util import norequest, StoreTestCase, SimpleStoreRequest
 
 from txdav.common.datastore.sql_tables import _BIND_MODE_DIRECT
 from txdav.xml import element as davxml
 from txdav.xml.parser import WebDAVDocument
-
-from xml.etree.cElementTree import XML
 from txdav.who.wiki import (
     DirectoryRecord as WikiDirectoryRecord,
     DirectoryService as WikiDirectoryService,
     WikiAccessLevel
 )
 
+from xml.etree.cElementTree import XML
+import urlparse
+
 sharedOwnerType = davxml.ResourceType.sharedownercalendar  # @UndefinedVariable
 regularCalendarType = davxml.ResourceType.calendar  # @UndefinedVariable
 
@@ -51,13 +54,13 @@
 
 
 
-class SharingTests(StoreTestCase):
+class BaseSharingTests(StoreTestCase):
 
     def configure(self):
         """
         Override configuration hook to turn on sharing.
         """
-        super(SharingTests, self).configure()
+        super(BaseSharingTests, self).configure()
         self.patch(config.Sharing, "Enabled", True)
         self.patch(config.Sharing.Calendars, "Enabled", True)
         self.patch(config.Authentication.Wiki, "Enabled", True)
@@ -65,7 +68,7 @@
 
     @inlineCallbacks
     def setUp(self):
-        yield super(SharingTests, self).setUp()
+        yield super(BaseSharingTests, self).setUp()
         self.resource = yield self._getResource()
 
 
@@ -73,7 +76,7 @@
     def _refreshRoot(self, request=None):
         if request is None:
             request = norequest()
-        result = yield super(SharingTests, self)._refreshRoot(request)
+        result = yield super(BaseSharingTests, self)._refreshRoot(request)
         self.resource = (
             yield self.site.resource.locateChild(request, ["calendar"])
         )[0]
@@ -228,6 +231,9 @@
         return None
 
 
+
+class SharingTests(BaseSharingTests):
+
     @inlineCallbacks
     def test_upgradeToShare(self):
 
@@ -1499,3 +1505,156 @@
                 customxml.InviteStatusAccepted(),
             ),
         ))
+
+
+
+class DropboxSharingTests(BaseSharingTests):
+
+    def configure(self):
+        """
+        Override configuration hook to turn on dropbox.
+        """
+        super(DropboxSharingTests, self).configure()
+        self.patch(config, "EnableDropBox", True)
+        self.patch(config, "EnableManagedAttachments", False)
+
+
+    @inlineCallbacks
+    def test_dropboxWithMissingInvitee(self):
+
+        yield self.resource.upgradeToShare()
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+            <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+                <CS:set>
+                    <D:href>mailto:user02 at example.com</D:href>
+                    <CS:summary>My Shared Calendar</CS:summary>
+                    <CS:read-write/>
+                </CS:set>
+            </CS:share>
+            """)
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        uid = self._getUIDElementValue(propInvite)
+
+        yield self._doPOSTSharerAccept("""<?xml version='1.0' encoding='UTF-8'?>
+            <invite-reply xmlns='http://calendarserver.org/ns/'>
+              <href xmlns='DAV:'>mailto:user01 at example.com</href>
+              <invite-accepted/>
+              <hosturl>
+                <href xmlns='DAV:'>/calendars/__uids__/user01/calendar/</href>
+              </hosturl>
+              <in-reply-to>%s</in-reply-to>
+              <summary>The Shared Calendar</summary>
+              <common-name>User 02</common-name>
+              <first-name>user</first-name>
+              <last-name>02</last-name>
+            </invite-reply>
+            """ % (uid,)
+        )
+
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+        component = Component.fromString("""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20060101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ATTACH;VALUE=URI:/calendars/users/home1/some-dropbox-id/some-dropbox-id/caldavd.plist
+X-APPLE-DROPBOX:/calendars/users/home1/dropbox/some-dropbox-id
+END:VEVENT
+END:VCALENDAR
+""")
+        yield calendar.createCalendarObjectWithName("dropbox.ics", component)
+        yield self.commit()
+
+        yield self.directory.removeRecords(((yield self.userUIDFromShortName("user02")),))
+        self.assertTrue((yield self.userUIDFromShortName("user02")) is None)
+
+        # Get dropbox and test ACLs
+        request = SimpleStoreRequest(self, "GET", "/calendars/__uids__/user01/dropbox/some-dropbox-id/")
+        resource = yield request.locateResource("/calendars/__uids__/user01/dropbox/some-dropbox-id/")
+        acl = yield resource.accessControlList(request)
+        self.assertTrue(acl is not None)
+
+
+
+class MamnagedAttachmentSharingTests(BaseSharingTests):
+
+    def configure(self):
+        """
+        Override configuration hook to turn on managed attachments.
+        """
+        super(MamnagedAttachmentSharingTests, self).configure()
+        self.patch(config, "EnableDropBox", False)
+        self.patch(config, "EnableManagedAttachments", True)
+
+
+    @inlineCallbacks
+    def test_attachmentWithMissingInvitee(self):
+
+        yield self.resource.upgradeToShare()
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+            <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+                <CS:set>
+                    <D:href>mailto:user02 at example.com</D:href>
+                    <CS:summary>My Shared Calendar</CS:summary>
+                    <CS:read-write/>
+                </CS:set>
+            </CS:share>
+            """)
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        uid = self._getUIDElementValue(propInvite)
+
+        yield self._doPOSTSharerAccept("""<?xml version='1.0' encoding='UTF-8'?>
+            <invite-reply xmlns='http://calendarserver.org/ns/'>
+              <href xmlns='DAV:'>mailto:user01 at example.com</href>
+              <invite-accepted/>
+              <hosturl>
+                <href xmlns='DAV:'>/calendars/__uids__/user01/calendar/</href>
+              </hosturl>
+              <in-reply-to>%s</in-reply-to>
+              <summary>The Shared Calendar</summary>
+              <common-name>User 02</common-name>
+              <first-name>user</first-name>
+              <last-name>02</last-name>
+            </invite-reply>
+            """ % (uid,)
+        )
+
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+        component = Component.fromString("""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20060101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+END:VEVENT
+END:VCALENDAR
+""")
+        obj = yield calendar.createCalendarObjectWithName("dropbox.ics", component)
+        _ignore_attachment, location = yield obj.addAttachment(None, MimeType("text", "plain"), "new.txt", MemoryStream("new attachment text"))
+        yield self.commit()
+
+        yield self.directory.removeRecords(((yield self.userUIDFromShortName("user02")),))
+        self.assertTrue((yield self.userUIDFromShortName("user02")) is None)
+
+        # Get dropbox and test ACLs
+        location = urlparse.urlparse(location)[2]
+        location = "/".join(location.split("/")[:-1])
+        request = SimpleStoreRequest(self, "GET", location)
+        resource = yield request.locateResource(location)
+        acl = yield resource.accessControlList(request)
+        self.assertTrue(acl is not None)

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_upgrade.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_upgrade.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/test/test_upgrade.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -1740,8 +1740,8 @@
 UID:1E238CA1-3C95-4468-B8CD-C8A399F78C71
 DTSTART;TZID=US/Pacific:20090203T120000
 DTEND;TZID=US/Pacific:20090203T130000
-ATTENDEE;CN=Wilfredo Sanchez;CUTYPE=INDIVIDUAL;EMAIL=wsanchez at example.com;
- PARTSTAT=ACCEPTED:urn:x-uid:6423F94A-6B76-4A3A-815B-D52CFD77935D
+ATTENDEE;CN=Wilfredo Sanchez-Vega;CUTYPE=INDIVIDUAL;EMAIL=wsanchez at example
+ .com;PARTSTAT=ACCEPTED:urn:x-uid:6423F94A-6B76-4A3A-815B-D52CFD77935D
 ATTENDEE;CN=Double 'quotey' Quotes;CUTYPE=INDIVIDUAL;EMAIL=doublequotes at ex
  ample.com;PARTSTAT=ACCEPTED:urn:x-uid:8E04787E-336D-41ED-A70B-D233AD0DCE6
  F

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Africa/Casablanca.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Africa/Casablanca.ics	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Africa/Casablanca.ics	2015-07-28 02:43:19 UTC (rev 15011)
@@ -27,16 +27,16 @@
 RDATE:20120820T020000
 RDATE:20130810T020000
 RDATE:20140802T020000
-RDATE:20150718T020000
-RDATE:20160709T020000
-RDATE:20170701T020000
-RDATE:20180616T020000
-RDATE:20190608T020000
-RDATE:20200530T020000
-RDATE:20210515T020000
-RDATE:20220507T020000
-RDATE:20230422T020000
-RDATE:20240413T020000
+RDATE:20150719T020000
+RDATE:20160710T020000
+RDATE:20170702T020000
+RDATE:20180617T020000
+RDATE:20190609T020000
+RDATE:20200524T020000
+RDATE:20210516T020000
+RDATE:20220508T020000
+RDATE:20230423T020000
+RDATE:20240414T020000
 TZNAME:WEST
 TZOFFSETFROM:+0000
 TZOFFSETTO:+0100
@@ -59,14 +59,13 @@
 RDATE:20120930T030000
 RDATE:20130707T030000
 RDATE:20140628T030000
-RDATE:20150613T030000
-RDATE:20160604T030000
-RDATE:20170520T030000
-RDATE:20180512T030000
-RDATE:20190504T030000
-RDATE:20200418T030000
-RDATE:20210410T030000
-RDATE:20220402T030000
+RDATE:20150614T030000
+RDATE:20160605T030000
+RDATE:20170521T030000
+RDATE:20180513T030000
+RDATE:20190505T030000
+RDATE:20200419T030000
+RDATE:20210411T030000
 TZNAME:WET
 TZOFFSETFROM:+0100
 TZOFFSETTO:+0000
@@ -108,7 +107,7 @@
 END:STANDARD
 BEGIN:DAYLIGHT
 DTSTART:20140330T020000
-RRULE:FREQ=YEARLY;UNTIL=20220327T020000Z;BYDAY=-1SU;BYMONTH=3
+RRULE:FREQ=YEARLY;UNTIL=20210328T020000Z;BYDAY=-1SU;BYMONTH=3
 TZNAME:WEST
 TZOFFSETFROM:+0000
 TZOFFSETTO:+0100

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Africa/El_Aaiun.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Africa/El_Aaiun.ics	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Africa/El_Aaiun.ics	2015-07-28 02:43:19 UTC (rev 15011)
@@ -39,14 +39,13 @@
 RDATE:20120930T030000
 RDATE:20130707T030000
 RDATE:20140628T030000
-RDATE:20150613T030000
-RDATE:20160604T030000
-RDATE:20170520T030000
-RDATE:20180512T030000
-RDATE:20190504T030000
-RDATE:20200418T030000
-RDATE:20210410T030000
-RDATE:20220402T030000
+RDATE:20150614T030000
+RDATE:20160605T030000
+RDATE:20170521T030000
+RDATE:20180513T030000
+RDATE:20190505T030000
+RDATE:20200419T030000
+RDATE:20210411T030000
 TZNAME:WET
 TZOFFSETFROM:+0100
 TZOFFSETTO:+0000
@@ -61,16 +60,16 @@
 RDATE:20120820T020000
 RDATE:20130810T020000
 RDATE:20140802T020000
-RDATE:20150718T020000
-RDATE:20160709T020000
-RDATE:20170701T020000
-RDATE:20180616T020000
-RDATE:20190608T020000
-RDATE:20200530T020000
-RDATE:20210515T020000
-RDATE:20220507T020000
-RDATE:20230422T020000
-RDATE:20240413T020000
+RDATE:20150719T020000
+RDATE:20160710T020000
+RDATE:20170702T020000
+RDATE:20180617T020000
+RDATE:20190609T020000
+RDATE:20200524T020000
+RDATE:20210516T020000
+RDATE:20220508T020000
+RDATE:20230423T020000
+RDATE:20240414T020000
 TZNAME:WEST
 TZOFFSETFROM:+0000
 TZOFFSETTO:+0100
@@ -91,7 +90,7 @@
 END:STANDARD
 BEGIN:DAYLIGHT
 DTSTART:20140330T020000
-RRULE:FREQ=YEARLY;UNTIL=20220327T020000Z;BYDAY=-1SU;BYMONTH=3
+RRULE:FREQ=YEARLY;UNTIL=20210328T020000Z;BYDAY=-1SU;BYMONTH=3
 TZNAME:WEST
 TZOFFSETFROM:+0000
 TZOFFSETTO:+0100

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/America/Cayman.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/America/Cayman.ics	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/America/Cayman.ics	2015-07-28 02:43:19 UTC (rev 15011)
@@ -8,16 +8,37 @@
 BEGIN:STANDARD
 DTSTART:18900101T000000
 RDATE:18900101T000000
-TZNAME:CMT
-TZOFFSETFROM:-051808
-TZOFFSETTO:-051936
+TZNAME:KMT
+TZOFFSETFROM:-052532
+TZOFFSETTO:-050711
 END:STANDARD
 BEGIN:STANDARD
-DTSTART:19080422T000000
-RDATE:19080422T000000
+DTSTART:19120201T000000
+RDATE:19120201T000000
 TZNAME:EST
-TZOFFSETFROM:-051936
+TZOFFSETFROM:-050711
 TZOFFSETTO:-0500
 END:STANDARD
+BEGIN:STANDARD
+DTSTART:20160101T000000
+RDATE:20160101T000000
+TZNAME:EST
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20160313T020000
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20161106T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
 END:VTIMEZONE
 END:VCALENDAR

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Morocco Standard Time.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Morocco Standard Time.ics	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/Morocco Standard Time.ics	2015-07-28 02:43:19 UTC (rev 15011)
@@ -27,16 +27,16 @@
 RDATE:20120820T020000
 RDATE:20130810T020000
 RDATE:20140802T020000
-RDATE:20150718T020000
-RDATE:20160709T020000
-RDATE:20170701T020000
-RDATE:20180616T020000
-RDATE:20190608T020000
-RDATE:20200530T020000
-RDATE:20210515T020000
-RDATE:20220507T020000
-RDATE:20230422T020000
-RDATE:20240413T020000
+RDATE:20150719T020000
+RDATE:20160710T020000
+RDATE:20170702T020000
+RDATE:20180617T020000
+RDATE:20190609T020000
+RDATE:20200524T020000
+RDATE:20210516T020000
+RDATE:20220508T020000
+RDATE:20230423T020000
+RDATE:20240414T020000
 TZNAME:WEST
 TZOFFSETFROM:+0000
 TZOFFSETTO:+0100
@@ -59,14 +59,13 @@
 RDATE:20120930T030000
 RDATE:20130707T030000
 RDATE:20140628T030000
-RDATE:20150613T030000
-RDATE:20160604T030000
-RDATE:20170520T030000
-RDATE:20180512T030000
-RDATE:20190504T030000
-RDATE:20200418T030000
-RDATE:20210410T030000
-RDATE:20220402T030000
+RDATE:20150614T030000
+RDATE:20160605T030000
+RDATE:20170521T030000
+RDATE:20180513T030000
+RDATE:20190505T030000
+RDATE:20200419T030000
+RDATE:20210411T030000
 TZNAME:WET
 TZOFFSETFROM:+0100
 TZOFFSETTO:+0000
@@ -108,7 +107,7 @@
 END:STANDARD
 BEGIN:DAYLIGHT
 DTSTART:20140330T020000
-RRULE:FREQ=YEARLY;UNTIL=20220327T020000Z;BYDAY=-1SU;BYMONTH=3
+RRULE:FREQ=YEARLY;UNTIL=20210328T020000Z;BYDAY=-1SU;BYMONTH=3
 TZNAME:WEST
 TZOFFSETFROM:+0000
 TZOFFSETTO:+0100

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/links.txt
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/links.txt	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/links.txt	2015-07-28 02:43:19 UTC (rev 15011)
@@ -45,7 +45,6 @@
 America/Atka	America/Adak
 America/Buenos_Aires	America/Argentina/Buenos_Aires
 America/Catamarca	America/Argentina/Catamarca
-America/Cayman	America/Panama
 America/Coral_Harbour	America/Atikokan
 America/Cordoba	America/Argentina/Cordoba
 America/Dominica	America/Port_of_Spain

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/timezones.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/timezones.xml	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/timezones.xml	2015-07-28 02:43:19 UTC (rev 15011)
@@ -2,7 +2,7 @@
 <!DOCTYPE timezones SYSTEM "timezones.dtd">
 
 <timezones>
-  <dtstamp>2015-04-24T16:13:20Z</dtstamp>
+  <dtstamp>2015-07-01T14:44:04Z</dtstamp>
   <timezone>
     <tzid>AUS Central Standard Time</tzid>
     <dtstamp>2014-08-25T14:48:40Z</dtstamp>
@@ -104,9 +104,9 @@
   </timezone>
   <timezone>
     <tzid>Africa/Casablanca</tzid>
-    <dtstamp>2015-04-24T16:13:20Z</dtstamp>
+    <dtstamp>2015-07-01T14:44:04Z</dtstamp>
     <alias>Morocco Standard Time</alias>
-    <md5>5d06f7259ed2bf2162e69a5dffd2954b</md5>
+    <md5>bc9dfe82c24d4798a1c952fa92ed4746</md5>
   </timezone>
   <timezone>
     <tzid>Africa/Ceuta</tzid>
@@ -140,8 +140,8 @@
   </timezone>
   <timezone>
     <tzid>Africa/El_Aaiun</tzid>
-    <dtstamp>2015-04-24T16:13:20Z</dtstamp>
-    <md5>d73f03e09cb199fb0349281b9b2d349f</md5>
+    <dtstamp>2015-07-01T14:44:04Z</dtstamp>
+    <md5>2404746bd6c906d84ba245b48ba0ebcb</md5>
   </timezone>
   <timezone>
     <tzid>Africa/Freetown</tzid>
@@ -554,8 +554,8 @@
   </timezone>
   <timezone>
     <tzid>America/Cayman</tzid>
-    <dtstamp>2015-04-24T16:13:20Z</dtstamp>
-    <md5>8319a9f7d2931dbc39b4960360992fa3</md5>
+    <dtstamp>2015-07-01T14:44:04Z</dtstamp>
+    <md5>32249cf2917460aa068887267d979a5d</md5>
   </timezone>
   <timezone>
     <tzid>America/Chicago</tzid>
@@ -3112,8 +3112,8 @@
   </timezone>
   <timezone>
     <tzid>Morocco Standard Time</tzid>
-    <dtstamp>2015-04-24T16:13:20Z</dtstamp>
-    <md5>a10813c9508c9e96a26c67edbd803e4d</md5>
+    <dtstamp>2015-07-01T14:44:04Z</dtstamp>
+    <md5>1e27973c6723a745831c474ebedbf11a</md5>
   </timezone>
   <timezone>
     <tzid>Mountain Standard Time</tzid>

Modified: CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/version.txt
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/version.txt	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/twistedcaldav/zoneinfo/version.txt	2015-07-28 02:43:19 UTC (rev 15011)
@@ -1 +1 @@
-IANA Timezone Registry: 2015d
\ No newline at end of file
+IANA Timezone Registry: 2015e
\ No newline at end of file

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/base/datastore/subpostgres.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/base/datastore/subpostgres.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/base/datastore/subpostgres.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -35,7 +35,7 @@
 from twext.python.filepath import CachingFilePath
 
 from twisted.protocols.basic import LineReceiver
-from twisted.internet.defer import Deferred
+from twisted.internet.defer import Deferred, succeed
 from txdav.base.datastore.dbapiclient import DBAPIConnector
 from txdav.base.datastore.dbapiclient import postgres
 from txdav.common.icommondatastore import InternalDataStoreError
@@ -280,6 +280,15 @@
             )
 
         self._pgCtl = locateCommand("pg_ctl", pgCtl)
+
+        # Make note of the inode for the pg_ctl script; if it changes or is
+        # missing when it comes time to stop postgres, instead send SIGTERM
+        # to stop our postgres (since we can't do a graceful shutdown)
+        try:
+            self._pgCtlInode = os.stat(self._pgCtl).st_ino
+        except:
+            self._pgCtlInode = 0
+
         self._initdb = locateCommand("initdb", initDB)
         self._reactor = reactor
         self._postgresPid = None
@@ -657,30 +666,43 @@
             # If pg_ctl's startup wasn't successful, don't bother to stop the
             # database.  (This also happens in command-line tools.)
             if self.shouldStopDatabase:
-                monitor = PostgresMonitor()
-                args = [
-                    self._pgCtl, "stop",
-                    "--log={}".format(self.logFile),
-                ]
-                log.info("Requesting postgres stop via: {args}", args=args)
-                self.reactor.spawnProcess(
-                    monitor, self._pgCtl,
-                    args,
-                    env=self.env, path=self.workingDir.path,
-                    uid=self.uid, gid=self.gid,
-                )
-                return monitor.completionDeferred
+
+                # Compare pg_ctl inode with one we saw at the start; if different
+                # (or missing), fall back to SIGTERM
+                try:
+                    newInode = os.stat(self._pgCtl).st_ino
+                except OSError:
+                    # Missing
+                    newInode = -1
+
+                if self._pgCtlInode != newInode:
+                    # send SIGTERM to postgres
+                    log.info("Postgres control script mismatch")
+                    if self._postgresPid:
+                        log.info("Sending SIGTERM to Postgres")
+                        try:
+                            os.kill(self._postgresPid, signal.SIGTERM)
+                        except OSError:
+                            pass
+                    return succeed(None)
+                else:
+                    # use pg_ctl stop
+                    monitor = PostgresMonitor()
+                    args = [
+                        self._pgCtl, "stop",
+                        "--log={}".format(self.logFile),
+                    ]
+                    log.info("Requesting postgres stop via: {args}", args=args)
+                    self.reactor.spawnProcess(
+                        monitor, self._pgCtl,
+                        args,
+                        env=self.env, path=self.workingDir.path,
+                        uid=self.uid, gid=self.gid,
+                    )
+                    return monitor.completionDeferred
         return d.addCallback(superStopped)
 
-#        def maybeStopSubprocess(result):
-#            if self.monitor is not None:
-#                self.monitor.transport.signalProcess("INT")
-#                return self.monitor.completionDeferred
-#            return result
-#        d.addCallback(maybeStopSubprocess)
-#        return d
 
-
     def hardStop(self):
         """
         Stop postgres quickly by sending it SIGQUIT

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/icalsplitter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/icalsplitter.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/icalsplitter.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -139,6 +139,15 @@
             # where the Organizer event has L{willSplit} == C{True}
             rid = break_point if allow_past_the_end else None
 
+        if rid is not None:
+            # rid value type must match
+            dtstart = ical.mainComponent().propertyValue("DTSTART")
+            if dtstart.isDateOnly():
+                rid.setDateOnly(True)
+            elif dtstart.floating():
+                rid.setTimezoneID(None)
+
+
         return rid
 
 

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/inbound.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/inbound.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/inbound.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -27,7 +27,6 @@
 from twisted.internet import protocol, defer, ssl
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twisted.mail import pop3client, imap4
-from twisted.mail.smtp import messageid
 
 from twistedcaldav.config import config
 from twistedcaldav.ical import Property, Component
@@ -189,6 +188,30 @@
 
 
 
+def sanitizeCalendar(calendar):
+    """
+    Clean up specific issues seen in the wild from third party IMIP capable
+    servers.
+
+    @param calendar: the calendar Component to sanitize
+    @type calendar: L{Component}
+    """
+    # Don't let a missing PRODID prevent the reply from being processed
+    if not calendar.hasProperty("PRODID"):
+        calendar.addProperty(
+            Property(
+                "PRODID", "Unknown"
+            )
+        )
+
+    # For METHOD:REPLY we can remove STATUS properties
+    methodProperty = calendar.getProperty("METHOD")
+    if methodProperty is not None:
+        if methodProperty.value() == "REPLY":
+            calendar.removeAllPropertiesWithName("STATUS")
+
+
+
 class MailReceiver(object):
 
     NO_TOKEN = 0
@@ -392,7 +415,7 @@
             del msg["To"]
             msg["To"] = toAddr
             log.warn("Mail gateway forwarding reply back to organizer")
-            yield smtpSender.sendMessage(fromAddr, toAddr, messageid(), msg.as_string())
+            yield smtpSender.sendMessage(fromAddr, toAddr, SMTPSender.betterMessageID(), msg.as_string())
             returnValue(self.REPLY_FORWARDED_TO_ORGANIZER)
 
         # Process the imip attachment; inject to calendar server
@@ -401,13 +424,7 @@
         calendar = Component.fromString(calBody)
         event = calendar.mainComponent()
 
-        # Don't let a missing PRODID prevent the reply from being processed
-        if not calendar.hasProperty("PRODID"):
-            calendar.addProperty(
-                Property(
-                    "PRODID", "Unknown"
-                )
-            )
+        sanitizeCalendar(calendar)
 
         calendar.removeAllButOneAttendee(record.attendee)
         organizerProperty = calendar.getOrganizerProperty()
@@ -691,7 +708,7 @@
         self.log.debug("IMAP in cbGotMessage")
         try:
             messageData = results.values()[0]['RFC822']
-        except IndexError:
+        except (IndexError, KeyError):
             # results will be empty unless the "twistedmail-imap-flags-anywhere"
             # patch from http://twistedmatrix.com/trac/ticket/1105 is applied
             self.log.error("Skipping empty results -- apply twisted patch!")

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/outbound.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/outbound.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/outbound.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -32,7 +32,7 @@
 from twext.enterprise.jobqueue import WorkItem
 from twext.python.log import Logger
 from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.mail.smtp import messageid, rfc822date
+from twisted.mail.smtp import rfc822date
 from twisted.web.microdom import Text as DOMText, Element as DOMElement
 from twisted.web.microdom import parseString
 from twisted.web.template import XMLString, TEMPLATE_NAMESPACE, Element, renderer, flattenString, tags
@@ -453,6 +453,8 @@
             orgEmail = organizerMailto[7:]
 
             orgCN = calendar.getOrganizerProperty().parameterValue('CN', None)
+            if orgCN:
+                orgCN = orgCN.decode("utf-8")
             addressWithToken = formattedFrom
 
         # At the point we've created the token in the db, which we always
@@ -558,7 +560,7 @@
         msg["Reply-To"] = replyToAddress
         msg["To"] = toAddress
         msg["Date"] = rfc822date()
-        msgId = messageid()
+        msgId = SMTPSender.betterMessageID()
         msg["Message-ID"] = msgId
 
         msgAlt = MIMEMultipart("alternative")
@@ -663,8 +665,9 @@
         # template stuff, and once again, it's just a 'mailto:'.
         # tags.a(href="mailto:"+email)[cn]
         if orgEmail:
-            details['htmlOrganizer'] = tags.a(href="mailto:%s" % (orgEmail,))(
-                orgCN)
+            if not orgCN:
+                orgCN = orgEmail
+            details['htmlOrganizer'] = tags.a(href="mailto:%s" % (orgEmail,))(orgCN)
         else:
             details['htmlOrganizer'] = orgCN
 

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/smtpsender.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/smtpsender.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/smtpsender.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -24,7 +24,8 @@
 from twext.internet.gaiendpoint import GAIEndpoint
 from twext.python.log import Logger
 from twisted.internet import defer, ssl, reactor as _reactor
-from twisted.mail.smtp import ESMTPSenderFactory
+from twisted.mail.smtp import ESMTPSenderFactory, messageid
+from twistedcaldav.config import config
 
 log = Logger()
 
@@ -81,3 +82,15 @@
         deferred.addCallback(_success, msgId, fromAddr, toAddr)
         deferred.addErrback(_failure, msgId, fromAddr, toAddr)
         return deferred
+
+
+    @staticmethod
+    def betterMessageID():
+        """
+        Strip out the domain in the default Twisted Message-ID value and replace with our configured
+        server host name. That will avoid leaking internal app-server host names in a multi-host setup.
+
+        @return: our safe message-id value
+        @rtype: L{str}
+        """
+        return "{}@{}>".format(messageid().split("@")[0], config.ServerHostName)

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -29,6 +29,7 @@
 from txdav.caldav.datastore.scheduling.imip.inbound import injectMessage
 from txdav.caldav.datastore.scheduling.imip.inbound import shouldDeleteAllMail
 from txdav.caldav.datastore.scheduling.imip.inbound import IMAP4DownloadProtocol
+from txdav.caldav.datastore.scheduling.imip.inbound import sanitizeCalendar
 from txdav.common.datastore.test.util import CommonCommonTests
 
 from twext.enterprise.jobqueue import JobItem
@@ -457,7 +458,64 @@
         self.assertEquals(self.flagDeletedResult, "xyzzy")
 
 
+    @inlineCallbacks
+    def test_missingIMAPMessages(self):
+        """
+        Make sure L{IMAP4DownloadProtocol.cbGotMessage} can deal with missing messages.
+        """
 
+        class DummyResult(object):
+            def __init__(self):
+                self._values = []
+
+            def values(self):
+                return self._values
+
+        noResult = DummyResult()
+        missingKey = DummyResult()
+        missingKey.values().append({})
+
+        imap4 = IMAP4DownloadProtocol()
+        imap4.messageUIDs = []
+        imap4.fetchNextMessage = lambda : None
+
+        result = yield imap4.cbGotMessage(noResult, [])
+        self.assertTrue(result is None)
+        result = yield imap4.cbGotMessage(missingKey, [])
+        self.assertTrue(result is None)
+
+
+    def test_sanitizeCalendar(self):
+        """
+        Verify certain inbound third party mistakes are corrected.
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20130208T120000Z
+DTSTART:20180601T120000Z
+DTEND:20180601T130000Z
+ORGANIZER:urn:x-uid:user01
+ATTENDEE:mailto:xyzzy at example.com;PARTSTAT=ACCEPTED
+STATUS:ACCEPTED
+STATUS:ACCEPTED
+END:VEVENT
+END:VCALENDAR
+"""
+        calendar = Component.fromString(data)
+        self.assertFalse(calendar.hasProperty("PRODID"))
+        self.assertTrue(calendar.masterComponent().hasProperty("STATUS"))
+        sanitizeCalendar(calendar)
+        self.assertTrue(calendar.hasProperty("PRODID"))
+        self.assertFalse(calendar.masterComponent().hasProperty("STATUS"))
+
+
+
+
+
 class StubFactory(object):
 
     def __init__(self, actionTaken, deleteAllMail):

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/test/test_outbound.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/test/test_outbound.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/imip/test/test_outbound.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -60,7 +60,7 @@
 DESCRIPTION:awesome description with "<" and "&"
 END:VEVENT
 END:VCALENDAR
-"""
+""".encode("utf-8")
 
 inviteTextNoTimezone = u"""BEGIN:VCALENDAR
 VERSION:2.0
@@ -83,7 +83,7 @@
 DESCRIPTION:awesome description with "<" and "&"
 END:VEVENT
 END:VCALENDAR
-"""
+""".encode("utf-8")
 
 inviteTextWithTimezone = u"""BEGIN:VCALENDAR
 VERSION:2.0
@@ -249,7 +249,7 @@
 DESCRIPTION:awesome description with "<" and "&"
 END:VEVENT
 END:VCALENDAR
-"""
+""".encode("utf-8")
 
 ORGANIZER = "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A"
 ATTENDEE = "mailto:attendee at example.com"
@@ -400,63 +400,63 @@
 
             # Update
             (
-                """BEGIN:VCALENDAR
+                u"""BEGIN:VCALENDAR
 VERSION:2.0
 METHOD:REQUEST
 BEGIN:VEVENT
 UID:CFDD5E46-4F74-478A-9311-B3FF905449C3
 DTSTART:20100325T154500Z
 DTEND:20100325T164500Z
-ATTENDEE;CN=The Attendee;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:
+ATTENDEE;CN=Th\xe9 Attendee;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:
  mailto:attendee at example.com
-ATTENDEE;CN=The Organizer;CUTYPE=INDIVIDUAL;EMAIL=organizer at example.com;PAR
+ATTENDEE;CN=Th\xe9 Organizer;CUTYPE=INDIVIDUAL;EMAIL=organizer at example.com;PAR
  TSTAT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
-ORGANIZER;CN=The Organizer;EMAIL=organizer at example.com:urn:uuid:C3B38B00-41
+ORGANIZER;CN=Th\xe9 Organizer;EMAIL=organizer at example.com:urn:uuid:C3B38B00-41
  66-11DD-B22C-A07C87E02F6A
-SUMMARY:testing outbound( ) *update*
+SUMMARY:t\xe9sting outbound( ) *update*
 END:VEVENT
 END:VCALENDAR
-""",
+""".encode("utf-8"),
                 "CFDD5E46-4F74-478A-9311-B3FF905449C3",
                 "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
                 "mailto:attendee at example.com",
                 "update",
                 "organizer at example.com",
-                "The Organizer",
+                u"Th\xe9 Organizer",
                 [
-                    (u'The Attendee', u'attendee at example.com'),
-                    (u'The Organizer', u'organizer at example.com')
+                    (u'Th\xe9 Attendee', u'attendee at example.com'),
+                    (u'Th\xe9 Organizer', u'organizer at example.com')
                 ],
-                "The Organizer <organizer at example.com>",
-                "The Organizer <organizer at example.com>",
+                u"Th\xe9 Organizer <organizer at example.com>",
+                "=?utf-8?q?Th=C3=A9_Organizer_=3Corganizer=40example=2Ecom=3E?=",
                 "attendee at example.com",
             ),
 
             # Reply
             (
-                """BEGIN:VCALENDAR
+                u"""BEGIN:VCALENDAR
 VERSION:2.0
 METHOD:REPLY
 BEGIN:VEVENT
 UID:DFDD5E46-4F74-478A-9311-B3FF905449C4
 DTSTART:20100325T154500Z
 DTEND:20100325T164500Z
-ATTENDEE;CN=The Attendee;CUTYPE=INDIVIDUAL;EMAIL=attendee at example.com;PARTST
+ATTENDEE;CN=Th\xe9 Attendee;CUTYPE=INDIVIDUAL;EMAIL=attendee at example.com;PARTST
  AT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
-ORGANIZER;CN=The Organizer;EMAIL=organizer at example.com:mailto:organizer at exam
+ORGANIZER;CN=Th\xe9 Organizer;EMAIL=organizer at example.com:mailto:organizer at exam
  ple.com
-SUMMARY:testing outbound( ) *reply*
+SUMMARY:t\xe9sting outbound( ) *reply*
 END:VEVENT
 END:VCALENDAR
-""",
+""".encode("utf-8"),
                 None,
                 "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
                 "mailto:organizer at example.com",
                 "reply",
                 "organizer at example.com",
-                "The Organizer",
+                u"Th\xe9 Organizer",
                 [
-                    (u'The Attendee', u'attendee at example.com'),
+                    (u'Th\xe9 Attendee', u'attendee at example.com'),
                 ],
                 "attendee at example.com",
                 "attendee at example.com",
@@ -635,6 +635,59 @@
         self.assertEquals(actualTypes, expectedTypes)
 
 
+    def test_generateEmail_noOrganizerCN(self):
+        """
+        L{MailHandler.generateEmail} generates a MIME-formatted email when
+        the organizer property has no CN parameter.
+        """
+        calendar = Component.fromString(initialInviteText)
+        _ignore_msgID, msgTxt = self.sender.generateEmail(
+            inviteState='new',
+            calendar=calendar,
+            orgEmail=u"user01 at localhost",
+            orgCN=None,
+            attendees=[(u"Us\xe9r One", "user01 at localhost"),
+                       (u"User 2", "user02 at localhost")],
+            fromAddress="user01 at localhost",
+            replyToAddress="imip-system at localhost",
+            toAddress="user03 at localhost",
+        )
+        message = email.message_from_string(msgTxt)
+        self.assertTrue(message is not None)
+
+
+    def test_generateEmail_noAttendeeCN(self):
+        """
+        L{MailHandler.generateEmail} generates a MIME-formatted email when
+        the attendee property has no CN parameter.
+        """
+        calendar = Component.fromString(initialInviteText)
+        _ignore_msgID, msgTxt = self.sender.generateEmail(
+            inviteState='new',
+            calendar=calendar,
+            orgEmail=u"user01 at localhost",
+            orgCN=u"User Z\xe9ro One",
+            attendees=[(None, "user01 at localhost"),
+                       (None, "user02 at localhost")],
+            fromAddress="user01 at localhost",
+            replyToAddress="imip-system at localhost",
+            toAddress="user03 at localhost",
+        )
+        message = email.message_from_string(msgTxt)
+        self.assertTrue(message is not None)
+
+
+    def test_messageID(self):
+        """
+        L{SMTPSender.betterMessageID} generates a Message-ID domain matching
+        the L{config.ServerHostName} value.
+        """
+        self.patch(config, "ServerHostName", "calendar.example.com")
+        msgID, message = self.generateSampleEmail()
+        self.assertEquals(message['Message-ID'], msgID)
+        self.assertEqual(msgID[:-1].split("@")[1], config.ServerHostName)
+
+
     def test_alwaysIncludeTimezones(self):
         """
         L{MailHandler.generateEmail} generates a MIME-formatted email with a

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/processing.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/scheduling/processing.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -413,7 +413,7 @@
                 log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - split already done" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                 returnValue((True, False, False, None,))
             else:
-                self.message.removeProperty("X-CALENDARSERVER-SPLIT-OLDER-UID")
+                self.message.removeProperty("X-CALENDARSERVER-SPLIT-NEWER-UID")
                 self.message.removeProperty("X-CALENDARSERVER-SPLIT-RID")
 
         # Different based on method

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/sql.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/sql.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -82,7 +82,8 @@
     UnknownTimezone, SetComponentOptions
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, ECALENDARTYPE
-from txdav.common.datastore.sql_directory import GroupsRecord
+from txdav.common.datastore.sql_directory import GroupsRecord, \
+    GroupMembershipRecord
 from txdav.common.datastore.sql_tables import _ATTACHMENTS_MODE_NONE, \
     _ATTACHMENTS_MODE_READ, _ATTACHMENTS_MODE_WRITE, _BIND_MODE_DIRECT, \
     _BIND_MODE_GROUP, _BIND_MODE_GROUP_READ, _BIND_MODE_GROUP_WRITE, \
@@ -2127,40 +2128,61 @@
 
         # First check that the actual group membership has changed
         if (yield self.updateShareeGroupLink(groupUID)):
-            group = yield self._txn.groupByUID(groupUID)
-            memberUIDs = yield self._txn.groupMemberUIDs(group.groupID)
+
+            # First find all the members of all the groups being shared to
+            groupMembers = yield GroupMembershipRecord.query(
+                self._txn,
+                GroupMembershipRecord.groupID.In(
+                    GroupShareeRecord.queryExpr(
+                        expr=GroupShareeRecord.calendarID == self.id(),
+                        attributes=[GroupShareeRecord.groupID, ]
+                    )
+                ),
+                distinct=True,
+            )
+            memberUIDs = set([grp.memberUID for grp in groupMembers])
+
+            # Find each currently bound group sharee UID along with their bind mode
             boundUIDs = set()
-
             home = self._homeSchema
             bind = self._bindSchema
             rows = yield Select(
-                [home.OWNER_UID],
-                From=home,
-                Where=home.RESOURCE_ID.In(
-                    Select(
-                        [bind.HOME_RESOURCE_ID],
-                        From=bind,
-                        Where=(bind.CALENDAR_RESOURCE_ID == self._resourceID).And(
-                            (bind.BIND_MODE == _BIND_MODE_GROUP)
-                            .Or(bind.BIND_MODE == _BIND_MODE_GROUP_READ)
-                            .Or(bind.BIND_MODE == _BIND_MODE_GROUP_WRITE)
-                        )
-                    )
+                [home.OWNER_UID, bind.BIND_MODE],
+                From=bind.join(home, on=bind.HOME_RESOURCE_ID == home.RESOURCE_ID),
+                Where=(bind.CALENDAR_RESOURCE_ID == self._resourceID).And(
+                    bind.BIND_MODE.In((_BIND_MODE_GROUP, _BIND_MODE_GROUP_READ, _BIND_MODE_GROUP_WRITE))
                 )
             ).on(self._txn)
-            for [shareeHomeUID] in rows:
+            for shareeHomeUID, shareeBindMode in rows:
+
                 if shareeHomeUID in memberUIDs:
+                    # Group sharee still referenced via a group - make a note of it
                     boundUIDs.add(shareeHomeUID)
-                else:
+
+                elif shareeBindMode == _BIND_MODE_GROUP:
+                    # Group only sharee is no longer referenced by any group - uninvite them
                     yield self.uninviteUIDFromShare(shareeHomeUID)
                     changed = True
 
+                else:
+                    # Group+individual sharee is no longer referenced by a group so update the bind
+                    # mode to reflect just the individual mode
+                    yield super(Calendar, self).inviteUIDToShare(
+                        shareeHomeUID,
+                        {
+                            _BIND_MODE_GROUP_READ: _BIND_MODE_READ,
+                            _BIND_MODE_GROUP_WRITE: _BIND_MODE_WRITE,
+                        }.get(shareeBindMode),
+                    )
+
             for memberUID in memberUIDs - boundUIDs:
-                shareeView = yield self.shareeView(memberUID)
-                newMode = _BIND_MODE_GROUP if shareeView is None else shareeView._groupModeAfterAddingOneGroupSharee()
-                if newMode is not None:
-                    yield super(Calendar, self).inviteUIDToShare(memberUID, newMode)
-                    changed = True
+                # Never reconcile the sharer
+                if memberUID != self._home.uid():
+                    shareeView = yield self.shareeView(memberUID)
+                    newMode = _BIND_MODE_GROUP if shareeView is None else shareeView._groupModeAfterAddingOneGroupSharee()
+                    if newMode is not None:
+                        yield super(Calendar, self).inviteUIDToShare(memberUID, newMode)
+                        changed = True
 
         returnValue(changed)
 
@@ -2502,6 +2524,24 @@
 
 
     @inlineCallbacks
+    def ownerDeleteShare(self):
+        """
+        This share is being deleted (by the owner) - we also need to clean up the group sharees.
+        """
+
+        yield super(Calendar, self).ownerDeleteShare()
+
+        # Delete referenced group sharees. Note that whilst the table uses an on delete cascade,
+        # we do need to remove the sharees for the case where the calendar is trashed and not
+        # removed. Since the cascade is not triggered in that case and we have to do it by hand.
+        gs = schema.GROUP_SHAREE
+        yield Delete(
+            From=gs,
+            Where=(gs.CALENDAR_ID == self._resourceID),
+        ).on(self._txn)
+
+
+    @inlineCallbacks
     def allInvitations(self):
         """
         Get list of all invitations (non-direct) to this object.
@@ -2568,11 +2608,8 @@
 }
 accesstype_to_accessMode = dict([(v, k) for k, v in accessMode_to_type.items()])
 
-def _pathToName(path):
-    return path.rsplit(".", 1)[0]
 
 
-
 class CalendarObject(CommonObjectResource, CalendarObjectBase):
     implements(ICalendarObject)
 
@@ -2592,7 +2629,7 @@
         self.scheduleTag = options.get("scheduleTag", "")
         self.scheduleEtags = options.get("scheduleEtags", "")
         self.hasPrivateComment = options.get("hasPrivateComment", False)
-        self._dropboxID = None
+        self._dropboxID = options.get("dropboxID", None)
 
         # Component caching
         self._cachedComponent = None
@@ -2694,7 +2731,9 @@
 
         # Possible timezone stripping
         if config.EnableTimezonesByReference:
-            component.stripStandardTimezones()
+            changed = component.stripStandardTimezones()
+            if changed:
+                self._componentChanged = True
 
         # Do validation on external requests
         if internal_state == ComponentUpdateState.NORMAL:
@@ -2747,6 +2786,7 @@
             if attendeeProp.parameterValue("CUTYPE") == "X-SERVER-GROUP"
         ])
 
+        # Map each group attendee to a list of potential member properties
         groupCUAToAttendeeMemberPropMap = {}
         for groupCUA in groupCUAs:
 
@@ -2805,6 +2845,8 @@
             else:
                 groupUID = uidFromCalendarUserAddress(groupCUA)
             group = yield self._txn.groupByUID(groupUID)
+            if group is None:
+                continue
 
             if group.groupID in groupIDToMembershipHashMap:
                 if groupIDToMembershipHashMap[group.groupID].membershipHash != group.membershipHash:
@@ -3087,6 +3129,8 @@
         comment (which will trigger scheduling with the organizer to remove the comment on the organizer's
         side).
         """
+        changed = False
+
         if config.Scheduling.CalDAV.get("EnablePrivateComments", True):
             old_has_private_comments = not inserting and self.hasPrivateComment
             new_has_private_comments = component.hasPropertyInAnyComponent((
@@ -3100,6 +3144,7 @@
                 component.transferProperties(old_calendar, (
                     "X-CALENDARSERVER-ATTENDEE-COMMENT",
                 ))
+                changed = True
 
             self.hasPrivateComment = new_has_private_comments
 
@@ -3110,7 +3155,9 @@
             if component.hasDuplicatePrivateComments(doFix=config.RemoveDuplicatePrivateComments) and internal_state == ComponentUpdateState.NORMAL:
                 raise DuplicatePrivateCommentsError("Duplicate X-CALENDARSERVER-ATTENDEE-COMMENT properties present.")
 
+        returnValue(changed)
 
+
     @inlineCallbacks
     def replaceMissingToDoProperties(self, calendar, inserting, internal_state):
         """
@@ -3135,7 +3182,10 @@
 
                 # Get the originator who is the owner of the calendar resource being modified
                 originatorPrincipal = yield self.calendar().ownerHome().directoryRecord()
-                originatorAddresses = originatorPrincipal.calendarUserAddresses
+                if originatorPrincipal is not None:
+                    originatorAddresses = originatorPrincipal.calendarUserAddresses
+                else:
+                    originatorAddresses = ("urn:x-uid:{}".format(self.calendar().ownerHome().uid),)
 
                 for component in calendar.subcomponents():
                     if component.name() != "VTODO":
@@ -3172,7 +3222,10 @@
 
                 # Get the originator who is the owner of the calendar resource being modified
                 originatorPrincipal = yield self.calendar().ownerHome().directoryRecord()
-                originatorAddresses = originatorPrincipal.calendarUserAddresses
+                if originatorPrincipal is not None:
+                    originatorAddresses = originatorPrincipal.calendarUserAddresses
+                else:
+                    originatorAddresses = ("urn:x-uid:{}".format(self.calendar().ownerHome().uid),)
 
                 for component in calendar.subcomponents():
                     if component.name() != "VTODO":
@@ -3296,14 +3349,39 @@
         Scan the component for ROOM attendees; if any are associated with an
         address record which has street address and geo coordinates, add an
         X-APPLE-STRUCTURED-LOCATION property and update the LOCATION property
-        to contain the name and street address.
+        to contain the name and street address.  X-APPLE-STRUCTURED-LOCATION
+        with X-CUADDR but no corresponding ATTENDEE are removed.
         """
+        dir = self.directoryService()
 
+        changed = False
         cache = {}
-        dir = self.directoryService()
+
         for sub in component.subcomponents():
-            locations = []
-            removed = False
+            existingLocationProps = list(sub.properties("LOCATION"))
+            if len(existingLocationProps) == 0:
+                existingLocationValue = ""
+            else:
+                existingLocationValue = existingLocationProps[0].value()
+            existingLocations = []
+            for value in existingLocationValue.split(";"):
+                if value:
+                    existingLocations.append(value.strip())
+
+            # index the structured locations on X-CUADDR and X-TITLE
+            allStructured = {
+                "cua": {},
+                "title": {}
+            }
+            for structured in sub.properties("X-APPLE-STRUCTURED-LOCATION"):
+                cuAddr = structured.parameterValue("X-CUADDR")
+                if cuAddr:
+                    allStructured["cua"][cuAddr] = structured
+                else:
+                    title = structured.parameterValue("X-TITLE")
+                    if title:
+                        allStructured["title"][title] = structured
+
             for attendee in sub.getAllAttendeeProperties():
                 if attendee.parameterValue("CUTYPE") == "ROOM":
                     value = attendee.value()
@@ -3326,34 +3404,96 @@
                     # Use the cached data if present
                     entry = cache[value]
                     if entry is not None:
+
                         street, geo, title = entry
+                        newLocationValue = "{0}\n{1}".format(title, street.encode("utf-8"))
+
+                        # Is there already a structured location property for
+                        # this attendee?  If so, we'll update it.
+                        # Unfortunately, we can only depend on X-CUADDR
+                        # going forward, but there is going to be old existing
+                        # X-APPLE-STRUCTURED-LOCATIONs that haven't yet had
+                        # those added.  So let's first look up by X-CUADDR and
+                        # then by X-TITLE.
+                        if value in allStructured["cua"]:
+                            structured = allStructured["cua"][value]
+                        elif title in allStructured["title"]:
+                            structured = allStructured["title"][title]
+                        else:
+                            structured = None
+
                         params = {
                             "X-ADDRESS": street,
                             "X-APPLE-RADIUS": "71",
                             "X-TITLE": title,
+                            "X-CUADDR": value,
                         }
-                        structured = Property(
-                            "X-APPLE-STRUCTURED-LOCATION",
-                            geo.encode("utf-8"), params=params,
-                            valuetype=Value.VALUETYPE_URI
-                        )
 
-                        # The first time we have any X- prop, remove all existing ones
-                        if not removed:
-                            sub.removeProperty("X-APPLE-STRUCTURED-LOCATION")
-                            removed = True
-                        sub.addProperty(structured)
-                        locations.append("{0}\n{1}".format(title, street.encode("utf-8")))
+                        if structured is None:
+                            # Create a new one
+                            prevTitle = attendee.parameterValue("CN")
+                            structured = Property(
+                                "X-APPLE-STRUCTURED-LOCATION",
+                                geo.encode("utf-8"), params=params,
+                                valuetype=Value.VALUETYPE_URI
+                            )
+                            changed = True
+                            sub.addProperty(structured)
+                        else:
+                            # Update existing one
+                            prevGeo = structured.value()
+                            if geo != prevGeo:
+                                structured.setValue(geo)
+                                changed = True
+                            prevTitle = structured.parameterValue("X-TITLE")
+                            for paramName, paramValue in params.iteritems():
+                                prevValue = structured.parameterValue(paramName)
+                                if paramValue != prevValue:
+                                    structured.setParameter(paramName, paramValue)
+                                    changed = True
 
-            # Update the LOCATION if X-'s were added
-            if locations:
+                        if changed:
+                            # Replace old location values with the new ones
+                            for i in xrange(len(existingLocations)):
+                                existingLocation = existingLocations[i]
+                                if (
+                                    prevTitle is not None and
+                                    (
+                                        # it's either an exact match or matches
+                                        # up to the newline which precedes the
+                                        # street address
+                                        existingLocation == prevTitle or
+                                        existingLocation.startswith("{}\n".format(prevTitle))
+                                    )
+                                ):
+                                    existingLocations[i] = newLocationValue
+                                    break
+                            else:
+                                existingLocations.append(newLocationValue)
+
+            # Remove any server-generated structured locations without an ATTENDEE
+            for structured in sub.properties("X-APPLE-STRUCTURED-LOCATION"):
+                cuAddr = structured.parameterValue("X-CUADDR")
+                if cuAddr is not None: # therefore it's one that requires an ATTENDEE...
+                    attendeeProp = sub.getAttendeeProperty((cuAddr,))
+                    if attendeeProp is None: # ...remove it if no matching ATTENDEE
+                        sub.removeProperty(structured)
+                        changed = True
+
+            # Update the LOCATION
+            newLocationValue = ";".join(existingLocations)
+            if newLocationValue != existingLocationValue:
                 newLocProperty = Property(
                     "LOCATION",
-                    "; ".join(locations)
+                    newLocationValue
                 )
                 sub.replaceProperty(newLocProperty)
+                changed = True
 
+        if changed:
+            self._componentChanged = True
 
+
     @inlineCallbacks
     def decorateHostedStatus(self, component):
         """
@@ -3649,9 +3789,13 @@
             yield self._lockAndCheckUID(component, inserting, internal_state)
 
             # Preserve private comments
-            yield self.preservePrivateComments(component, inserting, internal_state)
+            changed = yield self.preservePrivateComments(component, inserting, internal_state)
+            if changed:
+                self._componentChanged = True
 
             # Fix broken VTODOs
+            # Note: if this does recover lost organizer/attendee properties, the
+            # implicit code below will set _componentChanged
             yield self.replaceMissingToDoProperties(component, inserting, internal_state)
 
             # Handle sharing dropbox normalization
@@ -5071,6 +5215,19 @@
         if organizer is not None and organizerAddress.record.uid != self.calendar().ownerHome().uid():
             raise InvalidSplit("Only organizers can split events.")
 
+        # rid value type must match
+        dtstart = component.mainComponent().propertyValue("DTSTART")
+        if dtstart.isDateOnly():
+            if not rid.isDateOnly():
+                # We ought to reject this but for now we will fix it
+                rid.setDateOnly(True)
+        elif dtstart.floating():
+            if not rid.floating() or rid.isDateOnly():
+                raise InvalidSplit("rid parameter value type must match DTSTART value type.")
+        else:
+            if rid.floating():
+                raise InvalidSplit("rid parameter value type must match DTSTART value type.")
+
         # Determine valid split point
         splitter = iCalSplitter(1024, 14)
         rid = splitter.whereSplit(component, break_point=rid, allow_past_the_end=False)
@@ -5159,6 +5316,7 @@
             olderResourceName,
             calendar_old,
             ComponentUpdateState.SPLIT_OWNER,
+            options={"dropboxID": olderUID},
             split_details=(rid, newerUID, False, False)
         )
 
@@ -5217,7 +5375,12 @@
 
         # Create a new resource and store its data (but not if the parent is "inbox", or if it is empty)
         if not self.calendar().isInbox() and ical_old.mainType() is not None:
-            olderObject = yield self.calendar()._createCalendarObjectWithNameInternal("{0}.ics".format(olderUID,), ical_old, ComponentUpdateState.SPLIT_ATTENDEE)
+            olderObject = yield self.calendar()._createCalendarObjectWithNameInternal(
+                "{0}.ics".format(olderUID,),
+                ical_old,
+                ComponentUpdateState.SPLIT_ATTENDEE,
+                options={"dropboxID": olderUID},
+            )
 
             # Reconcile trash state
             if self.isInTrash():

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/sql_external.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/sql_external.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -255,15 +255,15 @@
     @inlineCallbacks
     def addAttachment(self, rids, content_type, filename, stream):
         result = yield self._txn.store().conduit.send_add_attachment(self, rids, content_type, filename, stream)
-        managedID, location = result
-        returnValue((ManagedAttachmentExternal(str(managedID)), str(location),))
+        managedID, size, location = result
+        returnValue((ManagedAttachmentExternal(str(managedID), size), str(location),))
 
 
     @inlineCallbacks
     def updateAttachment(self, managed_id, content_type, filename, stream):
         result = yield self._txn.store().conduit.send_update_attachment(self, managed_id, content_type, filename, stream)
-        managedID, location = result
-        returnValue((ManagedAttachmentExternal(str(managedID)), str(location),))
+        managedID, size, location = result
+        returnValue((ManagedAttachmentExternal(str(managedID), size), str(location),))
 
 
     @inlineCallbacks
@@ -279,12 +279,17 @@
     L{CalendarObjectExternal.updateAttachment}.
     """
 
-    def __init__(self, managedID):
+    def __init__(self, managedID, size):
         self._managedID = managedID
+        self._size = size
 
 
     def managedID(self):
         return self._managedID
 
 
+    def size(self):
+        return self._size
+
+
 CalendarExternal._objectResourceClass = CalendarObjectExternal

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/calendar_store/ho/me/home_bad/calendar_bad/1.ics
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/calendar_store/ho/me/home_bad/calendar_bad/1.ics	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/calendar_store/ho/me/home_bad/calendar_bad/1.ics	2015-07-28 02:43:19 UTC (rev 15011)
@@ -20,11 +20,11 @@
 END:STANDARD
 END:VTIMEZONE
 BEGIN:VEVENT
-DTEND;TZID=US/Pacific:20000324T124500
+DTEND;TZID=US/Pacific:%(now-1)s0324T124500
 UID:uid1
 DTSTAMP:20090326T145447Z
 SUMMARY:Busted
-DTSTART;TZID=US/Pacific:20000324T121500
+DTSTART;TZID=US/Pacific:%(now-1)s0324T121500
 CREATED:20090326T145440Z
 RRULE:FREQ=HOURLY
 END:VEVENT

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_attachments.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_attachments.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_attachments.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -1689,7 +1689,7 @@
         for attach in attachments:
             if attach.hasParameter("MANAGED-ID"):
                 managed_count += 1
-                self.assertTrue(attach.value().find("/dropbox/") == -1)
+                self.assertTrue(attach.value().find("/dropbox/") != -1)
                 self.assertTrue(attach.parameterValue("FILENAME") in filenames)
             else:
                 dropbox_count += 1
@@ -1869,7 +1869,7 @@
         for attach in attachments:
             if attach.hasParameter("MANAGED-ID"):
                 managed_count += 1
-                self.assertTrue(attach.value().find("1.2.dropbox") == -1)
+                self.assertTrue(attach.value().find("1.2.dropbox") != -1)
                 self.assertEqual(attach.parameterValue("MANAGED-ID"), mnew.managedID())
                 self.assertEqual(attach.parameterValue("FILENAME"), mnew.name())
             else:
@@ -1907,7 +1907,7 @@
         for attach in attachments:
             if attach.hasParameter("MANAGED-ID"):
                 managed_count += 1
-                self.assertTrue(attach.value().find("1.2.dropbox") == -1)
+                self.assertTrue(attach.value().find("1.2.dropbox") != -1)
                 self.assertTrue(attach.parameterValue("FILENAME") in ("attach_1_2_1.txt", "attach_1_2_2.txt"))
             else:
                 dropbox_count += 1

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_file.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_file.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -66,14 +66,19 @@
     storePath.copyTo(calendarPath)
 
     # Set year values to current year
+    subs = {}
     nowYear = DateTime.getToday().getYear()
+    subs["now"] = nowYear
+    for i in range(1, 10):
+        subs["now-{}".format(i)] = nowYear - 1
+        subs["now+{}".format(i)] = nowYear + 1
     for home in calendarPath.child("ho").child("me").children():
         if not home.basename().startswith("."):
             for calendar in home.children():
                 if not calendar.basename().startswith("."):
                     for resource in calendar.children():
                         if resource.basename().endswith(".ics"):
-                            resource.setContent(resource.getContent() % {"now": nowYear})
+                            resource.setContent(resource.getContent() % subs)
 
     testID = test.id()
     test.counter = 0

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_queue_scheduling.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_queue_scheduling.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_queue_scheduling.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -19,11 +19,10 @@
 from twext.python.clsprop import classproperty
 from twistedcaldav.config import config
 from txdav.caldav.datastore.scheduling.work import ScheduleWorkMixin
-from txdav.caldav.datastore.test.util import CommonStoreTests
-from txdav.common.datastore.test.util import componentUpdate
-from twistedcaldav.ical import normalize_iCalStr
+from txdav.caldav.datastore.test.util import CommonStoreTests, \
+    DateTimeSubstitutionsMixin
 
-class BaseQueueSchedulingTests(CommonStoreTests):
+class BaseQueueSchedulingTests(CommonStoreTests, DateTimeSubstitutionsMixin):
 
     """
     Test store-based calendar sharing.
@@ -41,7 +40,9 @@
         self.patch(config.Scheduling.Options.WorkQueues, "AttendeeRefreshBatchDelaySeconds", 1)
         self.patch(config.Scheduling.Options.WorkQueues, "AttendeeRefreshBatchIntervalSeconds", 1)
 
+        self.setupDateTimeValues()
 
+
     @classproperty(cache=False)
     def requirements(cls): #@NoSelf
         return {
@@ -87,7 +88,7 @@
         self.assertEqual(len(objs), 1)
 
         caldata = yield objs[0].componentForUser()
-        self.assertEqual(normalize_iCalStr(caldata), normalize_iCalStr(componentUpdate(data)))
+        self.assertEqualCalendarData(caldata, data.format(**self.dtsubs))
 
 
 
@@ -104,7 +105,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
-DTSTART:{now}T000000Z
+DTSTART:{nowDate}T000000Z
 DURATION:PT1H
 ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
 ATTENDEE:mailto:user02 at example.com
@@ -120,7 +121,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
-DTSTART:{now}T000000Z
+DTSTART:{nowDate}T000000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:user02
@@ -137,7 +138,7 @@
 METHOD:REQUEST
 BEGIN:VEVENT
 UID:12345-67890
-DTSTART:{now}T000000Z
+DTSTART:{nowDate}T000000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
@@ -153,7 +154,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
-DTSTART:{now}T000000Z
+DTSTART:{nowDate}T000000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
@@ -170,7 +171,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
-DTSTART:{now}T000000Z
+DTSTART:{nowDate}T000000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user02
@@ -186,7 +187,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
-DTSTART:{now}T000000Z
+DTSTART:{nowDate}T000000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:urn:x-uid:user02
@@ -203,7 +204,7 @@
 METHOD:REPLY
 BEGIN:VEVENT
 UID:12345-67890
-DTSTART:{now}T000000Z
+DTSTART:{nowDate}T000000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user02
 DTSTAMP:20051222T210507Z
@@ -219,7 +220,7 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:12345-67890
-DTSTART:{now}T000000Z
+DTSTART:{nowDate}T000000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user02
@@ -232,7 +233,7 @@
 
         waitForWork = ScheduleWorkMixin.allDone()
         calendar = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield calendar.createCalendarObjectWithName("data1.ics", componentUpdate(data1))
+        yield calendar.createCalendarObjectWithName("data1.ics", data1.format(**self.dtsubs))
         yield self.commit()
 
         yield waitForWork
@@ -244,7 +245,7 @@
 
         waitForWork = ScheduleWorkMixin.allDone()
         cobj = yield self._getOneResource("user02", "calendar")
-        yield cobj.setComponent(componentUpdate(data5))
+        yield cobj.setComponent(data5.format(**self.dtsubs))
         yield self.commit()
 
         yield waitForWork

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_sql.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/test_sql.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -21,6 +21,7 @@
 
 from pycalendar.datetime import DateTime
 from pycalendar.timezone import Timezone
+from pycalendar.value import Value
 
 
 from txweb2 import responsecode
@@ -38,7 +39,7 @@
 from twistedcaldav.caldavxml import CalendarDescription
 from twistedcaldav.stdconfig import config
 from twistedcaldav.dateops import datetimeMktime
-from twistedcaldav.ical import Component, normalize_iCalStr, diff_iCalStrs
+from twistedcaldav.ical import Component, normalize_iCalStr, diff_iCalStrs, Property
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 from twistedcaldav.timezones import TimezoneCache, readVTZ, TimezoneException
 
@@ -55,12 +56,13 @@
 from txdav.common.datastore.sql import ECALENDARTYPE, CommonObjectResource, \
     CommonStoreTransactionMonitor
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
-    _BIND_STATUS_ACCEPTED, _TRANSP_OPAQUE
+    _BIND_STATUS_ACCEPTED, _TRANSP_OPAQUE, _BIND_MODE_WRITE
 from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests, \
-    test_event_text
+    test_event_text, cal1Root, OTHER_HOME_UID
 from txdav.caldav.datastore.test.test_file import setUpCalendarStore
+from txdav.caldav.datastore.test.util import DateTimeSubstitutionsMixin
 from txdav.common.datastore.test.util import populateCalendarsFrom, \
-    CommonCommonTests
+    CommonCommonTests, updateToCurrentYear
 from txdav.caldav.datastore.util import _migrateCalendar, migrateHome
 from txdav.caldav.icalendarstore import ComponentUpdateState, InvalidDefaultCalendar, \
     InvalidSplit, UnknownTimezone
@@ -1912,117 +1914,6 @@
 
 
     @inlineCallbacks
-    def test_calendarRevisionChangeConcurrency(self):
-        """
-        Test that two concurrent attempts to add resources in two separate
-        calendar homes does not deadlock on the revision table update.
-        """
-
-        calendarStore = self._sqlCalendarStore
-
-        # Make sure homes are provisioned
-        txn = self.transactionUnderTest()
-        home_uid1 = yield txn.homeWithUID(ECALENDARTYPE, "user01", create=True)
-        home_uid2 = yield txn.homeWithUID(ECALENDARTYPE, "user02", create=True)
-        self.assertNotEqual(home_uid1, None)
-        self.assertNotEqual(home_uid2, None)
-        yield self.commit()
-
-        # Create first events in different calendar homes
-        txn1 = calendarStore.newTransaction()
-        txn2 = calendarStore.newTransaction()
-
-        calendar_uid1_in_txn1 = yield self.calendarUnderTest(txn1, "calendar", "user01")
-        calendar_uid2_in_txn2 = yield self.calendarUnderTest(txn2, "calendar", "user02")
-
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:data%(ctr)s
-DTSTART:20130102T140000Z
-DURATION:PT1H
-CREATED:20060102T190000Z
-DTSTAMP:20051222T210507Z
-SUMMARY:data%(ctr)s
-END:VEVENT
-END:VCALENDAR
-"""
-
-        component = Component.fromString(data % {"ctr": 1})
-        yield calendar_uid1_in_txn1.createCalendarObjectWithName("data1.ics", component)
-
-        component = Component.fromString(data % {"ctr": 2})
-        yield calendar_uid2_in_txn2.createCalendarObjectWithName("data2.ics", component)
-
-        # Setup deferreds to run concurrently and create second events in the calendar homes
-        # previously used by the other transaction - this could create the deadlock.
-        @inlineCallbacks
-        def _defer_uid3():
-            calendar_uid1_in_txn2 = yield self.calendarUnderTest(txn2, "calendar", "user01")
-            component = Component.fromString(data % {"ctr": 3})
-            yield calendar_uid1_in_txn2.createCalendarObjectWithName("data3.ics", component)
-            yield txn2.commit()
-        d1 = _defer_uid3()
-
-        @inlineCallbacks
-        def _defer_uid4():
-            calendar_uid2_in_txn1 = yield self.calendarUnderTest(txn1, "calendar", "user02")
-            component = Component.fromString(data % {"ctr": 4})
-            yield calendar_uid2_in_txn1.createCalendarObjectWithName("data4.ics", component)
-            yield txn1.commit()
-        d2 = _defer_uid4()
-
-        # Now do the concurrent provision attempt
-        yield DeferredList([d1, d2])
-
-        # Verify we did not have a deadlock and all resources have been created.
-        caldata1 = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
-        caldata2 = yield self.calendarObjectUnderTest(name="data2.ics", calendar_name="calendar", home="user02")
-        caldata3 = yield self.calendarObjectUnderTest(name="data3.ics", calendar_name="calendar", home="user01")
-        caldata4 = yield self.calendarObjectUnderTest(name="data4.ics", calendar_name="calendar", home="user02")
-        self.assertNotEqual(caldata1, None)
-        self.assertNotEqual(caldata2, None)
-        self.assertNotEqual(caldata3, None)
-        self.assertNotEqual(caldata4, None)
-
-
-    @inlineCallbacks
-    def test_calendarMissingRevision(self):
-        """
-        Test that two concurrent attempts to add resources in two separate
-        calendar homes does not deadlock on the revision table update.
-        """
-
-        # Get details
-        home = yield self.homeUnderTest(name="user01", create=True)
-        self.assertNotEqual(home, None)
-        calendar = yield home.childWithName("calendar")
-        self.assertNotEqual(calendar, None)
-
-        rev = calendar._revisionsSchema
-        yield Delete(
-            From=rev,
-            Where=(
-                rev.HOME_RESOURCE_ID == Parameter("homeID")).And(
-                rev.COLLECTION_NAME == Parameter("collectionName")
-            )
-        ).on(self.transactionUnderTest(), homeID=home.id(), collectionName="calendar")
-
-        yield self.commit()
-
-        home = yield self.homeUnderTest(name="user01")
-        children = yield home.loadChildren()
-        self.assertEqual(len(children), 3)
-        yield self.commit()
-
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
-        token = yield calendar.syncToken()
-        self.assertTrue(token is not None)
-
-
-    @inlineCallbacks
     def test_inboxTransp(self):
         """
         Make sure inbox is always transparent no matter what is stored in the DB.
@@ -2319,6 +2210,228 @@
 
 
     @inlineCallbacks
+    def test_sharedTasksMissingSharer(self):
+        """
+        Make sure that a sharee can store tasks when the sharer has been removed from
+        the directory.
+        """
+        home = yield self.homeUnderTest()
+        cal = yield home.createCalendarWithName("shared_tasks")
+        yield cal.setSupportedComponents("VTODO")
+        yield self.commit()
+
+        cal = yield self.calendarUnderTest(name="shared_tasks")
+        other = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        newCalName = yield cal.shareWith(other, _BIND_MODE_WRITE)
+        self.sharedName = newCalName
+        yield self.commit()
+
+        cal = yield self.calendarUnderTest(name="shared_tasks")
+        yield cal.createCalendarObjectWithName("data1.ics", Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTODO
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+SUMMARY:original
+END:VTODO
+END:VCALENDAR
+"""))
+        yield self.commit()
+
+        yield self._sqlCalendarStore.directoryService().removeRecords(((yield self.userUIDFromShortName("home1")),))
+
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name=self.sharedName, home=OTHER_HOME_UID)
+        yield cobj.setComponent(Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTODO
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+COMPLETED:20080601T130000Z
+SUMMARY:changed
+END:VTODO
+END:VCALENDAR
+"""))
+        yield self.commit()
+
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name=self.sharedName, home=OTHER_HOME_UID)
+        comp = yield cobj.componentForUser()
+        self.assertEqual(normalize_iCalStr(comp), normalize_iCalStr("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTODO
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+COMPLETED:20080601T130000Z
+SUMMARY:changed
+END:VTODO
+END:VCALENDAR
+"""))
+        yield self.commit()
+
+
+
+class SyncTests(CommonCommonTests, unittest.TestCase):
+    """
+    Revision table/sync report tests.
+    """
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(SyncTests, self).setUp()
+        yield self.buildStoreAndDirectory()
+        yield self.populate()
+
+
+    requirements = {
+        "user01": {
+            "calendar": {
+                "1.ics": (cal1Root.child("1.ics").getContent(), CalendarCommonTests.metadata1),
+                "2.ics": (cal1Root.child("2.ics").getContent(), CalendarCommonTests.metadata2),
+                "3.ics": (cal1Root.child("3.ics").getContent(), CalendarCommonTests.metadata3),
+                "4.ics": (cal1Root.child("4.ics").getContent(), CalendarCommonTests.metadata4),
+                "5.ics": (cal1Root.child("5.ics").getContent(), CalendarCommonTests.metadata5),
+            },
+        },
+    }
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    def token2revision(self, token):
+        """
+        FIXME: the API names for L{syncToken}() and L{resourceNamesSinceToken}()
+        are slightly inaccurate; one doesn't produce input for the other.
+        Actually it should be resource names since I{revision} and you need to
+        understand the structure of the tokens to extract the revision.  Right
+        now that logic lives in the protocol layer, so this testing method
+        replicates it.
+        """
+        _ignore_uuid, rev = token.split("_", 1)
+        rev = int(rev)
+        return rev
+
+
+    @inlineCallbacks
+    def test_calendarRevisionChangeConcurrency(self):
+        """
+        Test that two concurrent attempts to add resources in two separate
+        calendar homes does not deadlock on the revision table update.
+        """
+
+        calendarStore = self._sqlCalendarStore
+
+        # Make sure homes are provisioned
+        txn = self.transactionUnderTest()
+        home_uid1 = yield txn.homeWithUID(ECALENDARTYPE, "user01", create=True)
+        home_uid2 = yield txn.homeWithUID(ECALENDARTYPE, "user02", create=True)
+        self.assertNotEqual(home_uid1, None)
+        self.assertNotEqual(home_uid2, None)
+        yield self.commit()
+
+        # Create first events in different calendar homes
+        txn1 = calendarStore.newTransaction()
+        txn2 = calendarStore.newTransaction()
+
+        calendar_uid1_in_txn1 = yield self.calendarUnderTest(txn1, "calendar", "user01")
+        calendar_uid2_in_txn2 = yield self.calendarUnderTest(txn2, "calendar", "user02")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:data%(ctr)s
+DTSTART:20130102T140000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+SUMMARY:data%(ctr)s
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % {"ctr": 1})
+        yield calendar_uid1_in_txn1.createCalendarObjectWithName("data1.ics", component)
+
+        component = Component.fromString(data % {"ctr": 2})
+        yield calendar_uid2_in_txn2.createCalendarObjectWithName("data2.ics", component)
+
+        # Setup deferreds to run concurrently and create second events in the calendar homes
+        # previously used by the other transaction - this could create the deadlock.
+        @inlineCallbacks
+        def _defer_uid3():
+            calendar_uid1_in_txn2 = yield self.calendarUnderTest(txn2, "calendar", "user01")
+            component = Component.fromString(data % {"ctr": 3})
+            yield calendar_uid1_in_txn2.createCalendarObjectWithName("data3.ics", component)
+            yield txn2.commit()
+        d1 = _defer_uid3()
+
+        @inlineCallbacks
+        def _defer_uid4():
+            calendar_uid2_in_txn1 = yield self.calendarUnderTest(txn1, "calendar", "user02")
+            component = Component.fromString(data % {"ctr": 4})
+            yield calendar_uid2_in_txn1.createCalendarObjectWithName("data4.ics", component)
+            yield txn1.commit()
+        d2 = _defer_uid4()
+
+        # Now do the concurrent provision attempt
+        yield DeferredList([d1, d2])
+
+        # Verify we did not have a deadlock and all resources have been created.
+        caldata1 = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        caldata2 = yield self.calendarObjectUnderTest(name="data2.ics", calendar_name="calendar", home="user02")
+        caldata3 = yield self.calendarObjectUnderTest(name="data3.ics", calendar_name="calendar", home="user01")
+        caldata4 = yield self.calendarObjectUnderTest(name="data4.ics", calendar_name="calendar", home="user02")
+        self.assertNotEqual(caldata1, None)
+        self.assertNotEqual(caldata2, None)
+        self.assertNotEqual(caldata3, None)
+        self.assertNotEqual(caldata4, None)
+
+
+    @inlineCallbacks
+    def test_calendarMissingRevision(self):
+        """
+        Test that two concurrent attempts to add resources in two separate
+        calendar homes does not deadlock on the revision table update.
+        """
+
+        # Get details
+        home = yield self.homeUnderTest(name="user02", create=True)
+        self.assertNotEqual(home, None)
+        calendar = yield home.childWithName("calendar")
+        self.assertNotEqual(calendar, None)
+
+        rev = calendar._revisionsSchema
+        yield Delete(
+            From=rev,
+            Where=(
+                rev.HOME_RESOURCE_ID == Parameter("homeID")).And(
+                rev.COLLECTION_NAME == Parameter("collectionName")
+            )
+        ).on(self.transactionUnderTest(), homeID=home.id(), collectionName="calendar")
+
+        yield self.commit()
+
+        home = yield self.homeUnderTest(name="user02")
+        children = yield home.loadChildren()
+        self.assertEqual(len(children), 3)
+        yield self.commit()
+
+        calendar = yield self.calendarUnderTest(home="user02", name="calendar")
+        token = yield calendar.syncToken()
+        self.assertTrue(token is not None)
+
+
+    @inlineCallbacks
     def test_removeAfterRevisionCleanup(self):
         """
         Make sure L{Calendar}'s can be renamed after revision cleanup
@@ -2349,6 +2462,58 @@
 
 
     @inlineCallbacks
+    def test_revisionModified(self):
+        """
+        Make sure the revision table MODIFIED value changes for an update or delete
+        """
+
+        @inlineCallbacks
+        def _getModified():
+            home = yield self.homeUnderTest(name="user01")
+            calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+            rev = calendar._revisionsSchema
+            modified = yield Select(
+                [rev.MODIFIED, ],
+                From=rev,
+                Where=(
+                    rev.HOME_RESOURCE_ID == Parameter("homeID")).And(
+                    rev.CALENDAR_RESOURCE_ID == Parameter("collectionID")).And(
+                    rev.RESOURCE_NAME == Parameter("resourceName")
+                )
+            ).on(
+                home._txn,
+                homeID=home.id(),
+                collectionID=calendar.id(),
+                resourceName="1.ics",
+            )
+            yield self.commit()
+            returnValue(modified[0][0])
+
+        # Get current modified
+        old_modified = yield _getModified()
+        self.assertNotEqual(old_modified, None)
+
+        # Update resource
+        cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield cobj.setComponent(Component.fromString(updateToCurrentYear(cal1Root.child("1.ics").getContent())))
+        yield self.commit()
+
+        # Modified changed
+        update_modified = yield _getModified()
+        self.assertGreater(update_modified, old_modified)
+
+        # Delete resource
+        cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield cobj.remove()
+        yield self.commit()
+
+        # Modified changed
+        delete_modified = yield _getModified()
+        self.assertGreater(delete_modified, old_modified)
+        self.assertGreater(delete_modified, update_modified)
+
+
+    @inlineCallbacks
     def test_homeSyncTokenWithTrash_Visible(self):
         """
         L{ICalendarHome.resourceNamesSinceToken} will return the names of
@@ -2359,8 +2524,8 @@
         self.patch(config, "EnableTrashCollection", True)
         self.patch(config, "ExposeTrashCollection", True)
 
-        home = yield self.homeUnderTest()
-        cal = yield self.calendarUnderTest()
+        home = yield self.homeUnderTest(name="user01")
+        cal = yield self.calendarUnderTest(home="user01", name="calendar")
         st = yield home.syncToken()
         yield cal.createCalendarObjectWithName("new.ics", Component.fromString(
             test_event_text
@@ -2372,12 +2537,12 @@
         st2 = yield home.syncToken()
         self.failIfEquals(st, st2)
 
-        home = yield self.homeUnderTest()
+        home = yield self.homeUnderTest(name="user01")
 
         expected = [
-            "calendar_1/",
-            "calendar_1/new.ics",
-            "calendar_1/2.ics",
+            "calendar/",
+            "calendar/new.ics",
+            "calendar/2.ics",
             "other-calendar/"
         ]
 
@@ -2393,7 +2558,7 @@
             self.token2revision(st), "infinity")
 
         self.assertEquals(set(changed), set(expected))
-        self.assertEquals(set(deleted), set(["calendar_1/2.ics"]))
+        self.assertEquals(set(deleted), set(["calendar/2.ics"]))
         self.assertEquals(invalid, [])
 
         changed, deleted, invalid = yield home.resourceNamesSinceToken(
@@ -2413,8 +2578,8 @@
 
         self.patch(config, "EnableTrashCollection", True)
 
-        home = yield self.homeUnderTest()
-        cal = yield self.calendarUnderTest()
+        home = yield self.homeUnderTest(name="user01")
+        cal = yield self.calendarUnderTest(home="user01", name="calendar")
         st = yield home.syncToken()
         yield cal.createCalendarObjectWithName("new.ics", Component.fromString(
             test_event_text
@@ -2426,12 +2591,12 @@
         st2 = yield home.syncToken()
         self.failIfEquals(st, st2)
 
-        home = yield self.homeUnderTest()
+        home = yield self.homeUnderTest(name="user01")
 
         expected = [
-            "calendar_1/",
-            "calendar_1/new.ics",
-            "calendar_1/2.ics",
+            "calendar/",
+            "calendar/new.ics",
+            "calendar/2.ics",
             "other-calendar/"
         ]
 
@@ -2439,7 +2604,7 @@
             self.token2revision(st), "infinity")
 
         self.assertEquals(set(changed), set(expected))
-        self.assertEquals(set(deleted), set(["calendar_1/2.ics"]))
+        self.assertEquals(set(deleted), set(["calendar/2.ics"]))
         self.assertEquals(invalid, [])
 
         changed, deleted, invalid = yield home.resourceNamesSinceToken(
@@ -2450,7 +2615,7 @@
 
 
 
-class SchedulingTests(CommonCommonTests, unittest.TestCase):
+class SchedulingTests(CommonCommonTests, DateTimeSubstitutionsMixin, unittest.TestCase):
     """
     CalendarObject splitting tests
     """
@@ -2459,6 +2624,7 @@
     def setUp(self):
         yield super(SchedulingTests, self).setUp()
         yield self.buildStoreAndDirectory()
+        self.setupDateTimeValues()
 
         # Make sure homes are provisioned
         txn = self.transactionUnderTest()
@@ -2682,7 +2848,7 @@
             calendar_name="calendar",
             home="user01"
         )
-        comp = yield cobj.component()
+        comp = yield cobj.componentForUser()
         components = list(comp.subcomponents())
 
         # Check first component
@@ -2773,14 +2939,14 @@
             calendar_name="calendar",
             home="user01"
         )
-        comp = yield cobj.component()
+        comp = yield cobj.componentForUser()
         components = list(comp.subcomponents())
 
         # Check first component
         locProp = components[0].getProperty("LOCATION")
         self.assertEquals(
             locProp.value(),
-            "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014; Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
+            "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014;Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
         )
         structProps = tuple(components[0].properties("X-APPLE-STRUCTURED-LOCATION"))
         self.assertEqual(len(structProps), 2)
@@ -2817,7 +2983,7 @@
         locProp = components[0].getProperty("LOCATION")
         self.assertEquals(
             locProp.value(),
-            "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014; Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
+            "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014;Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
         )
         structProps = tuple(components[0].properties("X-APPLE-STRUCTURED-LOCATION"))
         self.assertEqual(len(structProps), 2)
@@ -2830,6 +2996,429 @@
 
 
     @inlineCallbacks
+    def test_setComponent_structuredLocation_Mixed(self):
+        """
+        Verify adding a location that's not in the directory to an event which
+        already has a location that's in the directory keeps them both.
+        X-APPLE-STRUCTURED-LOCATION properties which have X-CUADDR but no
+        corresponding ATTENDEE are removed.
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN
+BEGIN:VEVENT
+UID:561F5DBB-3F38-4B3A-986F-DD05CBAF554F
+DTSTART:20131211T164500Z
+DURATION:PT1H
+ATTENDEE;CN=Old Room with Address 1;CUTYPE=ROOM;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPAN
+ T;SCHEDULE-STATUS=2.0:urn:x-uid:room-addr-1
+ATTENDEE;CN=Room with Address 2;CUTYPE=ROOM;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPAN
+ T;SCHEDULE-STATUS=2.0:urn:x-uid:room-addr-2
+ATTENDEE;CN=Mercury Seven;CUTYPE=ROOM;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPAN
+ T;SCHEDULE-STATUS=2.0:urn:x-uid:mercury
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01 at example.com;PARTSTAT=AC
+ CEPTED:urn:x-uid:user01
+X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS="1 Infinite Loop, Cupertin
+ o, CA 95014";X-APPLE-RADIUS=71;X-CUADDR="urn:x-uid:room-addr-1";X-TITLE=O
+ ld Room with Address 1:geo:37.331741,-122.030333
+X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS="2 Infinite Loop, Cupertin
+ o, CA 95014";X-APPLE-RADIUS=71;X-TITLE=Room with Address 2:geo:37.332633,
+ -122.030502
+X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS=123 Main St;X-APPLE-RADIUS
+ =14164;X-TITLE=Mercury Seven:geo:37.351164,-122.032686
+CREATED:20131211T221854Z
+DTSTAMP:20131211T230632Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+SEQUENCE:1
+SUMMARY:locations
+LOCATION:Old Room with Address 1;Unstructured Location; Mercury Seven; Room with Address 2
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+        yield calendar.createCalendarObjectWithName(
+            "structured.ics",
+            Component.fromString(data)
+        )
+
+        yield self.commit()
+
+        cobj = yield self.calendarObjectUnderTest(
+            name="structured.ics",
+            calendar_name="calendar",
+            home="user01"
+        )
+        comp = yield cobj.componentForUser()
+        components = list(comp.subcomponents())
+
+        # Check first component -- LOCATION now has the street addresses, and
+        # location values that don't have an ATTENDEE or X-APPLE-STRUCTURED-LOCATIONs
+        # are retained
+        locProp = components[0].getProperty("LOCATION")
+        self.assertEquals(
+            locProp.value(),
+            "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014;Unstructured Location;Mercury Seven;Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
+        )
+        structProps = tuple(components[0].properties("X-APPLE-STRUCTURED-LOCATION"))
+        self.assertEqual(len(structProps), 3)
+        self.assertEquals(
+            structProps[0].value(),
+            "geo:37.331741,-122.030333",
+        )
+        # Make sure server has also added X-CUADDR
+        self.assertEquals(
+            structProps[0].parameterValue("X-CUADDR"),
+            "urn:x-uid:room-addr-1"
+        )
+
+        # Client now adds a location not in the directory:
+        comp = comp.duplicate()
+        main = comp.mainComponent()
+        main.replaceProperty(Property("LOCATION", "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014; Unstructured Location; Falafel Stop\n1325 Sunnyvale Saratoga, Sunnyvale, CA 94087;Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"))
+
+        params = {
+            "X-ADDRESS": "1325 Sunnyvale Saratoga Rd",
+            "X-APPLE-RADIUS": "14164",
+            "X-TITLE": "Falafel Stop",
+        }
+        structured = Property(
+            "X-APPLE-STRUCTURED-LOCATION",
+            "geo:37.351164,-122.032686", params=params,
+            valuetype=Value.VALUETYPE_URI
+        )
+        main.addProperty(structured)
+
+        # ...plus let's prove we clean up structured locations which have X-CUADDR
+        # but no matching ATTENDEE
+        params = {
+            "X-ADDRESS": "1122 Boogie Woogie Ave",
+            "X-APPLE-RADIUS": "14164",
+            "X-TITLE": "Home of the Boogie, House of the Funk",
+            "X-CUADDR": "urn:x-uid:boogie-home",
+        }
+        structured = Property(
+            "X-APPLE-STRUCTURED-LOCATION",
+            "geo:37.351164,-122.032686", params=params,
+            valuetype=Value.VALUETYPE_URI
+        )
+        main.addProperty(structured)
+
+        # Store the new component and let the server do its thing
+        yield cobj.setComponent(comp)
+        yield self.commit()
+
+        cobj = yield self.calendarObjectUnderTest(
+            name="structured.ics",
+            calendar_name="calendar",
+            home="user01"
+        )
+        comp = yield cobj.componentForUser()
+        components = list(comp.subcomponents())
+
+        # Check first component
+        structProps = tuple(components[0].properties("X-APPLE-STRUCTURED-LOCATION"))
+        self.assertEqual(len(structProps), 4)
+        self.assertEquals(
+            set([structProp.parameterValue("X-TITLE") for structProp in structProps]),
+            set(("Room with Address 1", "Room with Address 2", "Falafel Stop", "Mercury Seven"))
+        )
+
+        locProp = components[0].getProperty("LOCATION")
+        self.assertEquals(
+            locProp.value(),
+            "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014;Unstructured Location;Falafel Stop\n1325 Sunnyvale Saratoga, Sunnyvale, CA 94087;Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
+        )
+
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_setComponent_structuredLocation_MissingValue(self):
+        """
+        Verify we detect a change to X-APPLE-STRUCTURED-LOCATION and update it.
+        (This also works if the client neglects to provide a value)
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN
+BEGIN:VEVENT
+UID:561F5DBB-3F38-4B3A-986F-DD05CBAF554F
+DTSTART:20131211T164500Z
+DURATION:PT1H
+ATTENDEE;CN=Room with Address 1;CUTYPE=ROOM;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPAN
+ T;SCHEDULE-STATUS=2.0:urn:x-uid:room-addr-1
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01 at example.com;PARTSTAT=AC
+ CEPTED:urn:x-uid:user01
+X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-TITLE=Room with Address 1:
+CREATED:20131211T221854Z
+DTSTAMP:20131211T230632Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+SEQUENCE:1
+SUMMARY:locations
+LOCATION:Room with Address 1
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+        yield calendar.createCalendarObjectWithName(
+            "structured.ics",
+            Component.fromString(data)
+        )
+
+        yield self.commit()
+
+        cobj = yield self.calendarObjectUnderTest(
+            name="structured.ics",
+            calendar_name="calendar",
+            home="user01"
+        )
+        comp = yield cobj.componentForUser()
+        components = list(comp.subcomponents())
+
+        # Check first component -- LOCATION now has the street addresses, and
+        # location values that don't have an ATTENDEE or X-APPLE-STRUCTURED-LOCATIONs
+        # are retained
+        locProp = components[0].getProperty("LOCATION")
+        self.assertEquals(
+            locProp.value(),
+            "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014"
+        )
+        structProps = tuple(components[0].properties("X-APPLE-STRUCTURED-LOCATION"))
+        self.assertEqual(len(structProps), 1)
+        self.assertEquals(
+            structProps[0].value(),
+            "geo:37.331741,-122.030333",
+        )
+        # Make sure server has also added X-CUADDR
+        self.assertEquals(
+            structProps[0].parameterValue("X-CUADDR"),
+            "urn:x-uid:room-addr-1"
+        )
+
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_setComponent_changed_stripStandardTimezones(self):
+        """
+        Verify we let the client know we stripped standard timezones
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN
+BEGIN:VEVENT
+UID:561F5DBB-3F38-4B3A-986F-DD05CBAF554F
+DTSTART:20131211T164500Z
+DURATION:PT1H
+CREATED:20131211T221854Z
+DTSTAMP:20131211T230632Z
+SEQUENCE:1
+SUMMARY:testing
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        tzdata = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0800
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+DTSTART:20070311T020000
+TZNAME:PDT
+TZOFFSETTO:-0700
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:-0700
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+DTSTART:20071104T020000
+TZNAME:PST
+TZOFFSETTO:-0800
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        TimezoneCache.create()
+        self.addCleanup(TimezoneCache.clear)
+
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+        yield calendar.createCalendarObjectWithName(
+            "structured.ics",
+            Component.fromString(data)
+        )
+
+        yield self.commit()
+
+        cobj = yield self.calendarObjectUnderTest(
+            name="structured.ics",
+            calendar_name="calendar",
+            home="user01"
+        )
+        comp = yield cobj.componentForUser()
+        comp = comp.duplicate()
+
+        # create timezone component
+        tzCalendar = Component.fromString(tzdata)
+        tzComponent = list(tzCalendar.subcomponents())[0]
+        comp.addComponent(tzComponent)
+
+        # update DTSTART to reference that tz
+        main = comp.mainComponent()
+        newStart = Property(
+            "DTSTART",
+            DateTime(2015, 7, 7, 5, 0, 0, tzid=Timezone(tzid="US/Pacific"))
+        )
+        main.replaceProperty(newStart)
+        yield cobj.setComponent(comp)
+        comp = yield cobj.componentForUser()
+        self.assertTrue(cobj._componentChanged)
+
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_setComponent_changed_preservePrivateComments(self):
+        """
+        Verify we let the client know we preserved private comments
+        """
+
+        dataWith = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN
+BEGIN:VEVENT
+UID:561F5DBB-3F38-4B3A-986F-DD05CBAF554F
+DTSTART:%(now_fwd30)s
+DURATION:PT1H
+CREATED:%(now)s
+DTSTAMP:%(now)s
+SEQUENCE:1
+SUMMARY:testing
+TRANSP:OPAQUE
+X-CALENDARSERVER-ATTENDEE-COMMENT;X-CALENDARSERVER-ATTENDEE-REF="urn:uuid:user01";X-CALENDARSERVER-DTSTAMP=%(now)s:Message1
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        dataWithout = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN
+BEGIN:VEVENT
+UID:561F5DBB-3F38-4B3A-986F-DD05CBAF554F
+DTSTART:%(now_fwd30)s
+DURATION:PT1H
+CREATED:%(now)s
+DTSTAMP:%(now)s
+SEQUENCE:1
+SUMMARY:testing
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+        yield calendar.createCalendarObjectWithName(
+            "comments.ics",
+            Component.fromString(dataWith % self.dtsubs)
+        )
+
+        yield self.commit()
+
+        cobj = yield self.calendarObjectUnderTest(
+            name="comments.ics",
+            calendar_name="calendar",
+            home="user01"
+        )
+
+        comp = Component.fromString(dataWithout % self.dtsubs)
+        yield cobj.setComponent(comp)
+        comp = yield cobj.componentForUser()
+        self.assertTrue(cobj._componentChanged)
+
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_setComponent_changed_dropboxPathNormalization(self):
+        """
+        Verify we let the client know we normalized dropbox paths
+        """
+
+        dataWithout = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN
+BEGIN:VEVENT
+UID:561F5DBB-3F38-4B3A-986F-DD05CBAF554F
+DTSTART:%(now_fwd30)s
+DURATION:PT1H
+DTSTAMP:%(now)s
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        dataWith = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN
+BEGIN:VEVENT
+UID:561F5DBB-3F38-4B3A-986F-DD05CBAF554F
+DTSTART:%(now_fwd30)s
+DURATION:PT1H
+DTSTAMP:%(now)s
+SEQUENCE:2
+X-APPLE-DROPBOX:https://example.com/calendars/users/user02/dropbox/123.dropbox
+ATTACH;VALUE=URI:https://example.com/calendars/users/user02/dropbox/123.dropbox/1.txt
+ATTACH;VALUE=URI:https://example.org/attachments/2.txt
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        calendarCollection = (yield self.calendarUnderTest(name="calendar", home="user01"))
+        shareeHome = (yield self.homeUnderTest(name="user02"))
+        sharedName = (yield calendarCollection.shareWith(shareeHome, _BIND_MODE_WRITE,))
+        yield self.commit()
+
+        calendar = yield self.calendarUnderTest(name=sharedName, home="user02")
+        yield calendar.createCalendarObjectWithName(
+            "attach.ics",
+            Component.fromString(dataWithout % self.dtsubs)
+        )
+
+        yield self.commit()
+
+        cobj = yield self.calendarObjectUnderTest(
+            name="attach.ics",
+            calendar_name=sharedName,
+            home="user02"
+        )
+
+        comp = Component.fromString(dataWith % self.dtsubs)
+        yield cobj.setComponent(comp)
+        comp = yield cobj.componentForUser()
+        self.assertTrue(cobj._componentChanged)
+
+        yield self.commit()
+
+
+    @inlineCallbacks
     def test_setComponent_externalPrincipal(self):
         """
         Verify attendees who are not locally hosted have X-APPLE-HOSTED-STATUS=EXTERNAL
@@ -2922,7 +3511,7 @@
 
 
 
-class CalendarObjectSplitting(CommonCommonTests, unittest.TestCase):
+class CalendarObjectSplitting(CommonCommonTests, DateTimeSubstitutionsMixin, unittest.TestCase):
     """
     CalendarObject splitting tests
     """
@@ -2939,40 +3528,8 @@
             self.assertNotEqual(home_uid, None)
         yield self.commit()
 
-        self.subs = {}
+        self.setupDateTimeValues()
 
-        self.now = DateTime.getNowUTC()
-        self.now.setHHMMSS(0, 0, 0)
-
-        self.subs["now"] = self.now
-
-        for i in range(30):
-            attrname = "now_back%s" % (i + 1,)
-            setattr(self, attrname, self.now.duplicate())
-            getattr(self, attrname).offsetDay(-(i + 1))
-            self.subs[attrname] = getattr(self, attrname)
-
-            attrname_12h = "now_back%s_12h" % (i + 1,)
-            setattr(self, attrname_12h, getattr(self, attrname).duplicate())
-            getattr(self, attrname_12h).offsetHours(12)
-            self.subs[attrname_12h] = getattr(self, attrname_12h)
-
-            attrname_1 = "now_back%s_1" % (i + 1,)
-            setattr(self, attrname_1, getattr(self, attrname).duplicate())
-            getattr(self, attrname_1).offsetSeconds(-1)
-            self.subs[attrname_1] = getattr(self, attrname_1)
-
-        for i in range(30):
-            attrname = "now_fwd%s" % (i + 1,)
-            setattr(self, attrname, self.now.duplicate())
-            getattr(self, attrname).offsetDay(i + 1)
-            self.subs[attrname] = getattr(self, attrname)
-
-            attrname_12h = "now_fwd%s_12h" % (i + 1,)
-            setattr(self, attrname_12h, getattr(self, attrname).duplicate())
-            getattr(self, attrname_12h).offsetHours(12)
-            self.subs[attrname_12h] = getattr(self, attrname_12h)
-
         self.patch(config, "MaxAllowedInstances", 500)
 
 
@@ -3181,7 +3738,7 @@
 END:VCALENDAR
 """
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -3205,7 +3762,7 @@
         ical_future, ical_past, pastUID, relID, _ignore_new_name = yield self._splitDetails("user01")
 
         title = "temp"
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["uid"] = pastUID
         relsubs["relID"] = relID
         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
@@ -3213,6 +3770,428 @@
 
 
     @inlineCallbacks
+    def test_calendarObjectSplit_AllDay(self):
+        """
+        Test that (manual) splitting of all-day calendar objects works.
+        """
+
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", False)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+
+        # Create one event that will split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;VALUE=DATE:%(nowDate_back30)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+ATTENDEE:mailto:user3 at example.org
+ATTENDEE:mailto:user4 at example.org
+ATTENDEE:mailto:user5 at example.org
+ATTENDEE:mailto:user6 at example.org
+ATTENDEE:mailto:user7 at example.org
+ATTENDEE:mailto:user8 at example.org
+ATTENDEE:mailto:user9 at example.org
+ATTENDEE:mailto:user10 at example.org
+ATTENDEE:mailto:user11 at example.org
+ATTENDEE:mailto:user12 at example.org
+ATTENDEE:mailto:user13 at example.org
+ATTENDEE:mailto:user14 at example.org
+ATTENDEE:mailto:user15 at example.org
+ATTENDEE:mailto:user16 at example.org
+ATTENDEE:mailto:user17 at example.org
+ATTENDEE:mailto:user18 at example.org
+ATTENDEE:mailto:user19 at example.org
+ATTENDEE:mailto:user20 at example.org
+ATTENDEE:mailto:user21 at example.org
+ATTENDEE:mailto:user22 at example.org
+ATTENDEE:mailto:user23 at example.org
+ATTENDEE:mailto:user24 at example.org
+ATTENDEE:mailto:user25 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user1 at example.org
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID;VALUE=DATE:%(nowDate_back25)s
+DTSTART;VALUE=DATE:%(nowDate_back25)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user1 at example.org
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID;VALUE=DATE:%(nowDate_back24)s
+DTSTART;VALUE=DATE:%(nowDate_back24)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user1 at example.org
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;VALUE=DATE:%(nowDate_back14)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+ATTENDEE:mailto:user3 at example.org
+ATTENDEE:mailto:user4 at example.org
+ATTENDEE:mailto:user5 at example.org
+ATTENDEE:mailto:user6 at example.org
+ATTENDEE:mailto:user7 at example.org
+ATTENDEE:mailto:user8 at example.org
+ATTENDEE:mailto:user9 at example.org
+ATTENDEE:mailto:user10 at example.org
+ATTENDEE:mailto:user11 at example.org
+ATTENDEE:mailto:user12 at example.org
+ATTENDEE:mailto:user13 at example.org
+ATTENDEE:mailto:user14 at example.org
+ATTENDEE:mailto:user15 at example.org
+ATTENDEE:mailto:user16 at example.org
+ATTENDEE:mailto:user17 at example.org
+ATTENDEE:mailto:user18 at example.org
+ATTENDEE:mailto:user19 at example.org
+ATTENDEE:mailto:user20 at example.org
+ATTENDEE:mailto:user21 at example.org
+ATTENDEE:mailto:user22 at example.org
+ATTENDEE:mailto:user23 at example.org
+ATTENDEE:mailto:user24 at example.org
+ATTENDEE:mailto:user25 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SEQUENCE:1
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_past = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(uid)s
+DTSTART;VALUE=DATE:%(nowDate_back30)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+ATTENDEE:mailto:user3 at example.org
+ATTENDEE:mailto:user4 at example.org
+ATTENDEE:mailto:user5 at example.org
+ATTENDEE:mailto:user6 at example.org
+ATTENDEE:mailto:user7 at example.org
+ATTENDEE:mailto:user8 at example.org
+ATTENDEE:mailto:user9 at example.org
+ATTENDEE:mailto:user10 at example.org
+ATTENDEE:mailto:user11 at example.org
+ATTENDEE:mailto:user12 at example.org
+ATTENDEE:mailto:user13 at example.org
+ATTENDEE:mailto:user14 at example.org
+ATTENDEE:mailto:user15 at example.org
+ATTENDEE:mailto:user16 at example.org
+ATTENDEE:mailto:user17 at example.org
+ATTENDEE:mailto:user18 at example.org
+ATTENDEE:mailto:user19 at example.org
+ATTENDEE:mailto:user20 at example.org
+ATTENDEE:mailto:user21 at example.org
+ATTENDEE:mailto:user22 at example.org
+ATTENDEE:mailto:user23 at example.org
+ATTENDEE:mailto:user24 at example.org
+ATTENDEE:mailto:user25 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(nowDate_back15)s
+SEQUENCE:1
+TRANSP:OPAQUE
+END:VEVENT
+BEGIN:VEVENT
+UID:%(uid)s
+RECURRENCE-ID;VALUE=DATE:%(nowDate_back25)s
+DTSTART;VALUE=DATE:%(nowDate_back25)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+TRANSP:OPAQUE
+END:VEVENT
+BEGIN:VEVENT
+UID:%(uid)s
+RECURRENCE-ID;VALUE=DATE:%(nowDate_back24)s
+DTSTART;VALUE=DATE:%(nowDate_back24)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.dtsubs)
+        cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        # Do manual split
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        will = yield cobj.willSplit()
+        self.assertTrue(will)
+
+        yield cobj.split()
+        yield self.commit()
+
+        ical_future, ical_past, pastUID, relID, _ignore_new_name = yield self._splitDetails("user01")
+
+        title = "temp"
+        relsubs = dict(self.dtsubs)
+        relsubs["uid"] = pastUID
+        relsubs["relID"] = relID
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s %s" % (title, diff_iCalStrs(ical_future, data_future % relsubs)))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % relsubs, "Failed past: %s %s" % (title, diff_iCalStrs(ical_past, data_past % relsubs)))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_Floating(self):
+        """
+        Test that (manual) splitting of floating calendar objects works.
+        """
+
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", False)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+
+        # Create one event that will split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(nowFloating_back30)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+ATTENDEE:mailto:user3 at example.org
+ATTENDEE:mailto:user4 at example.org
+ATTENDEE:mailto:user5 at example.org
+ATTENDEE:mailto:user6 at example.org
+ATTENDEE:mailto:user7 at example.org
+ATTENDEE:mailto:user8 at example.org
+ATTENDEE:mailto:user9 at example.org
+ATTENDEE:mailto:user10 at example.org
+ATTENDEE:mailto:user11 at example.org
+ATTENDEE:mailto:user12 at example.org
+ATTENDEE:mailto:user13 at example.org
+ATTENDEE:mailto:user14 at example.org
+ATTENDEE:mailto:user15 at example.org
+ATTENDEE:mailto:user16 at example.org
+ATTENDEE:mailto:user17 at example.org
+ATTENDEE:mailto:user18 at example.org
+ATTENDEE:mailto:user19 at example.org
+ATTENDEE:mailto:user20 at example.org
+ATTENDEE:mailto:user21 at example.org
+ATTENDEE:mailto:user22 at example.org
+ATTENDEE:mailto:user23 at example.org
+ATTENDEE:mailto:user24 at example.org
+ATTENDEE:mailto:user25 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user1 at example.org
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(nowFloating_back25)s
+DTSTART:%(nowFloating_back25)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user1 at example.org
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(nowFloating_back24)s
+DTSTART:%(nowFloating_back24)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user1 at example.org
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(nowFloating_back14)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+ATTENDEE:mailto:user3 at example.org
+ATTENDEE:mailto:user4 at example.org
+ATTENDEE:mailto:user5 at example.org
+ATTENDEE:mailto:user6 at example.org
+ATTENDEE:mailto:user7 at example.org
+ATTENDEE:mailto:user8 at example.org
+ATTENDEE:mailto:user9 at example.org
+ATTENDEE:mailto:user10 at example.org
+ATTENDEE:mailto:user11 at example.org
+ATTENDEE:mailto:user12 at example.org
+ATTENDEE:mailto:user13 at example.org
+ATTENDEE:mailto:user14 at example.org
+ATTENDEE:mailto:user15 at example.org
+ATTENDEE:mailto:user16 at example.org
+ATTENDEE:mailto:user17 at example.org
+ATTENDEE:mailto:user18 at example.org
+ATTENDEE:mailto:user19 at example.org
+ATTENDEE:mailto:user20 at example.org
+ATTENDEE:mailto:user21 at example.org
+ATTENDEE:mailto:user22 at example.org
+ATTENDEE:mailto:user23 at example.org
+ATTENDEE:mailto:user24 at example.org
+ATTENDEE:mailto:user25 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_past = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(uid)s
+DTSTART:%(nowFloating_back30)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+ATTENDEE:mailto:user3 at example.org
+ATTENDEE:mailto:user4 at example.org
+ATTENDEE:mailto:user5 at example.org
+ATTENDEE:mailto:user6 at example.org
+ATTENDEE:mailto:user7 at example.org
+ATTENDEE:mailto:user8 at example.org
+ATTENDEE:mailto:user9 at example.org
+ATTENDEE:mailto:user10 at example.org
+ATTENDEE:mailto:user11 at example.org
+ATTENDEE:mailto:user12 at example.org
+ATTENDEE:mailto:user13 at example.org
+ATTENDEE:mailto:user14 at example.org
+ATTENDEE:mailto:user15 at example.org
+ATTENDEE:mailto:user16 at example.org
+ATTENDEE:mailto:user17 at example.org
+ATTENDEE:mailto:user18 at example.org
+ATTENDEE:mailto:user19 at example.org
+ATTENDEE:mailto:user20 at example.org
+ATTENDEE:mailto:user21 at example.org
+ATTENDEE:mailto:user22 at example.org
+ATTENDEE:mailto:user23 at example.org
+ATTENDEE:mailto:user24 at example.org
+ATTENDEE:mailto:user25 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(nowFloating_back14_1)s
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:%(uid)s
+RECURRENCE-ID:%(nowFloating_back25)s
+DTSTART:%(nowFloating_back25)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:%(uid)s
+RECURRENCE-ID:%(nowFloating_back24)s
+DTSTART:%(nowFloating_back24)s
+DURATION:P1D
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.dtsubs)
+        cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        # Do manual split
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        will = yield cobj.willSplit()
+        self.assertTrue(will)
+
+        yield cobj.split()
+        yield self.commit()
+
+        ical_future, ical_past, pastUID, relID, _ignore_new_name = yield self._splitDetails("user01")
+
+        title = "temp"
+        relsubs = dict(self.dtsubs)
+        relsubs["uid"] = pastUID
+        relsubs["relID"] = relID
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s %s" % (title, diff_iCalStrs(ical_future, data_future % relsubs)))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % relsubs, "Failed past: %s %s" % (title, diff_iCalStrs(ical_past, data_past % relsubs)))
+
+
+    @inlineCallbacks
     def test_calendarObjectSplit_work(self):
         """
         Test that splitting of calendar objects works.
@@ -3631,7 +4610,7 @@
 END:VCALENDAR
 """
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
         self.assertTrue(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -3660,7 +4639,7 @@
 
         # Verify user01 data
         title = "user01"
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["uid"] = pastUID
         relsubs["relID"] = relID
         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
@@ -3793,7 +4772,7 @@
 END:VCALENDAR
 """
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
         self.assertTrue(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -4015,7 +4994,7 @@
 END:VCALENDAR
 """
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -4027,16 +5006,16 @@
         cobj = cobjs[0]
         cname2 = cobj.name()
         ical = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(ical), normalize_iCalStr(data_2) % self.subs, "Failed 2")
-        yield cobj.setComponent(Component.fromString(data_2_update % self.subs))
+        self.assertEqual(normalize_iCalStr(ical), normalize_iCalStr(data_2) % self.dtsubs, "Failed 2")
+        yield cobj.setComponent(Component.fromString(data_2_update % self.dtsubs))
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         ical = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(ical), normalize_iCalStr(data_1) % self.subs, "Failed 2")
+        self.assertEqual(normalize_iCalStr(ical), normalize_iCalStr(data_1) % self.dtsubs, "Failed 2")
         cobj = yield self.calendarObjectUnderTest(name=cname2, calendar_name="calendar", home="user02")
         ical = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(ical), normalize_iCalStr(data_2_changed) % self.subs, "Failed 2")
+        self.assertEqual(normalize_iCalStr(ical), normalize_iCalStr(data_2_changed) % self.dtsubs, "Failed 2")
         yield self.commit()
 
 
@@ -4101,7 +5080,7 @@
 END:VCALENDAR
 """
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -4397,7 +5376,7 @@
 """
 
         # Create initial non-split event
-        cobj = yield calendar.createCalendarObjectWithName("data1.ics", Component.fromString(data_1 % self.subs))
+        cobj = yield calendar.createCalendarObjectWithName("data1.ics", Component.fromString(data_1 % self.dtsubs))
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
 
@@ -4414,7 +5393,7 @@
         self.assertEqual(attachment.parameterValue("MANAGED-ID"), mid)
         self.assertEqual(attachment.value(), location)
 
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["mid"] = mid
         relsubs["att_uri"] = location
         relsubs["dtstamp"] = str(ical.masterComponent().propertyValue("DTSTAMP"))
@@ -4441,6 +5420,7 @@
         self.assertEqual(attachment.value(), location)
 
         relsubs["past_mid"] = attachment.parameterValue("MANAGED-ID")
+        attachment = ical_past.masterComponent().getProperty("ATTACH")
         relsubs["att_past_uri"] = attachment.value()
 
         # Verify user01 data
@@ -4754,7 +5734,7 @@
 END:VCALENDAR
 """
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -4766,7 +5746,7 @@
         cobj = yield self.calendarObjectUnderTest(name="data.ics", calendar_name="calendar", home="user01")
         processor.recipient_calendar_resource = cobj
         processor.recipient_calendar = (yield cobj.componentForUser("user01"))
-        processor.message = Component.fromString(itip1 % self.subs)
+        processor.message = Component.fromString(itip1 % self.dtsubs)
         processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
         processor.recipient = LocalCalendarUser("urn:x-uid:user01", None)
         processor.method = "REQUEST"
@@ -4777,7 +5757,7 @@
         yield self.commit()
 
         new_names = []
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
 
         @inlineCallbacks
         def _verify_state():
@@ -4906,7 +5886,7 @@
 END:VCALENDAR
 """
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -4918,7 +5898,7 @@
         cobj = yield self.calendarObjectUnderTest(name="data.ics", calendar_name="calendar", home="user01")
         processor.recipient_calendar_resource = cobj
         processor.recipient_calendar = (yield cobj.componentForUser("user01"))
-        processor.message = Component.fromString(itip1 % self.subs)
+        processor.message = Component.fromString(itip1 % self.dtsubs)
         processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
         processor.recipient = LocalCalendarUser("urn:x-uid:user01", None)
         processor.method = "CANCEL"
@@ -4929,7 +5909,7 @@
         yield self.commit()
 
         # Get user01 data
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         cal = yield self.calendarUnderTest(name="calendar", home="user01")
         cobjs = yield cal.calendarObjects()
         self.assertEqual(len(cobjs), 1)
@@ -5027,7 +6007,7 @@
 END:VCALENDAR
 """
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -5039,7 +6019,7 @@
         cobj = yield self.calendarObjectUnderTest(name="data.ics", calendar_name="calendar", home="user01")
         processor.recipient_calendar_resource = cobj
         processor.recipient_calendar = (yield cobj.componentForUser("user01"))
-        processor.message = Component.fromString(itip1 % self.subs)
+        processor.message = Component.fromString(itip1 % self.dtsubs)
         processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
         processor.recipient = LocalCalendarUser("urn:x-uid:user01", None)
         processor.method = "REQUEST"
@@ -5050,7 +6030,7 @@
         yield self.commit()
 
         # Get user01 data
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         cal = yield self.calendarUnderTest(name="calendar", home="user01")
         cobjs = yield cal.calendarObjects()
         self.assertEqual(len(cobjs), 1)
@@ -5200,7 +6180,7 @@
 END:VCALENDAR
 """
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -5212,7 +6192,7 @@
         cobj = yield self.calendarObjectUnderTest(name="data.ics", calendar_name="calendar", home="user01")
         processor.recipient_calendar_resource = cobj
         processor.recipient_calendar = (yield cobj.componentForUser("user01"))
-        processor.message = Component.fromString(itip1 % self.subs)
+        processor.message = Component.fromString(itip1 % self.dtsubs)
         processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
         processor.recipient = LocalCalendarUser("urn:x-uid:user01", None)
         processor.method = "REQUEST"
@@ -5224,7 +6204,7 @@
 
         # Get user01 data
         ical_future, ical_past, pastUID, relID, _ignore_new_name = yield self._splitDetails("user01")
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["uid"] = pastUID
         relsubs["relID"] = relID
 
@@ -5403,7 +6383,7 @@
 END:VCALENDAR
 """
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -5424,7 +6404,7 @@
         cobj = yield self.calendarObjectUnderTest(name="data.ics", calendar_name="calendar", home="user01")
         processor.recipient_calendar_resource = cobj
         processor.recipient_calendar = (yield cobj.componentForUser("user01"))
-        processor.message = Component.fromString(itip1 % self.subs)
+        processor.message = Component.fromString(itip1 % self.dtsubs)
         processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
         processor.recipient = LocalCalendarUser("urn:x-uid:user01", None)
         processor.method = "REQUEST"
@@ -5443,7 +6423,7 @@
 
         processor.recipient_calendar_resource = None
         processor.recipient_calendar = None
-        processor.message = Component.fromString(itip2 % self.subs)
+        processor.message = Component.fromString(itip2 % self.dtsubs)
         processor.originator = RemoteCalendarUser("mailto:cuser01 at example.org")
         processor.recipient = LocalCalendarUser("urn:x-uid:user01", None)
         processor.method = "REQUEST"
@@ -5804,7 +6784,7 @@
                     responses.add(recipient, responsecode.NOT_FOUND, reqstatus=iTIPRequestStatus.INVALID_CALENDAR_USER)
             return succeed(responses)
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
         self.assertTrue(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -5845,7 +6825,7 @@
 
         # Verify user01 data
         title = "user01"
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["uid"] = newUID
         relsubs["relID"] = relID
         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s\n%s" % (title, diff_iCalStrs(ical_future, data_future % relsubs),))
@@ -6201,7 +7181,7 @@
         # Create one event without active split
         self.patch(config.Scheduling.Options.Splitting, "Enabled", False)
         calendar = yield self.calendarUnderTest(name="calendar", home="user01")
-        component = Component.fromString(data_init % self.subs)
+        component = Component.fromString(data_init % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -6236,7 +7216,7 @@
             return oldScheduling(self, do_smart_merge, split_details)
         self.patch(ImplicitScheduler, "doImplicitScheduling", newScheduling)
 
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         yield self.failUnlessFailure(cobj.setComponent(component), AlreadyFinishedError)
         self.assertTrue(self.transactionUnderTest().timedout)
 
@@ -6280,7 +7260,7 @@
 
         # Verify user01 data
         title = "user01"
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["uid"] = newUID
         relsubs["relID"] = relID
         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
@@ -6542,7 +7522,7 @@
         # Create one event without active split
         self.patch(config.Scheduling.Options.Splitting, "Enabled", False)
         calendar = yield self.calendarUnderTest(name="calendar", home="user01")
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -6585,7 +7565,7 @@
 
         # Verify user01 data
         title = "user01"
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["uid"] = newUID
         relsubs["relID"] = relID
         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
@@ -6885,7 +7865,7 @@
 """
 
         # Create it
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -7013,7 +7993,7 @@
 """
 
         # Create it
-        component = Component.fromString(data % self.subs)
+        component = Component.fromString(data % self.dtsubs)
         cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -7039,7 +8019,7 @@
 
         # Update it
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
-        oldobj = yield cobj.splitAt(DateTime.parseText("%(now_back14)s" % self.subs))
+        oldobj = yield cobj.splitAt(DateTime.parseText("%(now_back14)s" % self.dtsubs))
         oldname = oldobj.name()
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -7076,7 +8056,7 @@
 
         # Verify user01 data
         title = "user01"
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["uid"] = newUID
         relsubs["relID"] = relID
         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
@@ -7115,7 +8095,7 @@
 
         # Update it
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
-        oldobj = yield cobj.splitAt(DateTime.parseText("%(now_back15_12h)s" % self.subs))
+        oldobj = yield cobj.splitAt(DateTime.parseText("%(now_back15_12h)s" % self.dtsubs))
         oldname = oldobj.name()
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -7152,7 +8132,7 @@
 
         # Verify user01 data
         title = "user01"
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["uid"] = newUID
         relsubs["relID"] = relID
         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
@@ -7191,7 +8171,7 @@
 
         # Update it
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
-        oldobj = yield cobj.splitAt(DateTime.parseText("%(now_back14)s" % self.subs))
+        oldobj = yield cobj.splitAt(DateTime.parseText("%(now_back14)s" % self.dtsubs))
         oldname = oldobj.name()
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -7228,7 +8208,7 @@
 
         # Verify user01 data
         title = "user01"
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["uid"] = newUID
         relsubs["relID"] = relID
         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
@@ -7247,7 +8227,7 @@
         cal = yield self.calendarUnderTest(name="calendar", home="user02")
         cobjs = yield cal.calendarObjects()
         self.assertEqual(len(cobjs), 1)
-        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText("%(now_back14)s" % self.subs)), InvalidSplit)
+        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText("%(now_back14)s" % self.dtsubs)), InvalidSplit)
 
 
     @inlineCallbacks
@@ -7262,7 +8242,7 @@
         cal = yield self.calendarUnderTest(name="calendar", home="user02")
         cobjs = yield cal.calendarObjects()
         self.assertEqual(len(cobjs), 1)
-        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText("%(now_back30)s" % self.subs)), InvalidSplit)
+        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText("%(now_back30)s" % self.dtsubs)), InvalidSplit)
 
 
     @inlineCallbacks
@@ -7277,7 +8257,7 @@
         cal = yield self.calendarUnderTest(name="calendar", home="user02")
         cobjs = yield cal.calendarObjects()
         self.assertEqual(len(cobjs), 1)
-        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText("%(now_fwd25)s" % self.subs)), InvalidSplit)
+        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText("%(now_fwd25)s" % self.dtsubs)), InvalidSplit)
 
 
     @inlineCallbacks
@@ -7291,7 +8271,7 @@
 
         # Update it
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
-        oldobj = yield cobj.splitAt(DateTime.parseText("%(now_back14)s" % self.subs), pastUID=pastUID)
+        oldobj = yield cobj.splitAt(DateTime.parseText("%(now_back14)s" % self.dtsubs), pastUID=pastUID)
         oldname = oldobj.name()
         self.assertFalse(hasattr(cobj, "_workItems"))
         yield self.commit()
@@ -7330,7 +8310,7 @@
 
         # Verify user01 data
         title = "user01"
-        relsubs = dict(self.subs)
+        relsubs = dict(self.dtsubs)
         relsubs["uid"] = newUID
         relsubs["relID"] = relID
         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, "Failed future: %s" % (title,))
@@ -7370,7 +8350,7 @@
 
         # Update it
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
-        yield self.failUnlessFailure(cobj.splitAt(DateTime.parseText("%(now_back14)s" % self.subs), pastUID="12345-67890"), InvalidSplit)
+        yield self.failUnlessFailure(cobj.splitAt(DateTime.parseText("%(now_back14)s" % self.dtsubs), pastUID="12345-67890"), InvalidSplit)
 
 
     @inlineCallbacks
@@ -7400,17 +8380,64 @@
         yield self._setupSplitAt()
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user01")
-        component = Component.fromString(data_existing % self.subs)
+        component = Component.fromString(data_existing % self.dtsubs)
         yield calendar.createCalendarObjectWithName("data2.ics", component)
         yield self.commit()
 
         # Update it
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
-        yield self.failUnlessFailure(cobj.splitAt(DateTime.parseText("%(now_back14)s" % self.subs), pastUID="12345-67890-existing"), InvalidSplit)
+        yield self.failUnlessFailure(cobj.splitAt(DateTime.parseText("%(now_back14)s" % self.dtsubs), pastUID="12345-67890-existing"), InvalidSplit)
 
 
+    @inlineCallbacks
+    def test_calendarObjectSplit_splitat_wrong_value_type(self):
+        """
+        Test that user triggered splitting of calendar objects does not work if wrong rid value type is used.
+        """
 
-class TimeRangeUpdateOptimization(CommonCommonTests, unittest.TestCase):
+        yield self._setupSplitAt()
+
+        # DTSTART DATE-TIME UTC/rid DATE
+        cal = yield self.calendarUnderTest(name="calendar", home="user02")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText("%(nowDate)s" % self.dtsubs)), InvalidSplit)
+
+        # DTSTART DATE-TIME UTC/rid DATE-TIME floating
+        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText("%(nowFloating)s" % self.dtsubs)), InvalidSplit)
+
+        data_floating = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-existing
+DTSTART:%(nowFloating)s
+DURATION:P1D
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=DAILY;COUNT=50
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+        component = Component.fromString(data_floating % self.dtsubs)
+        yield calendar.createCalendarObjectWithName("data2.ics", component)
+        yield self.commit()
+
+        # DTSTART DATE/rid DATE-TIME floating
+        cobj = yield self.calendarObjectUnderTest(name="data2.ics", calendar_name="calendar", home="user01")
+        yield self.failUnlessFailure(cobj.splitAt(DateTime.parseText("%(nowFloating)s" % self.dtsubs)), InvalidSplit)
+
+        # DTSTART DATE/rid DATE-TIME UTC
+        yield self.failUnlessFailure(cobj.splitAt(DateTime.parseText("%(now)s" % self.dtsubs)), InvalidSplit)
+
+
+
+class TimeRangeUpdateOptimization(CommonCommonTests, DateTimeSubstitutionsMixin, unittest.TestCase):
     """
     CalendarObject time range optimization tests.
     """
@@ -7422,7 +8449,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -7437,7 +8464,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event #2
 DTSTAMP:20100203T013909Z
@@ -7452,7 +8479,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T130000Z
+DTSTART:{nowDate}T130000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -7467,7 +8494,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -7483,7 +8510,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 STATUS:CANCELLED
@@ -7499,7 +8526,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -7515,7 +8542,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -7531,7 +8558,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -7547,12 +8574,12 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
 RRULE:FREQ=DAILY
-EXDATE:{now}T120000Z
+EXDATE:{nowDate}T120000Z
 END:VEVENT
 END:VCALENDAR
 """
@@ -7564,12 +8591,12 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
 RRULE:FREQ=DAILY
-RDATE:{now}T150000Z
+RDATE:{nowDate}T150000Z
 END:VEVENT
 END:VCALENDAR
 """
@@ -7581,12 +8608,7 @@
         yield self.buildStoreAndDirectory()
         yield self.populate()
 
-        self.now = DateTime.getNowUTC()
-        self.now.setDateOnly(True)
-        self.now1 = self.now.duplicate()
-        self.now1.offsetDay(1)
-        self.now2 = self.now.duplicate()
-        self.now2.offsetDay(2)
+        self.setupDateTimeValues()
 
         self.trcount = 0
         base_addInstances = CalendarObject._addInstances
@@ -7630,7 +8652,7 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest()
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
@@ -7644,14 +8666,14 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest()
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
 
         # Second PUT does not cause T-R change
         cobj = yield self.calendarObjectUnderTest()
-        yield cobj.setComponent(Component.fromString(self.EVENT2.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.EVENT2.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
@@ -7667,14 +8689,14 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest()
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
 
         # Second PUT does cause T-R change
         cobj = yield self.calendarObjectUnderTest()
-        yield cobj.setComponent(Component.fromString(self.EVENT2.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.EVENT2.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 2)
@@ -7688,14 +8710,14 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest()
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
 
         # Second PUT causes T-R change
         cobj = yield self.calendarObjectUnderTest()
-        yield cobj.setComponent(Component.fromString(self.EVENT3.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.EVENT3.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 2)
@@ -7709,14 +8731,14 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest()
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
 
         # Second PUT causes T-R change
         cobj = yield self.calendarObjectUnderTest()
-        yield cobj.setComponent(Component.fromString(self.EVENT4.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.EVENT4.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 2)
@@ -7730,14 +8752,14 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest()
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
 
         # Second PUT causes T-R change
         cobj = yield self.calendarObjectUnderTest()
-        yield cobj.setComponent(Component.fromString(self.EVENT5.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.EVENT5.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 2)
@@ -7751,14 +8773,14 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest()
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
 
         # Second PUT causes T-R change
         cobj = yield self.calendarObjectUnderTest()
-        yield cobj.setComponent(Component.fromString(self.EVENT6.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.EVENT6.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 2)
@@ -7772,14 +8794,14 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest()
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT7.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT7.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
 
         # Second PUT causes T-R change
         cobj = yield self.calendarObjectUnderTest()
-        yield cobj.setComponent(Component.fromString(self.EVENT8.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.EVENT8.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 2)
@@ -7793,14 +8815,14 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest()
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT7.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT7.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
 
         # Second PUT causes T-R change
         cobj = yield self.calendarObjectUnderTest()
-        yield cobj.setComponent(Component.fromString(self.EVENT9.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.EVENT9.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 2)
@@ -7814,14 +8836,14 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest()
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT7.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT7.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 1)
 
         # Second PUT causes T-R change
         cobj = yield self.calendarObjectUnderTest()
-        yield cobj.setComponent(Component.fromString(self.EVENT10.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.EVENT10.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 2)
@@ -7834,7 +8856,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -7852,7 +8874,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -7870,7 +8892,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event #2
 DTSTAMP:20100203T013909Z
@@ -7888,7 +8910,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T140000Z
+DTSTART:{nowDate}T140000Z
 DURATION:PT1H
 SUMMARY:New Event #2
 DTSTAMP:20100203T013909Z
@@ -7910,7 +8932,7 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.INVITE1.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.INVITE1.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 3)
@@ -7919,21 +8941,21 @@
         cal = yield self.calendarUnderTest(home="user02", name="calendar")
         cobjs = yield cal.calendarObjects()
         self.assertEqual(len(cobjs), 1)
-        yield cobjs[0].setComponent(Component.fromString(self.INVITE2.format(now=self.now.getText())))
+        yield cobjs[0].setComponent(Component.fromString(self.INVITE2.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 5)
 
         # Organizer summary change does not cause T-R change (except for inbox item)
         cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar")
-        yield cobj.setComponent(Component.fromString(self.INVITE3.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.INVITE3.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 6)
 
         # Organizer dtstart change causes T-R change
         cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar")
-        yield cobj.setComponent(Component.fromString(self.INVITE4.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.INVITE4.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 9)
@@ -7952,7 +8974,7 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.INVITE1.format(now=self.now.getText())))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.INVITE1.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 3)
@@ -7961,21 +8983,21 @@
         cal = yield self.calendarUnderTest(home="user02", name="calendar")
         cobjs = yield cal.calendarObjects()
         self.assertEqual(len(cobjs), 1)
-        yield cobjs[0].setComponent(Component.fromString(self.INVITE2.format(now=self.now.getText())))
+        yield cobjs[0].setComponent(Component.fromString(self.INVITE2.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 5)
 
         # Organizer summary change causes T-R change
         cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar")
-        yield cobj.setComponent(Component.fromString(self.INVITE3.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.INVITE3.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 8)
 
         # Organizer dtstart change causes T-R change
         cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar")
-        yield cobj.setComponent(Component.fromString(self.INVITE4.format(now=self.now.getText())))
+        yield cobj.setComponent(Component.fromString(self.INVITE4.format(**self.dtsubs)))
         yield self.commit()
 
         self.assertEqual(self.trcount, 11)
@@ -7988,7 +9010,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -7999,8 +9021,8 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-RECURRENCE-ID:{now1}T120000Z
-DTSTART:{now1}T120000Z
+RECURRENCE-ID:{nowDate_fwd1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event now1
 DTSTAMP:20100203T013909Z
@@ -8018,8 +9040,8 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-RECURRENCE-ID:{now1}T120000Z
-DTSTART:{now1}T120000Z
+RECURRENCE-ID:{nowDate_fwd1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event now1
 DTSTAMP:20100203T013909Z
@@ -8037,7 +9059,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8048,8 +9070,8 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-RECURRENCE-ID:{now1}T120000Z
-DTSTART:{now1}T120000Z
+RECURRENCE-ID:{nowDate_fwd1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event now1
 DTSTAMP:20100203T013909Z
@@ -8060,8 +9082,8 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-RECURRENCE-ID:{now2}T120000Z
-DTSTART:{now2}T120000Z
+RECURRENCE-ID:{nowDate_fwd2}T120000Z
+DTSTART:{nowDate_fwd2}T120000Z
 DURATION:PT1H
 SUMMARY:New Event now2
 DTSTAMP:20100203T013909Z
@@ -8079,7 +9101,7 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-DTSTART:{now}T120000Z
+DTSTART:{nowDate}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8090,8 +9112,8 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-RECURRENCE-ID:{now1}T120000Z
-DTSTART:{now1}T120000Z
+RECURRENCE-ID:{nowDate_fwd1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event now1
 DTSTAMP:20100203T013909Z
@@ -8101,8 +9123,8 @@
 BEGIN:VEVENT
 CREATED:20100203T013849Z
 UID:uid1
-RECURRENCE-ID:{now2}T120000Z
-DTSTART:{now2}T120000Z
+RECURRENCE-ID:{nowDate_fwd2}T120000Z
+DTSTART:{nowDate_fwd2}T120000Z
 DURATION:PT1H
 SUMMARY:New Event now2
 DTSTAMP:20100203T013909Z
@@ -8126,11 +9148,7 @@
 
         # First PUT causes T-R change
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.INVITE_OVERRIDE1.format(
-            now=self.now.getText(),
-            now1=self.now1.getText(),
-            now2=self.now2.getText(),
-        )))
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.INVITE_OVERRIDE1.format(**self.dtsubs)))
         yield self.commit()
 
         # Wait for it to complete
@@ -8140,11 +9158,7 @@
 
         # Organizer adds attendee to override causes T-R change (except for their item)
         cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar")
-        yield cobj.setComponent(Component.fromString(self.INVITE_OVERRIDE3.format(
-            now=self.now.getText(),
-            now1=self.now1.getText(),
-            now2=self.now2.getText(),
-        )))
+        yield cobj.setComponent(Component.fromString(self.INVITE_OVERRIDE3.format(**self.dtsubs)))
         yield self.commit()
 
         # Wait for it to complete
@@ -8154,11 +9168,7 @@
 
         # Organizer removes attendee from override causes T-R change (except for their item)
         cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar")
-        yield cobj.setComponent(Component.fromString(self.INVITE_OVERRIDE4.format(
-            now=self.now.getText(),
-            now1=self.now1.getText(),
-            now2=self.now2.getText(),
-        )))
+        yield cobj.setComponent(Component.fromString(self.INVITE_OVERRIDE4.format(**self.dtsubs)))
         yield self.commit()
 
         # Wait for it to complete
@@ -8168,7 +9178,7 @@
 
 
 
-class GroupExpand(CommonCommonTests, unittest.TestCase):
+class GroupExpand(CommonCommonTests, DateTimeSubstitutionsMixin, unittest.TestCase):
     """
     CalendarObject group attendee expansion.
     """
@@ -8186,26 +9196,11 @@
 
         yield self.populate()
 
-        now = DateTime.getNowUTC()
-        now.setDateOnly(True)
-        past1 = now.duplicate()
-        past1.offsetDay(-1)
-        past2 = now.duplicate()
-        past2.offsetDay(-2)
-        past400 = now.duplicate()
+        self.setupDateTimeValues()
+
+        past400 = self.nowDate.duplicate()
         past400.offsetDay(-400)
-        now1 = now.duplicate()
-        now1.offsetDay(1)
-        now2 = now.duplicate()
-        now2.offsetDay(2)
-        self.subs = {
-            "now": now,
-            "past1": past1,
-            "past2": past2,
-            "past400": past400,
-            "now1": now1,
-            "now2": now2,
-        }
+        self.dtsubs["nowDate_back400"] = past400
 
 
     @inlineCallbacks
@@ -8244,7 +9239,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8253,7 +9248,7 @@
 ATTENDEE:urn:x-uid:group01
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8262,7 +9257,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8271,7 +9266,7 @@
 SUMMARY:New Event
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         # PUT causes expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
@@ -8298,7 +9293,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8306,7 +9301,7 @@
 ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         event2 = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8314,7 +9309,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8323,7 +9318,7 @@
 ATTENDEE:urn:x-uid:group01
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8332,7 +9327,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8342,8 +9337,17 @@
 SUMMARY:New Event
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
+        # Group user has no data
+        cal = yield self.calendarUnderTest(home="user02", name="calendar")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 0)
+        cal = yield self.calendarUnderTest(home="user02", name="inbox")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 0)
+        yield self.commit()
+
         # PUT does not cause expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
         yield cal.createObjectResourceWithName("1.ics", Component.fromString(event1))
@@ -8366,7 +9370,18 @@
         links = yield calobj.groupEventLinks()
         self.assertEqual(len(links), 1)
 
+        # Group user has invite data
+        cal = yield self.calendarUnderTest(home="user02", name="calendar")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        cal = yield self.calendarUnderTest(home="user02", name="inbox")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        comp = yield cobjs[0].componentForUser()
+        self.assertTrue("METHOD:REQUEST" in str(comp))
+        yield self.commit()
 
+
     @inlineCallbacks
     def test_expand_update_existing(self):
         """
@@ -8379,7 +9394,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8388,7 +9403,7 @@
 ATTENDEE:urn:x-uid:group01
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         event2 = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8397,7 +9412,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8407,7 +9422,7 @@
 SUMMARY:New Event #2
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         event3 = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8416,7 +9431,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{now1}T130000Z
+DTSTART:{nowDate_fwd1}T130000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8426,7 +9441,7 @@
 SUMMARY:New Event #2
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8435,7 +9450,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{now1}T130000Z
+DTSTART:{nowDate_fwd1}T130000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8445,7 +9460,7 @@
 SUMMARY:New Event #2
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         # PUT causes expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
@@ -8491,7 +9506,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8501,7 +9516,7 @@
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8510,7 +9525,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8520,7 +9535,7 @@
 SUMMARY:New Event
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         # PUT causes expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
@@ -8547,7 +9562,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8556,7 +9571,7 @@
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         event2 = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8564,7 +9579,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8574,7 +9589,7 @@
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8583,7 +9598,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8594,7 +9609,7 @@
 SUMMARY:New Event
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         # PUT does not cause expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
@@ -8631,7 +9646,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8641,7 +9656,7 @@
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         event2 = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8650,7 +9665,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{now1}T120000Z
+DTSTART:{nowDate_fwd1}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8661,7 +9676,7 @@
 SUMMARY:New Event #2
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         event3 = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8670,7 +9685,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{now1}T130000Z
+DTSTART:{nowDate_fwd1}T130000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8681,7 +9696,7 @@
 SUMMARY:New Event #2
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8690,7 +9705,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{now1}T130000Z
+DTSTART:{nowDate_fwd1}T130000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8701,7 +9716,7 @@
 SUMMARY:New Event #2
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         # PUT causes expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
@@ -8747,7 +9762,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{past1}T120000Z
+DTSTART:{nowDate_back1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8756,7 +9771,7 @@
 ATTENDEE:urn:x-uid:group01
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8765,7 +9780,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{past1}T120000Z
+DTSTART:{nowDate_back1}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8774,7 +9789,7 @@
 SUMMARY:New Event
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         # PUT causes expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
@@ -8801,7 +9816,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{past1}T120000Z
+DTSTART:{nowDate_back1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8809,7 +9824,7 @@
 ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         event2 = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8817,7 +9832,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{past1}T120000Z
+DTSTART:{nowDate_back1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8826,7 +9841,7 @@
 ATTENDEE:urn:x-uid:group01
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8835,7 +9850,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{past1}T120000Z
+DTSTART:{nowDate_back1}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8845,7 +9860,7 @@
 SUMMARY:New Event
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         # PUT does not cause expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
@@ -8882,7 +9897,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{past1}T120000Z
+DTSTART:{nowDate_back1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -8891,7 +9906,7 @@
 ATTENDEE:urn:x-uid:group01
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         event2 = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8900,7 +9915,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{past1}T120000Z
+DTSTART:{nowDate_back1}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8910,7 +9925,7 @@
 SUMMARY:New Event #2
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         event3 = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8919,7 +9934,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{past1}T130000Z
+DTSTART:{nowDate_back1}T130000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8929,7 +9944,7 @@
 SUMMARY:New Event #2
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -8938,7 +9953,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{past1}T130000Z
+DTSTART:{nowDate_back1}T130000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -8948,7 +9963,7 @@
 SUMMARY:New Event #2
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         # PUT causes expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
@@ -8995,7 +10010,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{past1}T120000Z
+DTSTART:{nowDate_back1}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -9005,7 +10020,7 @@
 RRULE:FREQ=YEARLY;INTERVAL=2
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -9014,7 +10029,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{past1}T120000Z
+DTSTART:{nowDate_back1}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -9024,7 +10039,7 @@
 SUMMARY:New Event
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         # PUT causes expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")
@@ -9052,7 +10067,7 @@
 CALSCALE:GREGORIAN
 BEGIN:VEVENT
 UID:uid1
-DTSTART:{past400}T120000Z
+DTSTART:{nowDate_back400}T120000Z
 DURATION:PT1H
 SUMMARY:New Event
 DTSTAMP:20100203T013909Z
@@ -9062,7 +10077,7 @@
 RRULE:FREQ=YEARLY;INTERVAL=4
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         result = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -9071,7 +10086,7 @@
 BEGIN:VEVENT
 UID:uid1
 DTSTAMP:20100203T013909Z
-DTSTART:{past400}T120000Z
+DTSTART:{nowDate_back400}T120000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -9081,7 +10096,7 @@
 SUMMARY:New Event
 END:VEVENT
 END:VCALENDAR
-""".format(**self.subs)
+""".format(**self.dtsubs)
 
         # PUT causes expansion
         cal = yield self.calendarUnderTest(home="user01", name="calendar")

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/util.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/caldav/datastore/test/util.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -15,8 +15,10 @@
 # limitations under the License.
 ##
 from twisted.trial.unittest import TestCase
+from twisted.internet.defer import inlineCallbacks
 from twext.python.clsprop import classproperty
-from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.ical import Component, normalize_iCalStr, diff_iCalStrs
+from pycalendar.datetime import DateTime
 
 """
 Store test utility functions
@@ -70,3 +72,96 @@
                 },
             },
         }
+
+
+
+class DateTimeSubstitutionsMixin(object):
+    """
+    Mix-in class for tests that defines a set of str.format() substitutions for date-time values
+    relative to the current time. This allows tests to always use relative-to-now values rather
+    than fixed values which may become in valid in the future (e.g., a test that needs an event
+    in the future and uses 2017 works fine up until 2017 and then starts to fail.
+    """
+
+    def setupDateTimeValues(self):
+
+        self.dtsubs = {}
+
+        # Set of "now" values that are directly accessible
+        self.now = DateTime.getNowUTC()
+        self.now.setHHMMSS(0, 0, 0)
+        self.now12 = DateTime.getNowUTC()
+        self.now12.setHHMMSS(12, 0, 0)
+        self.nowDate = self.now.duplicate()
+        self.nowDate.setDateOnly(True)
+        self.nowFloating = self.now.duplicate()
+        self.nowFloating.setTimezoneID(None)
+
+        self.dtsubs["now"] = self.now
+        self.dtsubs["now12"] = self.now12
+        self.dtsubs["nowDate"] = self.nowDate
+        self.dtsubs["nowFloating"] = self.nowFloating
+
+        # Values going 30 days back from now
+        for i in range(30):
+            attrname = "now_back%s" % (i + 1,)
+            setattr(self, attrname, self.now.duplicate())
+            getattr(self, attrname).offsetDay(-(i + 1))
+            self.dtsubs[attrname] = getattr(self, attrname)
+
+            attrname_12h = "now_back%s_12h" % (i + 1,)
+            setattr(self, attrname_12h, getattr(self, attrname).duplicate())
+            getattr(self, attrname_12h).offsetHours(12)
+            self.dtsubs[attrname_12h] = getattr(self, attrname_12h)
+
+            attrname_1 = "now_back%s_1" % (i + 1,)
+            setattr(self, attrname_1, getattr(self, attrname).duplicate())
+            getattr(self, attrname_1).offsetSeconds(-1)
+            self.dtsubs[attrname_1] = getattr(self, attrname_1)
+
+            attrname = "nowDate_back%s" % (i + 1,)
+            setattr(self, attrname, self.nowDate.duplicate())
+            getattr(self, attrname).offsetDay(-(i + 1))
+            self.dtsubs[attrname] = getattr(self, attrname)
+
+            attrname = "nowFloating_back%s" % (i + 1,)
+            setattr(self, attrname, self.nowFloating.duplicate())
+            getattr(self, attrname).offsetDay(-(i + 1))
+            self.dtsubs[attrname] = getattr(self, attrname)
+
+            attrname_1 = "nowFloating_back%s_1" % (i + 1,)
+            setattr(self, attrname_1, getattr(self, attrname).duplicate())
+            getattr(self, attrname_1).offsetSeconds(-1)
+            self.dtsubs[attrname_1] = getattr(self, attrname_1)
+
+        # Values going 30 days forward from now
+        for i in range(30):
+            attrname = "now_fwd%s" % (i + 1,)
+            setattr(self, attrname, self.now.duplicate())
+            getattr(self, attrname).offsetDay(i + 1)
+            self.dtsubs[attrname] = getattr(self, attrname)
+
+            attrname_12h = "now_fwd%s_12h" % (i + 1,)
+            setattr(self, attrname_12h, getattr(self, attrname).duplicate())
+            getattr(self, attrname_12h).offsetHours(12)
+            self.dtsubs[attrname_12h] = getattr(self, attrname_12h)
+
+            attrname = "nowDate_fwd%s" % (i + 1,)
+            setattr(self, attrname, self.nowDate.duplicate())
+            getattr(self, attrname).offsetDay(i + 1)
+            self.dtsubs[attrname] = getattr(self, attrname)
+
+            attrname = "nowFloating_fwd%s" % (i + 1,)
+            setattr(self, attrname, self.nowFloating.duplicate())
+            getattr(self, attrname).offsetDay(i + 1)
+            self.dtsubs[attrname] = getattr(self, attrname)
+
+
+    def assertEqualCalendarData(self, cal1, cal2):
+        if isinstance(cal1, str):
+            cal1 = Component.fromString(cal1)
+        if isinstance(cal2, str):
+            cal2 = Component.fromString(cal2)
+        ncal1 = normalize_iCalStr(cal1)
+        ncal2 = normalize_iCalStr(cal2)
+        self.assertEqual(ncal1, ncal2, msg=diff_iCalStrs(ncal1, ncal2))

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/carddav/datastore/sql.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/carddav/datastore/sql.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -570,7 +570,8 @@
             {
                 rev.REVISION: schema.REVISION_SEQ,
                 rev.OBJECT_RESOURCE_ID: Parameter("id"),
-                rev.DELETED: True
+                rev.DELETED: True,
+                rev.MODIFIED: utcNowSQL,
             },
             Where=(
                 rev.RESOURCE_ID == Parameter("resourceID")).And(

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/carddav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/carddav/datastore/test/test_sql.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/carddav/datastore/test/test_sql.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -28,20 +28,21 @@
 from twisted.trial import unittest
 
 from twistedcaldav import carddavxml
-from twistedcaldav.vcard import Component as VCard
+from twistedcaldav.vcard import Component as VCard, Component
 from twistedcaldav.vcard import Component as VComponent
 
 from txdav.base.propertystore.base import PropertyName
 
 from txdav.carddav.datastore.test.common import CommonTests as AddressBookCommonTests, \
-    vcard4_text
+    vcard4_text, adbk1Root
 from txdav.carddav.datastore.test.test_file import setUpAddressBookStore
 from txdav.carddav.datastore.util import _migrateAddressbook, migrateHome
 
 from txdav.common.icommondatastore import NoSuchObjectResourceError
 from txdav.common.datastore.sql import EADDRESSBOOKTYPE, CommonObjectResource
 from txdav.common.datastore.sql_tables import _ABO_KIND_PERSON, _ABO_KIND_GROUP, schema
-from txdav.common.datastore.test.util import cleanStore
+from txdav.common.datastore.test.util import cleanStore, CommonCommonTests, \
+    populateAddressBooksFrom
 from txdav.carddav.datastore.sql import AddressBook
 
 from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
@@ -919,7 +920,38 @@
         yield self.commit()
 
 
+
+class SyncTests(CommonCommonTests, unittest.TestCase):
+    """
+    Revision table/sync report tests.
+    """
+
     @inlineCallbacks
+    def setUp(self):
+        yield super(SyncTests, self).setUp()
+        yield self.buildStoreAndDirectory()
+        yield self.populate()
+
+
+    requirements = {
+        "user01": {
+            "addressbook": {
+                "1.vcf": adbk1Root.child("1.vcf").getContent(),
+                "2.vcf": adbk1Root.child("2.vcf").getContent(),
+                "3.vcf": adbk1Root.child("3.vcf").getContent(),
+            },
+            "not_a_addressbook": None
+        },
+    }
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateAddressBooksFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    @inlineCallbacks
     def test_updateAfterRevisionCleanup(self):
         """
         Make sure L{AddressBookObject}'s can be updated or removed after revision cleanup
@@ -957,26 +989,26 @@
 END:VCARD
 """
 
-        yield self.homeUnderTest()
-        adbk = yield self.addressbookUnderTest(name="addressbook")
+        yield self.addressbookHomeUnderTest(name="user01")
+        adbk = yield self.addressbookUnderTest(home="user01", name="addressbook")
         yield adbk.createAddressBookObjectWithName("person.vcf", VCard.fromString(person))
         yield adbk.createAddressBookObjectWithName("group.vcf", VCard.fromString(group))
         yield self.commit()
 
         # Remove the revision
-        adbk = yield self.addressbookUnderTest(name="addressbook")
+        adbk = yield self.addressbookUnderTest(home="user01", name="addressbook")
         yield adbk.syncToken()
         yield self.transactionUnderTest().deleteRevisionsBefore(adbk._syncTokenRevision + 1)
         yield self.commit()
 
         # Update the object
-        obj = yield self.addressbookObjectUnderTest(name="group.vcf", addressbook_name="addressbook")
+        obj = yield self.addressbookObjectUnderTest(name="group.vcf", addressbook_name="addressbook", home="user01")
         yield obj.setComponent(VCard.fromString(group_update))
         yield self.commit()
 
-        obj = yield self.addressbookObjectUnderTest(name="group.vcf", addressbook_name="addressbook")
+        obj = yield self.addressbookObjectUnderTest(name="group.vcf", addressbook_name="addressbook", home="user01")
         self.assertTrue(obj is not None)
-        obj = yield self.addressbookObjectUnderTest(name="person.vcf", addressbook_name="addressbook")
+        obj = yield self.addressbookObjectUnderTest(name="person.vcf", addressbook_name="addressbook", home="user01")
         self.assertTrue(obj is not None)
         yield self.commit()
 
@@ -1010,26 +1042,76 @@
 END:VCARD
 """
 
-        yield self.homeUnderTest()
-        adbk = yield self.addressbookUnderTest(name="addressbook")
+        yield self.addressbookHomeUnderTest(name="user01")
+        adbk = yield self.addressbookUnderTest(home="user01", name="addressbook")
         yield adbk.createAddressBookObjectWithName("person.vcf", VCard.fromString(person))
         yield adbk.createAddressBookObjectWithName("group.vcf", VCard.fromString(group))
         yield self.commit()
 
         # Remove the revision
-        adbk = yield self.addressbookUnderTest(name="addressbook")
+        adbk = yield self.addressbookUnderTest(home="user01", name="addressbook")
         yield adbk.syncToken()
         yield self.transactionUnderTest().deleteRevisionsBefore(adbk._syncTokenRevision + 1)
         yield self.commit()
 
         # Remove the object
-        obj = yield self.addressbookObjectUnderTest(name="group.vcf", addressbook_name="addressbook")
+        obj = yield self.addressbookObjectUnderTest(name="group.vcf", addressbook_name="addressbook", home="user01")
         self.assertTrue(obj is not None)
         yield obj.remove()
         yield self.commit()
 
-        obj = yield self.addressbookObjectUnderTest(name="group.vcf", addressbook_name="addressbook")
+        obj = yield self.addressbookObjectUnderTest(name="group.vcf", addressbook_name="addressbook", home="user01")
         self.assertTrue(obj is None)
-        obj = yield self.addressbookObjectUnderTest(name="person.vcf", addressbook_name="addressbook")
+        obj = yield self.addressbookObjectUnderTest(name="person.vcf", addressbook_name="addressbook", home="user01")
         self.assertTrue(obj is not None)
         yield self.commit()
+
+
+    @inlineCallbacks
+    def test_revisionModified(self):
+        """
+        Make sure the revision table MODIFIED value changes for an update or delete
+        """
+
+        @inlineCallbacks
+        def _getModified():
+            home = yield self.addressbookHomeUnderTest(name="user01")
+            addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
+            rev = addressbook._revisionsSchema
+            modified = yield Select(
+                [rev.MODIFIED, ],
+                From=rev,
+                Where=(
+                    rev.ADDRESSBOOK_HOME_RESOURCE_ID == Parameter("homeID")).And(
+                    rev.RESOURCE_NAME == Parameter("resourceName")
+                )
+            ).on(
+                home._txn,
+                homeID=home.id(),
+                resourceName="1.vcf",
+            )
+            yield self.commit()
+            returnValue(modified[0][0])
+
+        # Get current modified
+        old_modified = yield _getModified()
+        self.assertNotEqual(old_modified, None)
+
+        # Update resource
+        aobj = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="1.vcf")
+        yield aobj.setComponent(Component.fromString(adbk1Root.child("1.vcf").getContent()))
+        yield self.commit()
+
+        # Modified changed
+        update_modified = yield _getModified()
+        self.assertGreater(update_modified, old_modified)
+
+        # Delete resource
+        aobj = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="1.vcf")
+        yield aobj.remove()
+        yield self.commit()
+
+        # Modified changed
+        delete_modified = yield _getModified()
+        self.assertGreater(delete_modified, old_modified)
+        self.assertGreater(delete_modified, update_modified)

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/file.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/file.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -276,7 +276,8 @@
                             continue
                         txn = self.newTransaction("enumerate home %r" % (uid,))
                         home = txn.homeWithUID(storeType, uid, False)
-                        yield (txn, home)
+                        if home is not None:
+                            yield (txn, home)
 
 
     def _eachCalendarHome(self):

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/podding/attachments.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/podding/attachments.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/podding/attachments.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -69,7 +69,7 @@
             request["stream"],
         )
 
-        returnValue((attachment.managedID(), location,))
+        returnValue((attachment.managedID(), attachment.size(), location,))
 
 
     @inlineCallbacks
@@ -115,7 +115,7 @@
             request["stream"],
         )
 
-        returnValue((attachment.managedID(), location,))
+        returnValue((attachment.managedID(), attachment.size(), location,))
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/podding/test/test_conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/podding/test/test_conduit.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/podding/test/test_conduit.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -970,11 +970,13 @@
         yield self.commitTransaction(0)
 
         shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics")
-        attachment, location = yield shared_object.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
+        data = "Here is some text."
+        attachment, location = yield shared_object.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream(data))
         managedID = attachment.managedID()
         from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
         self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
-        self.assertTrue("user01/attachments/test" in location)
+        self.assertEqual(attachment.size(), len(data))
+        self.assertTrue("user01/dropbox/" in location)
         yield self.commitTransaction(1)
 
         cobjs = yield ManagedAttachment.referencesTo(self.theTransactionUnderTest(0), managedID)
@@ -1005,11 +1007,13 @@
         yield self.commitTransaction(0)
 
         shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics")
-        attachment, location = yield shared_object.updateAttachment(managedID, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some more text."))
+        data = "Here is some more text."
+        attachment, location = yield shared_object.updateAttachment(managedID, MimeType.fromString("text/plain"), "test.txt", MemoryStream(data))
         managedID = attachment.managedID()
         from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
         self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
-        self.assertTrue("user01/attachments/test" in location)
+        self.assertEqual(attachment.size(), len(data))
+        self.assertTrue("user01/dropbox/" in location)
         yield self.commitTransaction(1)
 
         cobjs = yield ManagedAttachment.referencesTo(self.theTransactionUnderTest(0), managedID)

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -3409,8 +3409,7 @@
             )
 
             # Get revisions
-            revisions = (yield cls._revisionsForResourceIDs(childResourceIDs).on(home._txn, resourceIDs=childResourceIDs))
-            revisions = dict(revisions)
+            revisions = yield cls.childSyncTokenRevisions(home, childResourceIDs)
 
         # Create the actual objects merging in properties
         for dataRow in dataRows:
@@ -3421,7 +3420,7 @@
             propstore = propertyStores.get(resourceID, None)
 
             child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, propstore)
-            child._syncTokenRevision = revisions.get(resourceID, 0)
+            child._syncTokenRevision = revisions.get(resourceID, None)
             results.append(child)
 
         returnValue(results)

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2015-07-28 02:43:19 UTC (rev 15011)
@@ -413,6 +413,12 @@
     primary key ("ORGANIZER", "ATTENDEE", "ICALUID")
 );
 
+create table TEST_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "DELAY" integer
+);
+
 create table IMIP_INVITATION_WORK (
     "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
@@ -669,7 +675,7 @@
     "VALUE" nvarchar2(255)
 );
 
-insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '55');
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '57');
 insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '6');
 insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
 insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
@@ -821,6 +827,10 @@
     "TOKEN"
 );
 
+create index TEST_WORK_JOB_ID_228ede32 on TEST_WORK (
+    "JOB_ID"
+);
+
 create index IMIP_INVITATION_WORK__586d064c on IMIP_INVITATION_WORK (
     "JOB_ID"
 );

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/current.sql	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/current.sql	2015-07-28 02:43:19 UTC (rev 15011)
@@ -765,7 +765,20 @@
 
 create sequence WORKITEM_SEQ;
 
+---------------
+-- Test Work --
+---------------
 
+create table TEST_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  DELAY 						integer
+);
+
+create index TEST_WORK_JOB_ID on
+  TEST_WORK(JOB_ID);
+
+
 ---------------------------
 -- IMIP Inivitation Work --
 ---------------------------
@@ -1269,7 +1282,7 @@
   VALUE                         varchar(255)
 );
 
-insert into CALENDARSERVER values ('VERSION', '55');
+insert into CALENDARSERVER values ('VERSION', '57');
 insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '6');
 insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
 insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');

Copied: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/oracle-dialect/v55.sql (from rev 15009, CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v55.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/oracle-dialect/v55.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/oracle-dialect/v55.sql	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,1034 @@
+create sequence RESOURCE_ID_SEQ;
+create sequence JOB_SEQ;
+create sequence INSTANCE_ID_SEQ;
+create sequence ATTACHMENT_ID_SEQ;
+create sequence REVISION_SEQ;
+create sequence WORKITEM_SEQ;
+create table NODE_INFO (
+    "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 JOB (
+    "JOB_ID" integer primary key,
+    "WORK_TYPE" nvarchar2(255),
+    "PRIORITY" integer default 0,
+    "WEIGHT" integer default 0,
+    "NOT_BEFORE" timestamp not null,
+    "ASSIGNED" timestamp default null,
+    "OVERDUE" timestamp default null,
+    "FAILED" integer default 0,
+    "PAUSE" integer default 0
+);
+
+create table CALENDAR_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255),
+    "STATUS" integer default 0 not null,
+    "DATAVERSION" integer default 0 not null, 
+    unique ("OWNER_UID", "STATUS")
+);
+
+create table HOME_STATUS (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into HOME_STATUS (DESCRIPTION, ID) values ('normal', 0);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('external', 1);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('purging', 2);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('migrating', 3);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('disabled', 4);
+create table CALENDAR (
+    "RESOURCE_ID" integer primary key
+);
+
+create table CALENDAR_HOME_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR_HOME on delete cascade,
+    "QUOTA_USED_BYTES" integer default 0 not null,
+    "TRASH" integer default null references CALENDAR on delete set null,
+    "DEFAULT_EVENTS" integer default null references CALENDAR on delete set null,
+    "DEFAULT_TASKS" integer default null references CALENDAR on delete set null,
+    "DEFAULT_POLLS" integer default null references CALENDAR on delete set null,
+    "ALARM_VEVENT_TIMED" nclob default null,
+    "ALARM_VEVENT_ALLDAY" nclob default null,
+    "ALARM_VTODO_TIMED" nclob default null,
+    "ALARM_VTODO_ALLDAY" nclob default null,
+    "AVAILABILITY" nclob default null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDAR_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR on delete cascade,
+    "SUPPORTED_COMPONENTS" nvarchar2(255) default null,
+    "CHILD_TYPE" integer default 0 not null,
+    "TRASHED" timestamp default null,
+    "IS_IN_TRASH" 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 CHILD_TYPE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CHILD_TYPE (DESCRIPTION, ID) values ('normal', 0);
+insert into CHILD_TYPE (DESCRIPTION, ID) values ('inbox', 1);
+insert into CHILD_TYPE (DESCRIPTION, ID) values ('trash', 2);
+create table CALENDAR_MIGRATION (
+    "CALENDAR_HOME_RESOURCE_ID" integer references CALENDAR_HOME on delete cascade,
+    "REMOTE_RESOURCE_ID" integer not null,
+    "LOCAL_RESOURCE_ID" integer references CALENDAR on delete cascade,
+    "LAST_SYNC_TOKEN" nvarchar2(255), 
+    primary key ("CALENDAR_HOME_RESOURCE_ID", "REMOTE_RESOURCE_ID")
+);
+
+create table NOTIFICATION_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255),
+    "STATUS" integer default 0 not null,
+    "DATAVERSION" integer default 0 not null, 
+    unique ("OWNER_UID", "STATUS")
+);
+
+create table NOTIFICATION (
+    "RESOURCE_ID" integer primary key,
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME,
+    "NOTIFICATION_UID" nvarchar2(255),
+    "NOTIFICATION_TYPE" nvarchar2(255),
+    "NOTIFICATION_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,
+    "BIND_REVISION" integer default 0 not null,
+    "BIND_UID" nvarchar2(36) default null,
+    "MESSAGE" nclob,
+    "TRANSP" integer default 0 not null,
+    "ALARM_VEVENT_TIMED" nclob default null,
+    "ALARM_VEVENT_ALLDAY" nclob default null,
+    "ALARM_VTODO_TIMED" nclob default null,
+    "ALARM_VTODO_ALLDAY" nclob default null,
+    "TIMEZONE" nclob default null, 
+    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);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('indirect', 4);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('group', 5);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('group_read', 6);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('group_write', 7);
+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);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('deleted', 4);
+create table CALENDAR_TRANSP (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('opaque', 0);
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('transparent', 1);
+create table CALENDAR_OBJECT (
+    "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),
+    "TRASHED" timestamp default null,
+    "ORIGINAL_COLLECTION" integer default null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "DATAVERSION" integer default 0 not null, 
+    unique ("CALENDAR_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table CALENDAR_OBJ_ATTACHMENTS_MODE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('none', 0);
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('write', 2);
+create table CALENDAR_ACCESS_TYPE (
+    "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 PERUSER (
+    "TIME_RANGE_INSTANCE_ID" integer not null references TIME_RANGE on delete cascade,
+    "USER_ID" nvarchar2(255),
+    "TRANSPARENT" integer not null,
+    "ADJUSTED_START_DATE" timestamp default null,
+    "ADJUSTED_END_DATE" timestamp default null, 
+    primary key ("TIME_RANGE_INSTANCE_ID", "USER_ID")
+);
+
+create table CALENDAR_OBJECT_MIGRATION (
+    "CALENDAR_HOME_RESOURCE_ID" integer references CALENDAR_HOME on delete cascade,
+    "REMOTE_RESOURCE_ID" integer not null,
+    "LOCAL_RESOURCE_ID" integer references CALENDAR_OBJECT on delete cascade, 
+    primary key ("CALENDAR_HOME_RESOURCE_ID", "REMOTE_RESOURCE_ID")
+);
+
+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 ATTACHMENT_MIGRATION (
+    "CALENDAR_HOME_RESOURCE_ID" integer references CALENDAR_HOME on delete cascade,
+    "REMOTE_RESOURCE_ID" integer not null,
+    "LOCAL_RESOURCE_ID" integer references ATTACHMENT on delete cascade, 
+    primary key ("CALENDAR_HOME_RESOURCE_ID", "REMOTE_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,
+    "ADDRESSBOOK_PROPERTY_STORE_ID" integer not null,
+    "OWNER_UID" nvarchar2(255),
+    "STATUS" integer default 0 not null,
+    "DATAVERSION" integer default 0 not null, 
+    unique ("OWNER_UID", "STATUS")
+);
+
+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 SHARED_ADDRESSBOOK_BIND (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "OWNER_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "BIND_REVISION" integer default 0 not null,
+    "BIND_UID" nvarchar2(36) default null,
+    "MESSAGE" nclob, 
+    primary key ("ADDRESSBOOK_HOME_RESOURCE_ID", "OWNER_HOME_RESOURCE_ID"), 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_NAME")
+);
+
+create table ADDRESSBOOK_OBJECT (
+    "RESOURCE_ID" integer primary key,
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "VCARD_TEXT" nclob,
+    "VCARD_UID" nvarchar2(255),
+    "KIND" integer not null,
+    "MD5" nchar(32),
+    "TRASHED" timestamp default null,
+    "IS_IN_TRASH" integer default 0 not null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "DATAVERSION" integer default 0 not null, 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "RESOURCE_NAME"), 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "VCARD_UID")
+);
+
+create table ADDRESSBOOK_OBJECT_KIND (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('person', 0);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('group', 1);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('resource', 2);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('location', 3);
+create table ABO_MEMBERS (
+    "GROUP_ID" integer not null,
+    "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "MEMBER_ID" integer not null,
+    "REVISION" integer not null,
+    "REMOVED" integer default 0 not null,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    primary key ("GROUP_ID", "MEMBER_ID", "REVISION")
+);
+
+create table ABO_FOREIGN_MEMBERS (
+    "GROUP_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+    "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "MEMBER_ADDRESS" nvarchar2(255), 
+    primary key ("GROUP_ID", "MEMBER_ADDRESS")
+);
+
+create table SHARED_GROUP_BIND (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "GROUP_RESOURCE_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+    "GROUP_ADDRESSBOOK_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "BIND_REVISION" integer default 0 not null,
+    "BIND_UID" nvarchar2(36) default null,
+    "MESSAGE" nclob, 
+    primary key ("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_RESOURCE_ID"), 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_ADDRESSBOOK_NAME")
+);
+
+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,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique ("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_ID", "CALENDAR_NAME", "RESOURCE_NAME")
+);
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "OWNER_HOME_RESOURCE_ID" integer references ADDRESSBOOK_HOME,
+    "ADDRESSBOOK_NAME" nvarchar2(255) default null,
+    "OBJECT_RESOURCE_ID" integer default 0,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "OWNER_HOME_RESOURCE_ID", "ADDRESSBOOK_NAME", "RESOURCE_NAME")
+);
+
+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,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    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,
+    "JOB_ID" integer not null references JOB,
+    "FROM_ADDR" nvarchar2(255),
+    "TO_ADDR" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table IMIP_POLLING_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table IMIP_REPLY_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table PUSH_NOTIFICATION_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "PUSH_ID" nvarchar2(255),
+    "PUSH_PRIORITY" integer not null
+);
+
+create table GROUP_CACHER_POLLING_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table GROUP_REFRESH_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "GROUP_UID" nvarchar2(255)
+);
+
+create table GROUP_DELEGATE_CHANGES_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "DELEGATOR_UID" nvarchar2(255),
+    "READ_DELEGATE_UID" nvarchar2(255),
+    "WRITE_DELEGATE_UID" nvarchar2(255)
+);
+
+create table GROUPS (
+    "GROUP_ID" integer primary key,
+    "NAME" nvarchar2(255),
+    "GROUP_UID" nvarchar2(255) unique,
+    "MEMBERSHIP_HASH" nvarchar2(255),
+    "EXTANT" integer default 1,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table GROUP_MEMBERSHIP (
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "MEMBER_UID" nvarchar2(255), 
+    primary key ("GROUP_ID", "MEMBER_UID")
+);
+
+create table GROUP_ATTENDEE_RECONCILE_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "GROUP_ID" integer not null references GROUPS on delete cascade
+);
+
+create table GROUP_ATTENDEE (
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "MEMBERSHIP_HASH" nvarchar2(255), 
+    primary key ("GROUP_ID", "RESOURCE_ID")
+);
+
+create table GROUP_SHAREE_RECONCILE_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "CALENDAR_ID" integer not null references CALENDAR on delete cascade,
+    "GROUP_ID" integer not null references GROUPS on delete cascade
+);
+
+create table GROUP_SHAREE (
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "CALENDAR_ID" integer not null references CALENDAR on delete cascade,
+    "GROUP_BIND_MODE" integer not null,
+    "MEMBERSHIP_HASH" nvarchar2(255), 
+    primary key ("GROUP_ID", "CALENDAR_ID")
+);
+
+create table DELEGATES (
+    "DELEGATOR" nvarchar2(255),
+    "DELEGATE" nvarchar2(255),
+    "READ_WRITE" integer not null, 
+    primary key ("DELEGATOR", "READ_WRITE", "DELEGATE")
+);
+
+create table DELEGATE_GROUPS (
+    "DELEGATOR" nvarchar2(255),
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "READ_WRITE" integer not null,
+    "IS_EXTERNAL" integer not null, 
+    primary key ("DELEGATOR", "READ_WRITE", "GROUP_ID")
+);
+
+create table EXTERNAL_DELEGATE_GROUPS (
+    "DELEGATOR" nvarchar2(255) primary key,
+    "GROUP_UID_READ" nvarchar2(255),
+    "GROUP_UID_WRITE" nvarchar2(255)
+);
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
+);
+
+create table CALENDAR_OBJECT_UPGRADE_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
+);
+
+create table FIND_MIN_VALID_REVISION_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table REVISION_CLEANUP_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table INBOX_CLEANUP_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table CLEANUP_ONE_INBOX_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "HOME_ID" integer not null unique references CALENDAR_HOME on delete cascade
+);
+
+create table SCHEDULE_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "ICALENDAR_UID" nvarchar2(255),
+    "WORK_TYPE" nvarchar2(255)
+);
+
+create table SCHEDULE_REFRESH_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "ATTENDEE_COUNT" integer
+);
+
+create table SCHEDULE_REFRESH_ATTENDEES (
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "ATTENDEE" nvarchar2(255), 
+    primary key ("RESOURCE_ID", "ATTENDEE")
+);
+
+create table SCHEDULE_AUTO_REPLY_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "PARTSTAT" nvarchar2(255)
+);
+
+create table SCHEDULE_ORGANIZER_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "SCHEDULE_ACTION" integer not null,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer,
+    "ICALENDAR_TEXT_OLD" nclob,
+    "ICALENDAR_TEXT_NEW" nclob,
+    "ATTENDEE_COUNT" integer,
+    "SMART_MERGE" integer
+);
+
+create table SCHEDULE_ACTION (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('create', 0);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify', 1);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify-cancelled', 2);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('remove', 3);
+create table SCHEDULE_ORGANIZER_SEND_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "SCHEDULE_ACTION" integer not null,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer,
+    "ATTENDEE" nvarchar2(255),
+    "ITIP_MSG" nclob,
+    "NO_REFRESH" integer
+);
+
+create table SCHEDULE_REPLY_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer,
+    "ITIP_MSG" nclob
+);
+
+create table PRINCIPAL_PURGE_POLLING_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table PRINCIPAL_PURGE_CHECK_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "UID" nvarchar2(255)
+);
+
+create table PRINCIPAL_PURGE_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "UID" nvarchar2(255)
+);
+
+create table PRINCIPAL_PURGE_HOME_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade
+);
+
+create table MIGRATION_CLEANUP_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade
+);
+
+create table HOME_CLEANUP_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "OWNER_UID" nvarchar2(255)
+);
+
+create table MIGRATED_HOME_CLEANUP_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "OWNER_UID" nvarchar2(255)
+);
+
+create table CALENDARSERVER (
+    "NAME" nvarchar2(255) primary key,
+    "VALUE" nvarchar2(255)
+);
+
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '55');
+insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '6');
+insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
+insert into CALENDARSERVER (NAME, VALUE) values ('MIN-VALID-REVISION', '1');
+create index CALENDAR_HOME_METADAT_475de898 on CALENDAR_HOME_METADATA (
+    "TRASH"
+);
+
+create index CALENDAR_HOME_METADAT_3cb9049e on CALENDAR_HOME_METADATA (
+    "DEFAULT_EVENTS"
+);
+
+create index CALENDAR_HOME_METADAT_d55e5548 on CALENDAR_HOME_METADATA (
+    "DEFAULT_TASKS"
+);
+
+create index CALENDAR_HOME_METADAT_910264ce on CALENDAR_HOME_METADATA (
+    "DEFAULT_POLLS"
+);
+
+create index CALENDAR_MIGRATION_LO_0525c72b on CALENDAR_MIGRATION (
+    "LOCAL_RESOURCE_ID"
+);
+
+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_c4dc619c on CALENDAR_OBJECT (
+    "CALENDAR_RESOURCE_ID",
+    "RECURRANCE_MAX",
+    "RECURRANCE_MIN"
+);
+
+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 CALENDAR_OBJECT_MIGRA_0502cbef on CALENDAR_OBJECT_MIGRATION (
+    "CALENDAR_HOME_RESOURCE_ID",
+    "LOCAL_RESOURCE_ID"
+);
+
+create index CALENDAR_OBJECT_MIGRA_3577efd9 on CALENDAR_OBJECT_MIGRATION (
+    "LOCAL_RESOURCE_ID"
+);
+
+create index ATTACHMENT_CALENDAR_H_0078845c on ATTACHMENT (
+    "CALENDAR_HOME_RESOURCE_ID"
+);
+
+create index ATTACHMENT_DROPBOX_ID_5073cf23 on ATTACHMENT (
+    "DROPBOX_ID"
+);
+
+create index ATTACHMENT_CALENDAR_O_81508484 on ATTACHMENT_CALENDAR_OBJECT (
+    "CALENDAR_OBJECT_RESOURCE_ID"
+);
+
+create index ATTACHMENT_MIGRATION__804bf85e on ATTACHMENT_MIGRATION (
+    "CALENDAR_HOME_RESOURCE_ID",
+    "LOCAL_RESOURCE_ID"
+);
+
+create index ATTACHMENT_MIGRATION__816947fe on ATTACHMENT_MIGRATION (
+    "LOCAL_RESOURCE_ID"
+);
+
+create index SHARED_ADDRESSBOOK_BI_e9a2e6d4 on SHARED_ADDRESSBOOK_BIND (
+    "OWNER_HOME_RESOURCE_ID"
+);
+
+create index ABO_MEMBERS_ADDRESSBO_4effa879 on ABO_MEMBERS (
+    "ADDRESSBOOK_ID"
+);
+
+create index ABO_MEMBERS_MEMBER_ID_8d66adcf on ABO_MEMBERS (
+    "MEMBER_ID"
+);
+
+create index ABO_FOREIGN_MEMBERS_A_1fd2c5e9 on ABO_FOREIGN_MEMBERS (
+    "ADDRESSBOOK_ID"
+);
+
+create index SHARED_GROUP_BIND_RES_cf52f95d on SHARED_GROUP_BIND (
+    "GROUP_RESOURCE_ID"
+);
+
+create index CALENDAR_OBJECT_REVIS_6d9d929c on CALENDAR_OBJECT_REVISIONS (
+    "CALENDAR_RESOURCE_ID",
+    "RESOURCE_NAME",
+    "DELETED",
+    "REVISION"
+);
+
+create index CALENDAR_OBJECT_REVIS_265c8acf on CALENDAR_OBJECT_REVISIONS (
+    "CALENDAR_RESOURCE_ID",
+    "REVISION"
+);
+
+create index CALENDAR_OBJECT_REVIS_550b1c56 on CALENDAR_OBJECT_REVISIONS (
+    "CALENDAR_HOME_RESOURCE_ID",
+    "REVISION"
+);
+
+create index ADDRESSBOOK_OBJECT_RE_00fe8288 on ADDRESSBOOK_OBJECT_REVISIONS (
+    "OWNER_HOME_RESOURCE_ID",
+    "RESOURCE_NAME",
+    "DELETED",
+    "REVISION"
+);
+
+create index ADDRESSBOOK_OBJECT_RE_45004780 on ADDRESSBOOK_OBJECT_REVISIONS (
+    "OWNER_HOME_RESOURCE_ID",
+    "REVISION"
+);
+
+create index NOTIFICATION_OBJECT_R_036a9cee on NOTIFICATION_OBJECT_REVISIONS (
+    "NOTIFICATION_HOME_RESOURCE_ID",
+    "REVISION"
+);
+
+create index APN_SUBSCRIPTIONS_RES_9610d78e on APN_SUBSCRIPTIONS (
+    "RESOURCE_KEY"
+);
+
+create index IMIP_TOKENS_TOKEN_e94b918f on IMIP_TOKENS (
+    "TOKEN"
+);
+
+create index IMIP_INVITATION_WORK__586d064c on IMIP_INVITATION_WORK (
+    "JOB_ID"
+);
+
+create index IMIP_POLLING_WORK_JOB_d5535891 on IMIP_POLLING_WORK (
+    "JOB_ID"
+);
+
+create index IMIP_REPLY_WORK_JOB_I_bf4ae73e on IMIP_REPLY_WORK (
+    "JOB_ID"
+);
+
+create index PUSH_NOTIFICATION_WOR_8bbab117 on PUSH_NOTIFICATION_WORK (
+    "JOB_ID"
+);
+
+create index PUSH_NOTIFICATION_WOR_3a3ee588 on PUSH_NOTIFICATION_WORK (
+    "PUSH_ID"
+);
+
+create index GROUP_CACHER_POLLING__6eb3151c on GROUP_CACHER_POLLING_WORK (
+    "JOB_ID"
+);
+
+create index GROUP_REFRESH_WORK_JO_717ede20 on GROUP_REFRESH_WORK (
+    "JOB_ID"
+);
+
+create index GROUP_REFRESH_WORK_GR_0325f3a8 on GROUP_REFRESH_WORK (
+    "GROUP_UID"
+);
+
+create index GROUP_DELEGATE_CHANGE_8bf9e6d8 on GROUP_DELEGATE_CHANGES_WORK (
+    "JOB_ID"
+);
+
+create index GROUP_DELEGATE_CHANGE_d8f7af69 on GROUP_DELEGATE_CHANGES_WORK (
+    "DELEGATOR_UID"
+);
+
+create index GROUP_MEMBERSHIP_MEMB_0ca508e8 on GROUP_MEMBERSHIP (
+    "MEMBER_UID"
+);
+
+create index GROUP_ATTENDEE_RECONC_da73d3c2 on GROUP_ATTENDEE_RECONCILE_WORK (
+    "JOB_ID"
+);
+
+create index GROUP_ATTENDEE_RECONC_b894ee7a on GROUP_ATTENDEE_RECONCILE_WORK (
+    "RESOURCE_ID"
+);
+
+create index GROUP_ATTENDEE_RECONC_5eabc549 on GROUP_ATTENDEE_RECONCILE_WORK (
+    "GROUP_ID"
+);
+
+create index GROUP_ATTENDEE_RESOUR_855124dc on GROUP_ATTENDEE (
+    "RESOURCE_ID"
+);
+
+create index GROUP_SHAREE_RECONCIL_9aad0858 on GROUP_SHAREE_RECONCILE_WORK (
+    "JOB_ID"
+);
+
+create index GROUP_SHAREE_RECONCIL_4dc60f78 on GROUP_SHAREE_RECONCILE_WORK (
+    "CALENDAR_ID"
+);
+
+create index GROUP_SHAREE_RECONCIL_1d14c921 on GROUP_SHAREE_RECONCILE_WORK (
+    "GROUP_ID"
+);
+
+create index GROUP_SHAREE_CALENDAR_28a88850 on GROUP_SHAREE (
+    "CALENDAR_ID"
+);
+
+create index DELEGATE_TO_DELEGATOR_5e149b11 on DELEGATES (
+    "DELEGATE",
+    "READ_WRITE",
+    "DELEGATOR"
+);
+
+create index DELEGATE_GROUPS_GROUP_25117446 on DELEGATE_GROUPS (
+    "GROUP_ID"
+);
+
+create index CALENDAR_OBJECT_SPLIT_af71dcda on CALENDAR_OBJECT_SPLITTER_WORK (
+    "RESOURCE_ID"
+);
+
+create index CALENDAR_OBJECT_SPLIT_33603b72 on CALENDAR_OBJECT_SPLITTER_WORK (
+    "JOB_ID"
+);
+
+create index CALENDAR_OBJECT_UPGRA_a5c181eb on CALENDAR_OBJECT_UPGRADE_WORK (
+    "RESOURCE_ID"
+);
+
+create index CALENDAR_OBJECT_UPGRA_39d6f8f9 on CALENDAR_OBJECT_UPGRADE_WORK (
+    "JOB_ID"
+);
+
+create index FIND_MIN_VALID_REVISI_78d17400 on FIND_MIN_VALID_REVISION_WORK (
+    "JOB_ID"
+);
+
+create index REVISION_CLEANUP_WORK_eb062686 on REVISION_CLEANUP_WORK (
+    "JOB_ID"
+);
+
+create index INBOX_CLEANUP_WORK_JO_799132bd on INBOX_CLEANUP_WORK (
+    "JOB_ID"
+);
+
+create index CLEANUP_ONE_INBOX_WOR_375dac36 on CLEANUP_ONE_INBOX_WORK (
+    "JOB_ID"
+);
+
+create index SCHEDULE_WORK_JOB_ID_65e810ee on SCHEDULE_WORK (
+    "JOB_ID"
+);
+
+create index SCHEDULE_WORK_ICALEND_089f33dc on SCHEDULE_WORK (
+    "ICALENDAR_UID"
+);
+
+create index SCHEDULE_REFRESH_WORK_26084c7b on SCHEDULE_REFRESH_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index SCHEDULE_REFRESH_WORK_989efe54 on SCHEDULE_REFRESH_WORK (
+    "RESOURCE_ID"
+);
+
+create index SCHEDULE_AUTO_REPLY_W_0256478d on SCHEDULE_AUTO_REPLY_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index SCHEDULE_AUTO_REPLY_W_0755e754 on SCHEDULE_AUTO_REPLY_WORK (
+    "RESOURCE_ID"
+);
+
+create index SCHEDULE_ORGANIZER_WO_18ce4edd on SCHEDULE_ORGANIZER_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index SCHEDULE_ORGANIZER_WO_14702035 on SCHEDULE_ORGANIZER_WORK (
+    "RESOURCE_ID"
+);
+
+create index SCHEDULE_ORGANIZER_SE_9ec9f827 on SCHEDULE_ORGANIZER_SEND_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index SCHEDULE_ORGANIZER_SE_699fefc4 on SCHEDULE_ORGANIZER_SEND_WORK (
+    "RESOURCE_ID"
+);
+
+create index SCHEDULE_REPLY_WORK_H_745af8cf on SCHEDULE_REPLY_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index SCHEDULE_REPLY_WORK_R_11bd3fbb on SCHEDULE_REPLY_WORK (
+    "RESOURCE_ID"
+);
+
+create index PRINCIPAL_PURGE_POLLI_6383e68a on PRINCIPAL_PURGE_POLLING_WORK (
+    "JOB_ID"
+);
+
+create index PRINCIPAL_PURGE_CHECK_b0c024c1 on PRINCIPAL_PURGE_CHECK_WORK (
+    "JOB_ID"
+);
+
+create index PRINCIPAL_PURGE_CHECK_198388a5 on PRINCIPAL_PURGE_CHECK_WORK (
+    "UID"
+);
+
+create index PRINCIPAL_PURGE_WORK__7a8141a3 on PRINCIPAL_PURGE_WORK (
+    "JOB_ID"
+);
+
+create index PRINCIPAL_PURGE_WORK__db35cfdc on PRINCIPAL_PURGE_WORK (
+    "UID"
+);
+
+create index PRINCIPAL_PURGE_HOME__f35eea7a on PRINCIPAL_PURGE_HOME_WORK (
+    "JOB_ID"
+);
+
+create index PRINCIPAL_PURGE_HOME__967e4480 on PRINCIPAL_PURGE_HOME_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index MIGRATION_CLEANUP_WOR_8c23cc35 on MIGRATION_CLEANUP_WORK (
+    "JOB_ID"
+);
+
+create index MIGRATION_CLEANUP_WOR_86181cb8 on MIGRATION_CLEANUP_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index HOME_CLEANUP_WORK_JOB_9631dfb0 on HOME_CLEANUP_WORK (
+    "JOB_ID"
+);
+
+create index MIGRATED_HOME_CLEANUP_4c714fd4 on MIGRATED_HOME_CLEANUP_WORK (
+    "JOB_ID"
+);
+
+-- Extra schema to add to current-oracle-dialect.sql

Copied: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/oracle-dialect/v56.sql (from rev 15009, CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v56.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/oracle-dialect/v56.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/oracle-dialect/v56.sql	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,1044 @@
+create sequence RESOURCE_ID_SEQ;
+create sequence JOB_SEQ;
+create sequence INSTANCE_ID_SEQ;
+create sequence ATTACHMENT_ID_SEQ;
+create sequence REVISION_SEQ;
+create sequence WORKITEM_SEQ;
+create table NODE_INFO (
+    "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 JOB (
+    "JOB_ID" integer primary key,
+    "WORK_TYPE" nvarchar2(255),
+    "PRIORITY" integer default 0,
+    "WEIGHT" integer default 0,
+    "NOT_BEFORE" timestamp not null,
+    "ASSIGNED" timestamp default null,
+    "OVERDUE" timestamp default null,
+    "FAILED" integer default 0,
+    "PAUSE" integer default 0
+);
+
+create table CALENDAR_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255),
+    "STATUS" integer default 0 not null,
+    "DATAVERSION" integer default 0 not null, 
+    unique ("OWNER_UID", "STATUS")
+);
+
+create table HOME_STATUS (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into HOME_STATUS (DESCRIPTION, ID) values ('normal', 0);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('external', 1);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('purging', 2);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('migrating', 3);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('disabled', 4);
+create table CALENDAR (
+    "RESOURCE_ID" integer primary key
+);
+
+create table CALENDAR_HOME_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR_HOME on delete cascade,
+    "QUOTA_USED_BYTES" integer default 0 not null,
+    "TRASH" integer default null references CALENDAR on delete set null,
+    "DEFAULT_EVENTS" integer default null references CALENDAR on delete set null,
+    "DEFAULT_TASKS" integer default null references CALENDAR on delete set null,
+    "DEFAULT_POLLS" integer default null references CALENDAR on delete set null,
+    "ALARM_VEVENT_TIMED" nclob default null,
+    "ALARM_VEVENT_ALLDAY" nclob default null,
+    "ALARM_VTODO_TIMED" nclob default null,
+    "ALARM_VTODO_ALLDAY" nclob default null,
+    "AVAILABILITY" nclob default null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDAR_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR on delete cascade,
+    "SUPPORTED_COMPONENTS" nvarchar2(255) default null,
+    "CHILD_TYPE" integer default 0 not null,
+    "TRASHED" timestamp default null,
+    "IS_IN_TRASH" 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 CHILD_TYPE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CHILD_TYPE (DESCRIPTION, ID) values ('normal', 0);
+insert into CHILD_TYPE (DESCRIPTION, ID) values ('inbox', 1);
+insert into CHILD_TYPE (DESCRIPTION, ID) values ('trash', 2);
+create table CALENDAR_MIGRATION (
+    "CALENDAR_HOME_RESOURCE_ID" integer references CALENDAR_HOME on delete cascade,
+    "REMOTE_RESOURCE_ID" integer not null,
+    "LOCAL_RESOURCE_ID" integer references CALENDAR on delete cascade,
+    "LAST_SYNC_TOKEN" nvarchar2(255), 
+    primary key ("CALENDAR_HOME_RESOURCE_ID", "REMOTE_RESOURCE_ID")
+);
+
+create table NOTIFICATION_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255),
+    "STATUS" integer default 0 not null,
+    "DATAVERSION" integer default 0 not null, 
+    unique ("OWNER_UID", "STATUS")
+);
+
+create table NOTIFICATION (
+    "RESOURCE_ID" integer primary key,
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME,
+    "NOTIFICATION_UID" nvarchar2(255),
+    "NOTIFICATION_TYPE" nvarchar2(255),
+    "NOTIFICATION_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,
+    "BIND_REVISION" integer default 0 not null,
+    "BIND_UID" nvarchar2(36) default null,
+    "MESSAGE" nclob,
+    "TRANSP" integer default 0 not null,
+    "ALARM_VEVENT_TIMED" nclob default null,
+    "ALARM_VEVENT_ALLDAY" nclob default null,
+    "ALARM_VTODO_TIMED" nclob default null,
+    "ALARM_VTODO_ALLDAY" nclob default null,
+    "TIMEZONE" nclob default null, 
+    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);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('indirect', 4);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('group', 5);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('group_read', 6);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('group_write', 7);
+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);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('deleted', 4);
+create table CALENDAR_TRANSP (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('opaque', 0);
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('transparent', 1);
+create table CALENDAR_OBJECT (
+    "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),
+    "TRASHED" timestamp default null,
+    "ORIGINAL_COLLECTION" integer default null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "DATAVERSION" integer default 0 not null, 
+    unique ("CALENDAR_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table CALENDAR_OBJ_ATTACHMENTS_MODE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('none', 0);
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('write', 2);
+create table CALENDAR_ACCESS_TYPE (
+    "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 PERUSER (
+    "TIME_RANGE_INSTANCE_ID" integer not null references TIME_RANGE on delete cascade,
+    "USER_ID" nvarchar2(255),
+    "TRANSPARENT" integer not null,
+    "ADJUSTED_START_DATE" timestamp default null,
+    "ADJUSTED_END_DATE" timestamp default null, 
+    primary key ("TIME_RANGE_INSTANCE_ID", "USER_ID")
+);
+
+create table CALENDAR_OBJECT_MIGRATION (
+    "CALENDAR_HOME_RESOURCE_ID" integer references CALENDAR_HOME on delete cascade,
+    "REMOTE_RESOURCE_ID" integer not null,
+    "LOCAL_RESOURCE_ID" integer references CALENDAR_OBJECT on delete cascade, 
+    primary key ("CALENDAR_HOME_RESOURCE_ID", "REMOTE_RESOURCE_ID")
+);
+
+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 ATTACHMENT_MIGRATION (
+    "CALENDAR_HOME_RESOURCE_ID" integer references CALENDAR_HOME on delete cascade,
+    "REMOTE_RESOURCE_ID" integer not null,
+    "LOCAL_RESOURCE_ID" integer references ATTACHMENT on delete cascade, 
+    primary key ("CALENDAR_HOME_RESOURCE_ID", "REMOTE_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,
+    "ADDRESSBOOK_PROPERTY_STORE_ID" integer not null,
+    "OWNER_UID" nvarchar2(255),
+    "STATUS" integer default 0 not null,
+    "DATAVERSION" integer default 0 not null, 
+    unique ("OWNER_UID", "STATUS")
+);
+
+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 SHARED_ADDRESSBOOK_BIND (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "OWNER_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "BIND_REVISION" integer default 0 not null,
+    "BIND_UID" nvarchar2(36) default null,
+    "MESSAGE" nclob, 
+    primary key ("ADDRESSBOOK_HOME_RESOURCE_ID", "OWNER_HOME_RESOURCE_ID"), 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_NAME")
+);
+
+create table ADDRESSBOOK_OBJECT (
+    "RESOURCE_ID" integer primary key,
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "VCARD_TEXT" nclob,
+    "VCARD_UID" nvarchar2(255),
+    "KIND" integer not null,
+    "MD5" nchar(32),
+    "TRASHED" timestamp default null,
+    "IS_IN_TRASH" integer default 0 not null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "DATAVERSION" integer default 0 not null, 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "RESOURCE_NAME"), 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "VCARD_UID")
+);
+
+create table ADDRESSBOOK_OBJECT_KIND (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('person', 0);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('group', 1);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('resource', 2);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('location', 3);
+create table ABO_MEMBERS (
+    "GROUP_ID" integer not null,
+    "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "MEMBER_ID" integer not null,
+    "REVISION" integer not null,
+    "REMOVED" integer default 0 not null,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    primary key ("GROUP_ID", "MEMBER_ID", "REVISION")
+);
+
+create table ABO_FOREIGN_MEMBERS (
+    "GROUP_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+    "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "MEMBER_ADDRESS" nvarchar2(255), 
+    primary key ("GROUP_ID", "MEMBER_ADDRESS")
+);
+
+create table SHARED_GROUP_BIND (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "GROUP_RESOURCE_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+    "GROUP_ADDRESSBOOK_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "BIND_REVISION" integer default 0 not null,
+    "BIND_UID" nvarchar2(36) default null,
+    "MESSAGE" nclob, 
+    primary key ("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_RESOURCE_ID"), 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_ADDRESSBOOK_NAME")
+);
+
+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,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique ("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_ID", "CALENDAR_NAME", "RESOURCE_NAME")
+);
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "OWNER_HOME_RESOURCE_ID" integer references ADDRESSBOOK_HOME,
+    "ADDRESSBOOK_NAME" nvarchar2(255) default null,
+    "OBJECT_RESOURCE_ID" integer default 0,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "OWNER_HOME_RESOURCE_ID", "ADDRESSBOOK_NAME", "RESOURCE_NAME")
+);
+
+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,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    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 TEST_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "DELAY" integer
+);
+
+create table IMIP_INVITATION_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "FROM_ADDR" nvarchar2(255),
+    "TO_ADDR" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table IMIP_POLLING_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table IMIP_REPLY_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table PUSH_NOTIFICATION_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "PUSH_ID" nvarchar2(255),
+    "PUSH_PRIORITY" integer not null
+);
+
+create table GROUP_CACHER_POLLING_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table GROUP_REFRESH_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "GROUP_UID" nvarchar2(255)
+);
+
+create table GROUP_DELEGATE_CHANGES_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "DELEGATOR_UID" nvarchar2(255),
+    "READ_DELEGATE_UID" nvarchar2(255),
+    "WRITE_DELEGATE_UID" nvarchar2(255)
+);
+
+create table GROUPS (
+    "GROUP_ID" integer primary key,
+    "NAME" nvarchar2(255),
+    "GROUP_UID" nvarchar2(255) unique,
+    "MEMBERSHIP_HASH" nvarchar2(255),
+    "EXTANT" integer default 1,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table GROUP_MEMBERSHIP (
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "MEMBER_UID" nvarchar2(255), 
+    primary key ("GROUP_ID", "MEMBER_UID")
+);
+
+create table GROUP_ATTENDEE_RECONCILE_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "GROUP_ID" integer not null references GROUPS on delete cascade
+);
+
+create table GROUP_ATTENDEE (
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "MEMBERSHIP_HASH" nvarchar2(255), 
+    primary key ("GROUP_ID", "RESOURCE_ID")
+);
+
+create table GROUP_SHAREE_RECONCILE_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "CALENDAR_ID" integer not null references CALENDAR on delete cascade,
+    "GROUP_ID" integer not null references GROUPS on delete cascade
+);
+
+create table GROUP_SHAREE (
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "CALENDAR_ID" integer not null references CALENDAR on delete cascade,
+    "GROUP_BIND_MODE" integer not null,
+    "MEMBERSHIP_HASH" nvarchar2(255), 
+    primary key ("GROUP_ID", "CALENDAR_ID")
+);
+
+create table DELEGATES (
+    "DELEGATOR" nvarchar2(255),
+    "DELEGATE" nvarchar2(255),
+    "READ_WRITE" integer not null, 
+    primary key ("DELEGATOR", "READ_WRITE", "DELEGATE")
+);
+
+create table DELEGATE_GROUPS (
+    "DELEGATOR" nvarchar2(255),
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "READ_WRITE" integer not null,
+    "IS_EXTERNAL" integer not null, 
+    primary key ("DELEGATOR", "READ_WRITE", "GROUP_ID")
+);
+
+create table EXTERNAL_DELEGATE_GROUPS (
+    "DELEGATOR" nvarchar2(255) primary key,
+    "GROUP_UID_READ" nvarchar2(255),
+    "GROUP_UID_WRITE" nvarchar2(255)
+);
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
+);
+
+create table CALENDAR_OBJECT_UPGRADE_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
+);
+
+create table FIND_MIN_VALID_REVISION_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table REVISION_CLEANUP_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table INBOX_CLEANUP_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table CLEANUP_ONE_INBOX_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "HOME_ID" integer not null unique references CALENDAR_HOME on delete cascade
+);
+
+create table SCHEDULE_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "ICALENDAR_UID" nvarchar2(255),
+    "WORK_TYPE" nvarchar2(255)
+);
+
+create table SCHEDULE_REFRESH_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "ATTENDEE_COUNT" integer
+);
+
+create table SCHEDULE_REFRESH_ATTENDEES (
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "ATTENDEE" nvarchar2(255), 
+    primary key ("RESOURCE_ID", "ATTENDEE")
+);
+
+create table SCHEDULE_AUTO_REPLY_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "PARTSTAT" nvarchar2(255)
+);
+
+create table SCHEDULE_ORGANIZER_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "SCHEDULE_ACTION" integer not null,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer,
+    "ICALENDAR_TEXT_OLD" nclob,
+    "ICALENDAR_TEXT_NEW" nclob,
+    "ATTENDEE_COUNT" integer,
+    "SMART_MERGE" integer
+);
+
+create table SCHEDULE_ACTION (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('create', 0);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify', 1);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify-cancelled', 2);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('remove', 3);
+create table SCHEDULE_ORGANIZER_SEND_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "SCHEDULE_ACTION" integer not null,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer,
+    "ATTENDEE" nvarchar2(255),
+    "ITIP_MSG" nclob,
+    "NO_REFRESH" integer
+);
+
+create table SCHEDULE_REPLY_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer,
+    "ITIP_MSG" nclob
+);
+
+create table PRINCIPAL_PURGE_POLLING_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB
+);
+
+create table PRINCIPAL_PURGE_CHECK_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "UID" nvarchar2(255)
+);
+
+create table PRINCIPAL_PURGE_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "UID" nvarchar2(255)
+);
+
+create table PRINCIPAL_PURGE_HOME_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade
+);
+
+create table MIGRATION_CLEANUP_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade
+);
+
+create table HOME_CLEANUP_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "OWNER_UID" nvarchar2(255)
+);
+
+create table MIGRATED_HOME_CLEANUP_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "OWNER_UID" nvarchar2(255)
+);
+
+create table CALENDARSERVER (
+    "NAME" nvarchar2(255) primary key,
+    "VALUE" nvarchar2(255)
+);
+
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '56');
+insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '6');
+insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
+insert into CALENDARSERVER (NAME, VALUE) values ('MIN-VALID-REVISION', '1');
+create index CALENDAR_HOME_METADAT_475de898 on CALENDAR_HOME_METADATA (
+    "TRASH"
+);
+
+create index CALENDAR_HOME_METADAT_3cb9049e on CALENDAR_HOME_METADATA (
+    "DEFAULT_EVENTS"
+);
+
+create index CALENDAR_HOME_METADAT_d55e5548 on CALENDAR_HOME_METADATA (
+    "DEFAULT_TASKS"
+);
+
+create index CALENDAR_HOME_METADAT_910264ce on CALENDAR_HOME_METADATA (
+    "DEFAULT_POLLS"
+);
+
+create index CALENDAR_MIGRATION_LO_0525c72b on CALENDAR_MIGRATION (
+    "LOCAL_RESOURCE_ID"
+);
+
+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_c4dc619c on CALENDAR_OBJECT (
+    "CALENDAR_RESOURCE_ID",
+    "RECURRANCE_MAX",
+    "RECURRANCE_MIN"
+);
+
+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 CALENDAR_OBJECT_MIGRA_0502cbef on CALENDAR_OBJECT_MIGRATION (
+    "CALENDAR_HOME_RESOURCE_ID",
+    "LOCAL_RESOURCE_ID"
+);
+
+create index CALENDAR_OBJECT_MIGRA_3577efd9 on CALENDAR_OBJECT_MIGRATION (
+    "LOCAL_RESOURCE_ID"
+);
+
+create index ATTACHMENT_CALENDAR_H_0078845c on ATTACHMENT (
+    "CALENDAR_HOME_RESOURCE_ID"
+);
+
+create index ATTACHMENT_DROPBOX_ID_5073cf23 on ATTACHMENT (
+    "DROPBOX_ID"
+);
+
+create index ATTACHMENT_CALENDAR_O_81508484 on ATTACHMENT_CALENDAR_OBJECT (
+    "CALENDAR_OBJECT_RESOURCE_ID"
+);
+
+create index ATTACHMENT_MIGRATION__804bf85e on ATTACHMENT_MIGRATION (
+    "CALENDAR_HOME_RESOURCE_ID",
+    "LOCAL_RESOURCE_ID"
+);
+
+create index ATTACHMENT_MIGRATION__816947fe on ATTACHMENT_MIGRATION (
+    "LOCAL_RESOURCE_ID"
+);
+
+create index SHARED_ADDRESSBOOK_BI_e9a2e6d4 on SHARED_ADDRESSBOOK_BIND (
+    "OWNER_HOME_RESOURCE_ID"
+);
+
+create index ABO_MEMBERS_ADDRESSBO_4effa879 on ABO_MEMBERS (
+    "ADDRESSBOOK_ID"
+);
+
+create index ABO_MEMBERS_MEMBER_ID_8d66adcf on ABO_MEMBERS (
+    "MEMBER_ID"
+);
+
+create index ABO_FOREIGN_MEMBERS_A_1fd2c5e9 on ABO_FOREIGN_MEMBERS (
+    "ADDRESSBOOK_ID"
+);
+
+create index SHARED_GROUP_BIND_RES_cf52f95d on SHARED_GROUP_BIND (
+    "GROUP_RESOURCE_ID"
+);
+
+create index CALENDAR_OBJECT_REVIS_6d9d929c on CALENDAR_OBJECT_REVISIONS (
+    "CALENDAR_RESOURCE_ID",
+    "RESOURCE_NAME",
+    "DELETED",
+    "REVISION"
+);
+
+create index CALENDAR_OBJECT_REVIS_265c8acf on CALENDAR_OBJECT_REVISIONS (
+    "CALENDAR_RESOURCE_ID",
+    "REVISION"
+);
+
+create index CALENDAR_OBJECT_REVIS_550b1c56 on CALENDAR_OBJECT_REVISIONS (
+    "CALENDAR_HOME_RESOURCE_ID",
+    "REVISION"
+);
+
+create index ADDRESSBOOK_OBJECT_RE_00fe8288 on ADDRESSBOOK_OBJECT_REVISIONS (
+    "OWNER_HOME_RESOURCE_ID",
+    "RESOURCE_NAME",
+    "DELETED",
+    "REVISION"
+);
+
+create index ADDRESSBOOK_OBJECT_RE_45004780 on ADDRESSBOOK_OBJECT_REVISIONS (
+    "OWNER_HOME_RESOURCE_ID",
+    "REVISION"
+);
+
+create index NOTIFICATION_OBJECT_R_036a9cee on NOTIFICATION_OBJECT_REVISIONS (
+    "NOTIFICATION_HOME_RESOURCE_ID",
+    "REVISION"
+);
+
+create index APN_SUBSCRIPTIONS_RES_9610d78e on APN_SUBSCRIPTIONS (
+    "RESOURCE_KEY"
+);
+
+create index IMIP_TOKENS_TOKEN_e94b918f on IMIP_TOKENS (
+    "TOKEN"
+);
+
+create index TEST_WORK_JOB_ID_228ede32 on TEST_WORK (
+    "JOB_ID"
+);
+
+create index IMIP_INVITATION_WORK__586d064c on IMIP_INVITATION_WORK (
+    "JOB_ID"
+);
+
+create index IMIP_POLLING_WORK_JOB_d5535891 on IMIP_POLLING_WORK (
+    "JOB_ID"
+);
+
+create index IMIP_REPLY_WORK_JOB_I_bf4ae73e on IMIP_REPLY_WORK (
+    "JOB_ID"
+);
+
+create index PUSH_NOTIFICATION_WOR_8bbab117 on PUSH_NOTIFICATION_WORK (
+    "JOB_ID"
+);
+
+create index PUSH_NOTIFICATION_WOR_3a3ee588 on PUSH_NOTIFICATION_WORK (
+    "PUSH_ID"
+);
+
+create index GROUP_CACHER_POLLING__6eb3151c on GROUP_CACHER_POLLING_WORK (
+    "JOB_ID"
+);
+
+create index GROUP_REFRESH_WORK_JO_717ede20 on GROUP_REFRESH_WORK (
+    "JOB_ID"
+);
+
+create index GROUP_REFRESH_WORK_GR_0325f3a8 on GROUP_REFRESH_WORK (
+    "GROUP_UID"
+);
+
+create index GROUP_DELEGATE_CHANGE_8bf9e6d8 on GROUP_DELEGATE_CHANGES_WORK (
+    "JOB_ID"
+);
+
+create index GROUP_DELEGATE_CHANGE_d8f7af69 on GROUP_DELEGATE_CHANGES_WORK (
+    "DELEGATOR_UID"
+);
+
+create index GROUP_MEMBERSHIP_MEMB_0ca508e8 on GROUP_MEMBERSHIP (
+    "MEMBER_UID"
+);
+
+create index GROUP_ATTENDEE_RECONC_da73d3c2 on GROUP_ATTENDEE_RECONCILE_WORK (
+    "JOB_ID"
+);
+
+create index GROUP_ATTENDEE_RECONC_b894ee7a on GROUP_ATTENDEE_RECONCILE_WORK (
+    "RESOURCE_ID"
+);
+
+create index GROUP_ATTENDEE_RECONC_5eabc549 on GROUP_ATTENDEE_RECONCILE_WORK (
+    "GROUP_ID"
+);
+
+create index GROUP_ATTENDEE_RESOUR_855124dc on GROUP_ATTENDEE (
+    "RESOURCE_ID"
+);
+
+create index GROUP_SHAREE_RECONCIL_9aad0858 on GROUP_SHAREE_RECONCILE_WORK (
+    "JOB_ID"
+);
+
+create index GROUP_SHAREE_RECONCIL_4dc60f78 on GROUP_SHAREE_RECONCILE_WORK (
+    "CALENDAR_ID"
+);
+
+create index GROUP_SHAREE_RECONCIL_1d14c921 on GROUP_SHAREE_RECONCILE_WORK (
+    "GROUP_ID"
+);
+
+create index GROUP_SHAREE_CALENDAR_28a88850 on GROUP_SHAREE (
+    "CALENDAR_ID"
+);
+
+create index DELEGATE_TO_DELEGATOR_5e149b11 on DELEGATES (
+    "DELEGATE",
+    "READ_WRITE",
+    "DELEGATOR"
+);
+
+create index DELEGATE_GROUPS_GROUP_25117446 on DELEGATE_GROUPS (
+    "GROUP_ID"
+);
+
+create index CALENDAR_OBJECT_SPLIT_af71dcda on CALENDAR_OBJECT_SPLITTER_WORK (
+    "RESOURCE_ID"
+);
+
+create index CALENDAR_OBJECT_SPLIT_33603b72 on CALENDAR_OBJECT_SPLITTER_WORK (
+    "JOB_ID"
+);
+
+create index CALENDAR_OBJECT_UPGRA_a5c181eb on CALENDAR_OBJECT_UPGRADE_WORK (
+    "RESOURCE_ID"
+);
+
+create index CALENDAR_OBJECT_UPGRA_39d6f8f9 on CALENDAR_OBJECT_UPGRADE_WORK (
+    "JOB_ID"
+);
+
+create index FIND_MIN_VALID_REVISI_78d17400 on FIND_MIN_VALID_REVISION_WORK (
+    "JOB_ID"
+);
+
+create index REVISION_CLEANUP_WORK_eb062686 on REVISION_CLEANUP_WORK (
+    "JOB_ID"
+);
+
+create index INBOX_CLEANUP_WORK_JO_799132bd on INBOX_CLEANUP_WORK (
+    "JOB_ID"
+);
+
+create index CLEANUP_ONE_INBOX_WOR_375dac36 on CLEANUP_ONE_INBOX_WORK (
+    "JOB_ID"
+);
+
+create index SCHEDULE_WORK_JOB_ID_65e810ee on SCHEDULE_WORK (
+    "JOB_ID"
+);
+
+create index SCHEDULE_WORK_ICALEND_089f33dc on SCHEDULE_WORK (
+    "ICALENDAR_UID"
+);
+
+create index SCHEDULE_REFRESH_WORK_26084c7b on SCHEDULE_REFRESH_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index SCHEDULE_REFRESH_WORK_989efe54 on SCHEDULE_REFRESH_WORK (
+    "RESOURCE_ID"
+);
+
+create index SCHEDULE_AUTO_REPLY_W_0256478d on SCHEDULE_AUTO_REPLY_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index SCHEDULE_AUTO_REPLY_W_0755e754 on SCHEDULE_AUTO_REPLY_WORK (
+    "RESOURCE_ID"
+);
+
+create index SCHEDULE_ORGANIZER_WO_18ce4edd on SCHEDULE_ORGANIZER_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index SCHEDULE_ORGANIZER_WO_14702035 on SCHEDULE_ORGANIZER_WORK (
+    "RESOURCE_ID"
+);
+
+create index SCHEDULE_ORGANIZER_SE_9ec9f827 on SCHEDULE_ORGANIZER_SEND_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index SCHEDULE_ORGANIZER_SE_699fefc4 on SCHEDULE_ORGANIZER_SEND_WORK (
+    "RESOURCE_ID"
+);
+
+create index SCHEDULE_REPLY_WORK_H_745af8cf on SCHEDULE_REPLY_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index SCHEDULE_REPLY_WORK_R_11bd3fbb on SCHEDULE_REPLY_WORK (
+    "RESOURCE_ID"
+);
+
+create index PRINCIPAL_PURGE_POLLI_6383e68a on PRINCIPAL_PURGE_POLLING_WORK (
+    "JOB_ID"
+);
+
+create index PRINCIPAL_PURGE_CHECK_b0c024c1 on PRINCIPAL_PURGE_CHECK_WORK (
+    "JOB_ID"
+);
+
+create index PRINCIPAL_PURGE_CHECK_198388a5 on PRINCIPAL_PURGE_CHECK_WORK (
+    "UID"
+);
+
+create index PRINCIPAL_PURGE_WORK__7a8141a3 on PRINCIPAL_PURGE_WORK (
+    "JOB_ID"
+);
+
+create index PRINCIPAL_PURGE_WORK__db35cfdc on PRINCIPAL_PURGE_WORK (
+    "UID"
+);
+
+create index PRINCIPAL_PURGE_HOME__f35eea7a on PRINCIPAL_PURGE_HOME_WORK (
+    "JOB_ID"
+);
+
+create index PRINCIPAL_PURGE_HOME__967e4480 on PRINCIPAL_PURGE_HOME_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index MIGRATION_CLEANUP_WOR_8c23cc35 on MIGRATION_CLEANUP_WORK (
+    "JOB_ID"
+);
+
+create index MIGRATION_CLEANUP_WOR_86181cb8 on MIGRATION_CLEANUP_WORK (
+    "HOME_RESOURCE_ID"
+);
+
+create index HOME_CLEANUP_WORK_JOB_9631dfb0 on HOME_CLEANUP_WORK (
+    "JOB_ID"
+);
+
+create index MIGRATED_HOME_CLEANUP_4c714fd4 on MIGRATED_HOME_CLEANUP_WORK (
+    "JOB_ID"
+);
+
+-- Extra schema to add to current-oracle-dialect.sql

Copied: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/postgres-dialect/v55.sql (from rev 15009, CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v55.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/postgres-dialect/v55.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/postgres-dialect/v55.sql	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,1276 @@
+-- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+
+----
+-- Copyright (c) 2010-2015 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
+);
+
+
+--------------------
+-- Jobs           --
+--------------------
+
+create sequence JOB_SEQ;
+
+create table JOB (
+  JOB_ID      integer primary key default nextval('JOB_SEQ'), --implicit index
+  WORK_TYPE   varchar(255) not null,
+  PRIORITY    integer default 0,
+  WEIGHT      integer default 0,
+  NOT_BEFORE  timestamp not null,
+  ASSIGNED    timestamp default null,
+  OVERDUE     timestamp default null,
+  FAILED      integer default 0,
+  PAUSE       integer default 0
+);
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null,                		                -- implicit index
+  STATUS           integer      default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION      integer      default 0 not null,
+  
+  unique (OWNER_UID, STATUS)	-- implicit index
+);
+
+-- Enumeration of statuses
+
+create table HOME_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into HOME_STATUS values (0, 'normal' );
+insert into HOME_STATUS values (1, 'external');
+insert into HOME_STATUS values (2, 'purging');
+insert into HOME_STATUS values (3, 'migrating');
+insert into HOME_STATUS values (4, 'disabled');
+
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+
+----------------------------
+-- Calendar Home Metadata --
+----------------------------
+
+create table CALENDAR_HOME_METADATA (
+  RESOURCE_ID              integer     primary key references CALENDAR_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES         integer     default 0 not null,
+  TRASH                    integer     default null references CALENDAR on delete set null,
+  DEFAULT_EVENTS           integer     default null references CALENDAR on delete set null,
+  DEFAULT_TASKS            integer     default null references CALENDAR on delete set null,
+  DEFAULT_POLLS            integer     default null references CALENDAR on delete set null,
+  ALARM_VEVENT_TIMED       text        default null,
+  ALARM_VEVENT_ALLDAY      text        default null,
+  ALARM_VTODO_TIMED        text        default null,
+  ALARM_VTODO_ALLDAY       text        default null,
+  AVAILABILITY             text        default null,
+  CREATED                  timestamp   default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                 timestamp   default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+create index CALENDAR_HOME_METADATA_TRASH on
+  CALENDAR_HOME_METADATA(TRASH);
+create index CALENDAR_HOME_METADATA_DEFAULT_EVENTS on
+  CALENDAR_HOME_METADATA(DEFAULT_EVENTS);
+create index CALENDAR_HOME_METADATA_DEFAULT_TASKS on
+  CALENDAR_HOME_METADATA(DEFAULT_TASKS);
+create index CALENDAR_HOME_METADATA_DEFAULT_POLLS on
+  CALENDAR_HOME_METADATA(DEFAULT_POLLS);
+
+
+-----------------------
+-- Calendar Metadata --
+-----------------------
+
+create table CALENDAR_METADATA (
+  RESOURCE_ID           integer      primary key references CALENDAR on delete cascade, -- implicit index
+  SUPPORTED_COMPONENTS  varchar(255) default null,
+  CHILD_TYPE            integer      default 0 not null,                             	-- enum CHILD_TYPE
+  TRASHED               timestamp    default null,
+  IS_IN_TRASH           boolean      default false not null, -- collection is in the trash
+  CREATED               timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+-- Enumeration of child type
+
+create table CHILD_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CHILD_TYPE values (0, 'normal');
+insert into CHILD_TYPE values (1, 'inbox');
+insert into CHILD_TYPE values (2, 'trash');
+
+
+------------------------
+-- Calendar Migration --
+------------------------
+
+create table CALENDAR_MIGRATION (
+  CALENDAR_HOME_RESOURCE_ID		integer references CALENDAR_HOME on delete cascade,
+  REMOTE_RESOURCE_ID			integer not null,
+  LOCAL_RESOURCE_ID				integer	references CALENDAR on delete cascade,
+  LAST_SYNC_TOKEN				varchar(255),
+   
+  primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
+);
+
+create index CALENDAR_MIGRATION_LOCAL_RESOURCE_ID on
+  CALENDAR_MIGRATION(LOCAL_RESOURCE_ID);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID   varchar(255) not null,	                                   -- implicit index
+  STATUS      integer      default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION integer      default 0 not null,
+    
+  unique (OWNER_UID, STATUS)	-- 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,
+  NOTIFICATION_TYPE             varchar(255) not null,
+  NOTIFICATION_DATA             text         not null,
+  MD5                           char(32)     not null,
+  CREATED                       timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID) -- implicit index
+);
+
+create index NOTIFICATION_NOTIFICATION_HOME_RESOURCE_ID on
+  NOTIFICATION(NOTIFICATION_HOME_RESOURCE_ID);
+
+
+-------------------
+-- Calendar Bind --
+-------------------
+
+-- Joins CALENDAR_HOME and CALENDAR
+
+create table CALENDAR_BIND (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      not null references CALENDAR on delete cascade,
+  CALENDAR_RESOURCE_NAME    varchar(255) not null,
+  BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION             integer      default 0 not null,
+  BIND_UID                  varchar(36)  default null,
+  MESSAGE                   text,
+  TRANSP                    integer      default 0 not null, -- enum CALENDAR_TRANSP
+  ALARM_VEVENT_TIMED        text         default null,
+  ALARM_VEVENT_ALLDAY       text         default null,
+  ALARM_VTODO_TIMED         text         default null,
+  ALARM_VTODO_ALLDAY        text         default null,
+  TIMEZONE                  text         default null,
+
+  primary key (CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID), -- implicit index
+  unique (CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)     -- implicit index
+);
+
+create index CALENDAR_BIND_RESOURCE_ID on
+  CALENDAR_BIND(CALENDAR_RESOURCE_ID);
+
+-- Enumeration of calendar bind modes
+
+create table CALENDAR_BIND_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_MODE values (0, 'own'  );
+insert into CALENDAR_BIND_MODE values (1, 'read' );
+insert into CALENDAR_BIND_MODE values (2, 'write');
+insert into CALENDAR_BIND_MODE values (3, 'direct');
+insert into CALENDAR_BIND_MODE values (4, 'indirect');
+insert into CALENDAR_BIND_MODE values (5, 'group');
+insert into CALENDAR_BIND_MODE values (6, 'group_read');
+insert into CALENDAR_BIND_MODE values (7, 'group_write');
+
+-- Enumeration of statuses
+
+create table CALENDAR_BIND_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_STATUS values (0, 'invited' );
+insert into CALENDAR_BIND_STATUS values (1, 'accepted');
+insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
+insert into CALENDAR_BIND_STATUS values (4, 'deleted');
+
+
+-- Enumeration of transparency
+
+create table CALENDAR_TRANSP (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_TRANSP values (0, 'opaque' );
+insert into CALENDAR_TRANSP values (1, 'transparent');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+  RESOURCE_ID          integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID integer      not null references CALENDAR on delete cascade,
+  RESOURCE_NAME        varchar(255) not null,
+  ICALENDAR_TEXT       text         not null,
+  ICALENDAR_UID        varchar(255) not null,
+  ICALENDAR_TYPE       varchar(255) not null,
+  ATTACHMENTS_MODE     integer      default 0 not null, -- enum CALENDAR_OBJ_ATTACHMENTS_MODE
+  DROPBOX_ID           varchar(255),
+  ORGANIZER            varchar(255),
+  RECURRANCE_MIN       date,        -- minimum date that recurrences have been expanded to.
+  RECURRANCE_MAX       date,        -- maximum date that recurrences have been expanded to.
+  ACCESS               integer      default 0 not null,
+  SCHEDULE_OBJECT      boolean      default false,
+  SCHEDULE_TAG         varchar(36)  default null,
+  SCHEDULE_ETAGS       text         default null,
+  PRIVATE_COMMENTS     boolean      default false not null,
+  MD5                  char(32)     not null,
+  TRASHED              timestamp    default null,
+  ORIGINAL_COLLECTION  integer      default null, -- calendar_resource_id prior to trash
+  CREATED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED             timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  DATAVERSION          integer      default 0 not null,
+
+  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_MIN on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, RECURRANCE_MAX, RECURRANCE_MIN);
+
+create index CALENDAR_OBJECT_ICALENDAR_UID on
+  CALENDAR_OBJECT(ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_DROPBOX_ID on
+  CALENDAR_OBJECT(DROPBOX_ID);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJ_ATTACHMENTS_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (0, 'none' );
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (1, 'read' );
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (2, 'write');
+
+
+-- Enumeration of calendar access types
+
+create table CALENDAR_ACCESS_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(32) not null unique
+);
+
+insert into CALENDAR_ACCESS_TYPE values (0, ''             );
+insert into CALENDAR_ACCESS_TYPE values (1, 'public'       );
+insert into CALENDAR_ACCESS_TYPE values (2, 'private'      );
+insert into CALENDAR_ACCESS_TYPE values (3, 'confidential' );
+insert into CALENDAR_ACCESS_TYPE values (4, 'restricted'   );
+
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+  INSTANCE_ID                 integer        primary key default nextval('INSTANCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID        integer        not null references CALENDAR on delete cascade,
+  CALENDAR_OBJECT_RESOURCE_ID integer        not null references CALENDAR_OBJECT on delete cascade,
+  FLOATING                    boolean        not null,
+  START_DATE                  timestamp      not null,
+  END_DATE                    timestamp      not null,
+  FBTYPE                      integer        not null,
+  TRANSPARENT                 boolean        not null
+);
+
+create index TIME_RANGE_CALENDAR_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_RESOURCE_ID);
+create index TIME_RANGE_CALENDAR_OBJECT_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_OBJECT_RESOURCE_ID);
+
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown'         );
+insert into FREE_BUSY_TYPE values (1, 'free'            );
+insert into FREE_BUSY_TYPE values (2, 'busy'            );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative'  );
+
+
+-------------------
+-- Per-user data --
+-------------------
+
+create table PERUSER (
+  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
+  USER_ID                     varchar(255) not null,
+  TRANSPARENT                 boolean      not null,
+  ADJUSTED_START_DATE         timestamp    default null,
+  ADJUSTED_END_DATE           timestamp    default null,
+
+  primary key (TIME_RANGE_INSTANCE_ID, USER_ID)    -- implicit index
+);
+
+
+-------------------------------
+-- Calendar Object Migration --
+-------------------------------
+
+create table CALENDAR_OBJECT_MIGRATION (
+  CALENDAR_HOME_RESOURCE_ID		integer references CALENDAR_HOME on delete cascade,
+  REMOTE_RESOURCE_ID			integer not null,
+  LOCAL_RESOURCE_ID				integer	references CALENDAR_OBJECT on delete cascade,
+   
+  primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
+);
+
+create index CALENDAR_OBJECT_MIGRATION_HOME_LOCAL on
+  CALENDAR_OBJECT_MIGRATION(CALENDAR_HOME_RESOURCE_ID, LOCAL_RESOURCE_ID);
+create index CALENDAR_OBJECT_MIGRATION_LOCAL_RESOURCE_ID on
+  CALENDAR_OBJECT_MIGRATION(LOCAL_RESOURCE_ID);
+
+
+----------------
+-- Attachment --
+----------------
+
+create sequence ATTACHMENT_ID_SEQ;
+
+create table ATTACHMENT (
+  ATTACHMENT_ID               integer           primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
+  CALENDAR_HOME_RESOURCE_ID   integer           not null references CALENDAR_HOME,
+  DROPBOX_ID                  varchar(255),
+  CONTENT_TYPE                varchar(255)      not null,
+  SIZE                        integer           not null,
+  MD5                         char(32)          not null,
+  CREATED                     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                    timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  PATH                        varchar(1024)     not null
+);
+
+create index ATTACHMENT_CALENDAR_HOME_RESOURCE_ID on
+  ATTACHMENT(CALENDAR_HOME_RESOURCE_ID);
+
+create index ATTACHMENT_DROPBOX_ID on
+  ATTACHMENT(DROPBOX_ID);
+
+-- Many-to-many relationship between attachments and calendar objects
+create table ATTACHMENT_CALENDAR_OBJECT (
+  ATTACHMENT_ID                  integer      not null references ATTACHMENT on delete cascade,
+  MANAGED_ID                     varchar(255) not null,
+  CALENDAR_OBJECT_RESOURCE_ID    integer      not null references CALENDAR_OBJECT on delete cascade,
+
+  primary key (ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID), -- implicit index
+  unique (MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
+);
+
+create index ATTACHMENT_CALENDAR_OBJECT_CALENDAR_OBJECT_RESOURCE_ID on
+  ATTACHMENT_CALENDAR_OBJECT(CALENDAR_OBJECT_RESOURCE_ID);
+
+-----------------------------------
+-- Calendar Attachment Migration --
+-----------------------------------
+
+create table ATTACHMENT_MIGRATION (
+  CALENDAR_HOME_RESOURCE_ID		integer references CALENDAR_HOME on delete cascade,
+  REMOTE_RESOURCE_ID			integer not null,
+  LOCAL_RESOURCE_ID				integer	references ATTACHMENT on delete cascade,
+   
+  primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
+);
+
+create index ATTACHMENT_MIGRATION_HOME_LOCAL on
+  ATTACHMENT_MIGRATION(CALENDAR_HOME_RESOURCE_ID, LOCAL_RESOURCE_ID);
+create index ATTACHMENT_MIGRATION_LOCAL_RESOURCE_ID on
+  ATTACHMENT_MIGRATION(LOCAL_RESOURCE_ID);
+
+
+-----------------------
+-- Resource Property --
+-----------------------
+
+create table RESOURCE_PROPERTY (
+  RESOURCE_ID integer      not null, -- foreign key: *.RESOURCE_ID
+  NAME        varchar(255) not null,
+  VALUE       text         not null, -- FIXME: xml?
+  VIEWER_UID  varchar(255),
+
+  primary key (RESOURCE_ID, NAME, VIEWER_UID) -- implicit index
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+  RESOURCE_ID                   integer         primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  ADDRESSBOOK_PROPERTY_STORE_ID integer         default nextval('RESOURCE_ID_SEQ') not null,    -- implicit index
+  OWNER_UID                     varchar(255)    not null,
+  STATUS                        integer         default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION                   integer         default 0 not null,
+    
+  unique (OWNER_UID, STATUS)	-- implicit index
+);
+
+
+-------------------------------
+-- AddressBook Home Metadata --
+-------------------------------
+
+create table ADDRESSBOOK_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references ADDRESSBOOK_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+-----------------------------
+-- Shared AddressBook Bind --
+-----------------------------
+
+-- Joins sharee ADDRESSBOOK_HOME and owner ADDRESSBOOK_HOME
+
+create table SHARED_ADDRESSBOOK_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID          integer         not null references ADDRESSBOOK_HOME,
+  OWNER_HOME_RESOURCE_ID                integer         not null references ADDRESSBOOK_HOME on delete cascade,
+  ADDRESSBOOK_RESOURCE_NAME             varchar(255)    not null,
+  BIND_MODE                             integer         not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                           integer         not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION                         integer         default 0 not null,
+  BIND_UID                              varchar(36)     default null,
+  MESSAGE                               text,                     -- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)     -- implicit index
+);
+
+create index SHARED_ADDRESSBOOK_BIND_RESOURCE_ID on
+  SHARED_ADDRESSBOOK_BIND(OWNER_HOME_RESOURCE_ID);
+
+
+------------------------
+-- AddressBook Object --
+------------------------
+
+create table ADDRESSBOOK_OBJECT (
+  RESOURCE_ID                   integer         primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  ADDRESSBOOK_HOME_RESOURCE_ID  integer         not null references ADDRESSBOOK_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255)    not null,
+  VCARD_TEXT                    text            not null,
+  VCARD_UID                     varchar(255)    not null,
+  KIND                          integer         not null,  -- enum ADDRESSBOOK_OBJECT_KIND
+  MD5                           char(32)        not null,
+  TRASHED                       timestamp       default null,
+  IS_IN_TRASH                   boolean         default false not null,
+  CREATED                       timestamp       default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp       default timezone('UTC', CURRENT_TIMESTAMP),
+  DATAVERSION                   integer         default 0 not null,
+
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, RESOURCE_NAME), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, VCARD_UID)      -- implicit index
+);
+
+
+-----------------------------
+-- AddressBook Object kind --
+-----------------------------
+
+create table ADDRESSBOOK_OBJECT_KIND (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND values (0, 'person');
+insert into ADDRESSBOOK_OBJECT_KIND values (1, 'group' );
+insert into ADDRESSBOOK_OBJECT_KIND values (2, 'resource');
+insert into ADDRESSBOOK_OBJECT_KIND values (3, 'location');
+
+
+----------------------------------
+-- Revisions, forward reference --
+----------------------------------
+
+create sequence REVISION_SEQ;
+
+---------------------------------
+-- Address Book Object Members --
+---------------------------------
+
+create table ABO_MEMBERS (
+  GROUP_ID        integer     not null, -- references ADDRESSBOOK_OBJECT on delete cascade,   -- AddressBook Object's (kind=='group') RESOURCE_ID
+  ADDRESSBOOK_ID  integer     not null references ADDRESSBOOK_HOME on delete cascade,
+  MEMBER_ID       integer     not null, -- references ADDRESSBOOK_OBJECT,                     -- member AddressBook Object's RESOURCE_ID
+  REVISION        integer     default nextval('REVISION_SEQ') not null,
+  REMOVED         boolean     default false not null,
+  MODIFIED        timestamp   default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (GROUP_ID, MEMBER_ID, REVISION) -- implicit index
+);
+
+create index ABO_MEMBERS_ADDRESSBOOK_ID on
+  ABO_MEMBERS(ADDRESSBOOK_ID);
+create index ABO_MEMBERS_MEMBER_ID on
+  ABO_MEMBERS(MEMBER_ID);
+
+------------------------------------------
+-- Address Book Object Foreign Members  --
+------------------------------------------
+
+create table ABO_FOREIGN_MEMBERS (
+  GROUP_ID           integer      not null references ADDRESSBOOK_OBJECT on delete cascade,  -- AddressBook Object's (kind=='group') RESOURCE_ID
+  ADDRESSBOOK_ID     integer      not null references ADDRESSBOOK_HOME on delete cascade,
+  MEMBER_ADDRESS     varchar(255) not null,                                                  -- member AddressBook Object's 'calendar' address
+
+  primary key (GROUP_ID, MEMBER_ADDRESS) -- implicit index
+);
+
+create index ABO_FOREIGN_MEMBERS_ADDRESSBOOK_ID on
+  ABO_FOREIGN_MEMBERS(ADDRESSBOOK_ID);
+
+-----------------------
+-- Shared Group Bind --
+-----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK_OBJECT (kind == group)
+
+create table SHARED_GROUP_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID      integer      not null references ADDRESSBOOK_HOME,
+  GROUP_RESOURCE_ID                 integer      not null references ADDRESSBOOK_OBJECT on delete cascade,
+  GROUP_ADDRESSBOOK_NAME            varchar(255) not null,
+  BIND_MODE                         integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                       integer      not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION                     integer      default 0 not null,
+  BIND_UID                          varchar(36)  default null,
+  MESSAGE                           text,                  -- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_ADDRESSBOOK_NAME)  -- implicit index
+);
+
+create index SHARED_GROUP_BIND_RESOURCE_ID on
+  SHARED_GROUP_BIND(GROUP_RESOURCE_ID);
+
+
+---------------
+-- Revisions --
+---------------
+
+-- create sequence REVISION_SEQ;
+
+
+-------------------------------
+-- Calendar Object Revisions --
+-------------------------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      references CALENDAR,
+  CALENDAR_NAME             varchar(255) default null,
+  RESOURCE_NAME             varchar(255),
+  REVISION                  integer      default nextval('REVISION_SEQ') not null,
+  DELETED                   boolean      not null,
+  MODIFIED                  timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID, CALENDAR_NAME, RESOURCE_NAME)    -- implicit index
+);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, REVISION);
+
+create index CALENDAR_OBJECT_REVISIONS_HOME_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_HOME_RESOURCE_ID, REVISION);
+
+
+----------------------------------
+-- AddressBook Object Revisions --
+----------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+  ADDRESSBOOK_HOME_RESOURCE_ID  integer      not null references ADDRESSBOOK_HOME,
+  OWNER_HOME_RESOURCE_ID        integer      references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_NAME              varchar(255) default null,
+  OBJECT_RESOURCE_ID            integer      default 0,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID, ADDRESSBOOK_NAME, RESOURCE_NAME)    -- implicit index
+);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, REVISION);
+
+
+-----------------------------------
+-- Notification Object Revisions --
+-----------------------------------
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (NOTIFICATION_HOME_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+);
+
+create index NOTIFICATION_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on NOTIFICATION_OBJECT_REVISIONS(NOTIFICATION_HOME_RESOURCE_ID, REVISION);
+
+
+-------------------------------------------
+-- Apple Push Notification Subscriptions --
+-------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer      not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  USER_AGENT                    varchar(255) default null,
+  IP_ADDR                       varchar(255) default null,
+
+  primary key (TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+  on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+
+-----------------
+-- IMIP Tokens --
+-----------------
+
+create table IMIP_TOKENS (
+  TOKEN                         varchar(255) not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALUID                       varchar(255) not null,
+  ACCESSED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (ORGANIZER, ATTENDEE, ICALUID) -- implicit index
+);
+
+create index IMIP_TOKENS_TOKEN
+  on IMIP_TOKENS(TOKEN);
+
+
+----------------
+-- Work Items --
+----------------
+
+create sequence WORKITEM_SEQ;
+
+
+---------------------------
+-- IMIP Inivitation Work --
+---------------------------
+
+create table IMIP_INVITATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  FROM_ADDR                     varchar(255) not null,
+  TO_ADDR                       varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+create index IMIP_INVITATION_WORK_JOB_ID on
+  IMIP_INVITATION_WORK(JOB_ID);
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+create table IMIP_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index IMIP_POLLING_WORK_JOB_ID on
+  IMIP_POLLING_WORK(JOB_ID);
+
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+create index IMIP_REPLY_WORK_JOB_ID on
+  IMIP_REPLY_WORK(JOB_ID);
+
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  PUSH_ID                       varchar(255) not null,
+  PUSH_PRIORITY                 integer      not null -- 1:low 5:medium 10:high
+);
+
+create index PUSH_NOTIFICATION_WORK_JOB_ID on
+  PUSH_NOTIFICATION_WORK(JOB_ID);
+create index PUSH_NOTIFICATION_WORK_PUSH_ID on
+  PUSH_NOTIFICATION_WORK(PUSH_ID);
+
+-----------------
+-- GroupCacher --
+-----------------
+
+create table GROUP_CACHER_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index GROUP_CACHER_POLLING_WORK_JOB_ID on
+  GROUP_CACHER_POLLING_WORK(JOB_ID);
+
+create table GROUP_REFRESH_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  GROUP_UID                     varchar(255) not null
+);
+
+create index GROUP_REFRESH_WORK_JOB_ID on
+  GROUP_REFRESH_WORK(JOB_ID);
+create index GROUP_REFRESH_WORK_GROUP_UID on
+  GROUP_REFRESH_WORK(GROUP_UID);
+
+create table GROUP_DELEGATE_CHANGES_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  DELEGATOR_UID                 varchar(255) not null,
+  READ_DELEGATE_UID             varchar(255) not null,
+  WRITE_DELEGATE_UID            varchar(255) not null
+);
+
+create index GROUP_DELEGATE_CHANGES_WORK_JOB_ID on
+  GROUP_DELEGATE_CHANGES_WORK(JOB_ID);
+create index GROUP_DELEGATE_CHANGES_WORK_DELEGATOR_UID on
+  GROUP_DELEGATE_CHANGES_WORK(DELEGATOR_UID);
+
+create table GROUPS (
+  GROUP_ID                      integer      primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  NAME                          varchar(255) not null,
+  GROUP_UID                     varchar(255) not null unique,									-- implicit index
+  MEMBERSHIP_HASH               varchar(255) not null,
+  EXTANT                        integer default 1,
+  CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+create table GROUP_MEMBERSHIP (
+  GROUP_ID                     integer not null references GROUPS on delete cascade,
+  MEMBER_UID                   varchar(255) not null,
+
+  primary key (GROUP_ID, MEMBER_UID)
+);
+
+create index GROUP_MEMBERSHIP_MEMBER on
+  GROUP_MEMBERSHIP(MEMBER_UID);
+
+create table GROUP_ATTENDEE_RECONCILE_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer not null references JOB,
+  RESOURCE_ID                   integer not null references CALENDAR_OBJECT on delete cascade,
+  GROUP_ID                      integer not null references GROUPS on delete cascade
+);
+
+create index GROUP_ATTENDEE_RECONCILE_WORK_JOB_ID on
+  GROUP_ATTENDEE_RECONCILE_WORK(JOB_ID);
+create index GROUP_ATTENDEE_RECONCILE_WORK_RESOURCE_ID on
+  GROUP_ATTENDEE_RECONCILE_WORK(RESOURCE_ID);
+create index GROUP_ATTENDEE_RECONCILE_WORK_GROUP_ID on
+  GROUP_ATTENDEE_RECONCILE_WORK(GROUP_ID);
+
+
+create table GROUP_ATTENDEE (
+  GROUP_ID                      integer not null references GROUPS on delete cascade,
+  RESOURCE_ID                   integer not null references CALENDAR_OBJECT on delete cascade,
+  MEMBERSHIP_HASH               varchar(255) not null,
+
+  primary key (GROUP_ID, RESOURCE_ID)
+);
+
+create index GROUP_ATTENDEE_RESOURCE_ID on
+  GROUP_ATTENDEE(RESOURCE_ID);
+
+
+create table GROUP_SHAREE_RECONCILE_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer not null references JOB,
+  CALENDAR_ID                   integer	not null references CALENDAR on delete cascade,
+  GROUP_ID                      integer not null references GROUPS on delete cascade
+);
+
+create index GROUP_SHAREE_RECONCILE_WORK_JOB_ID on
+  GROUP_SHAREE_RECONCILE_WORK(JOB_ID);
+create index GROUP_SHAREE_RECONCILE_WORK_CALENDAR_ID on
+  GROUP_SHAREE_RECONCILE_WORK(CALENDAR_ID);
+create index GROUP_SHAREE_RECONCILE_WORK_GROUP_ID on
+  GROUP_SHAREE_RECONCILE_WORK(GROUP_ID);
+
+
+create table GROUP_SHAREE (
+  GROUP_ID                      integer not null references GROUPS on delete cascade,
+  CALENDAR_ID      				integer not null references CALENDAR on delete cascade,
+  GROUP_BIND_MODE               integer not null, -- enum CALENDAR_BIND_MODE
+  MEMBERSHIP_HASH               varchar(255) not null,
+
+  primary key (GROUP_ID, CALENDAR_ID)
+);
+
+create index GROUP_SHAREE_CALENDAR_ID on
+  GROUP_SHAREE(CALENDAR_ID);
+
+---------------
+-- Delegates --
+---------------
+
+create table DELEGATES (
+  DELEGATOR                     varchar(255) not null,
+  DELEGATE                      varchar(255) not null,
+  READ_WRITE                    integer      not null, -- 1 = ReadWrite, 0 = ReadOnly
+
+  primary key (DELEGATOR, READ_WRITE, DELEGATE)
+);
+create index DELEGATE_TO_DELEGATOR on
+  DELEGATES(DELEGATE, READ_WRITE, DELEGATOR);
+
+create table DELEGATE_GROUPS (
+  DELEGATOR                     varchar(255) not null,
+  GROUP_ID                      integer      not null references GROUPS on delete cascade,
+  READ_WRITE                    integer      not null, -- 1 = ReadWrite, 0 = ReadOnly
+  IS_EXTERNAL                   integer      not null, -- 1 = External, 0 = Internal
+
+  primary key (DELEGATOR, READ_WRITE, GROUP_ID)
+);
+create index DELEGATE_GROUPS_GROUP_ID on
+  DELEGATE_GROUPS(GROUP_ID);
+
+create table EXTERNAL_DELEGATE_GROUPS (
+  DELEGATOR                     varchar(255) primary key,
+  GROUP_UID_READ                varchar(255),
+  GROUP_UID_WRITE               varchar(255)
+);
+
+--------------------------
+-- Object Splitter Work --
+--------------------------
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade
+);
+
+create index CALENDAR_OBJECT_SPLITTER_WORK_RESOURCE_ID on
+  CALENDAR_OBJECT_SPLITTER_WORK(RESOURCE_ID);
+create index CALENDAR_OBJECT_SPLITTER_WORK_JOB_ID on
+  CALENDAR_OBJECT_SPLITTER_WORK(JOB_ID);
+
+-------------------------
+-- Object Upgrade Work --
+-------------------------
+
+create table CALENDAR_OBJECT_UPGRADE_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade
+);
+
+create index CALENDAR_OBJECT_UPGRADE_WORK_RESOURCE_ID on
+  CALENDAR_OBJECT_UPGRADE_WORK(RESOURCE_ID);
+create index CALENDAR_OBJECT_UPGRADE_WORK_JOB_ID on
+  CALENDAR_OBJECT_UPGRADE_WORK(JOB_ID);
+
+---------------------------
+-- Revision Cleanup Work --
+---------------------------
+
+create table FIND_MIN_VALID_REVISION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index FIND_MIN_VALID_REVISION_WORK_JOB_ID on
+  FIND_MIN_VALID_REVISION_WORK(JOB_ID);
+
+create table REVISION_CLEANUP_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index REVISION_CLEANUP_WORK_JOB_ID on
+  REVISION_CLEANUP_WORK(JOB_ID);
+
+------------------------
+-- Inbox Cleanup Work --
+------------------------
+
+create table INBOX_CLEANUP_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index INBOX_CLEANUP_WORK_JOB_ID on
+   INBOX_CLEANUP_WORK(JOB_ID);
+
+create table CLEANUP_ONE_INBOX_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_ID                       integer      not null unique references CALENDAR_HOME on delete cascade -- implicit index
+);
+
+create index CLEANUP_ONE_INBOX_WORK_JOB_ID on
+  CLEANUP_ONE_INBOX_WORK(JOB_ID);
+
+-------------------
+-- Schedule Work --
+-------------------
+
+create table SCHEDULE_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ICALENDAR_UID                 varchar(255) not null,
+  WORK_TYPE                     varchar(255) not null
+);
+
+create index SCHEDULE_WORK_JOB_ID on
+  SCHEDULE_WORK(JOB_ID);
+create index SCHEDULE_WORK_ICALENDAR_UID on
+  SCHEDULE_WORK(ICALENDAR_UID);
+
+---------------------------
+-- Schedule Refresh Work --
+---------------------------
+
+create table SCHEDULE_REFRESH_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  ATTENDEE_COUNT                integer
+);
+
+create index SCHEDULE_REFRESH_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_REFRESH_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_REFRESH_WORK_RESOURCE_ID on
+  SCHEDULE_REFRESH_WORK(RESOURCE_ID);
+
+create table SCHEDULE_REFRESH_ATTENDEES (
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  ATTENDEE                      varchar(255) not null,
+
+  primary key (RESOURCE_ID, ATTENDEE)
+);
+
+------------------------------
+-- Schedule Auto Reply Work --
+------------------------------
+
+create table SCHEDULE_AUTO_REPLY_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  PARTSTAT                      varchar(255) not null
+);
+
+create index SCHEDULE_AUTO_REPLY_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_AUTO_REPLY_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_AUTO_REPLY_WORK_RESOURCE_ID on
+  SCHEDULE_AUTO_REPLY_WORK(RESOURCE_ID);
+
+-----------------------------
+-- Schedule Organizer Work --
+-----------------------------
+
+create table SCHEDULE_ORGANIZER_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  SCHEDULE_ACTION               integer      not null, -- Enum SCHEDULE_ACTION
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer,     -- this references a possibly non-existent CALENDAR_OBJECT
+  ICALENDAR_TEXT_OLD            text,
+  ICALENDAR_TEXT_NEW            text,
+  ATTENDEE_COUNT                integer,
+  SMART_MERGE                   boolean
+);
+
+create index SCHEDULE_ORGANIZER_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_ORGANIZER_WORK_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_WORK(RESOURCE_ID);
+
+-- Enumeration of schedule actions
+
+create table SCHEDULE_ACTION (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into SCHEDULE_ACTION values (0, 'create');
+insert into SCHEDULE_ACTION values (1, 'modify');
+insert into SCHEDULE_ACTION values (2, 'modify-cancelled');
+insert into SCHEDULE_ACTION values (3, 'remove');
+
+----------------------------------
+-- Schedule Organizer Send Work --
+----------------------------------
+
+create table SCHEDULE_ORGANIZER_SEND_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  SCHEDULE_ACTION               integer      not null, -- Enum SCHEDULE_ACTION
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer,     -- this references a possibly non-existent CALENDAR_OBJECT
+  ATTENDEE                      varchar(255) not null,
+  ITIP_MSG                      text,
+  NO_REFRESH                    boolean
+);
+
+create index SCHEDULE_ORGANIZER_SEND_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_SEND_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_ORGANIZER_SEND_WORK_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_SEND_WORK(RESOURCE_ID);
+
+-------------------------
+-- Schedule Reply Work --
+-------------------------
+
+create table SCHEDULE_REPLY_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer,     -- this references a possibly non-existent CALENDAR_OBJECT
+  ITIP_MSG                      text
+);
+
+create index SCHEDULE_REPLY_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_REPLY_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_REPLY_WORK_RESOURCE_ID on
+  SCHEDULE_REPLY_WORK(RESOURCE_ID);
+
+----------------------------------
+-- Principal Purge Polling Work --
+----------------------------------
+
+create table PRINCIPAL_PURGE_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index PRINCIPAL_PURGE_POLLING_WORK_JOB_ID on
+  PRINCIPAL_PURGE_POLLING_WORK(JOB_ID);
+
+--------------------------------
+-- Principal Purge Check Work --
+--------------------------------
+
+create table PRINCIPAL_PURGE_CHECK_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  UID                           varchar(255) not null
+);
+
+create index PRINCIPAL_PURGE_CHECK_WORK_JOB_ID on
+  PRINCIPAL_PURGE_CHECK_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_CHECK_WORK_UID on
+  PRINCIPAL_PURGE_CHECK_WORK(UID);
+
+--------------------------
+-- Principal Purge Work --
+--------------------------
+
+create table PRINCIPAL_PURGE_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  UID                           varchar(255) not null
+);
+
+create index PRINCIPAL_PURGE_WORK_JOB_ID on
+  PRINCIPAL_PURGE_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_WORK_UID on
+  PRINCIPAL_PURGE_WORK(UID);
+
+
+--------------------------------
+-- Principal Home Remove Work --
+--------------------------------
+
+create table PRINCIPAL_PURGE_HOME_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade
+);
+
+create index PRINCIPAL_PURGE_HOME_WORK_JOB_ID on
+  PRINCIPAL_PURGE_HOME_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_HOME_HOME_RESOURCE_ID on
+  PRINCIPAL_PURGE_HOME_WORK(HOME_RESOURCE_ID);
+
+
+----------------------------
+-- Migration Cleanup Work --
+----------------------------
+
+create table MIGRATION_CLEANUP_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade
+);
+
+create index MIGRATION_CLEANUP_WORK_JOB_ID on
+  MIGRATION_CLEANUP_WORK(JOB_ID);
+create index MIGRATION_CLEANUP_WORK_HOME_RESOURCE_ID on
+  MIGRATION_CLEANUP_WORK(HOME_RESOURCE_ID);
+
+-----------------------
+-- Home Cleanup Work --
+-----------------------
+
+create table HOME_CLEANUP_WORK (
+  WORK_ID          integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID           integer      references JOB not null,
+  OWNER_UID        varchar(255) not null
+);
+
+create index HOME_CLEANUP_WORK_JOB_ID on
+  HOME_CLEANUP_WORK(JOB_ID);
+
+--------------------------------
+-- Migrated Home Cleanup Work --
+--------------------------------
+
+create table MIGRATED_HOME_CLEANUP_WORK (
+  WORK_ID          integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID           integer      references JOB not null,
+  OWNER_UID        varchar(255) not null
+);
+
+create index MIGRATED_HOME_CLEANUP_WORK_JOB_ID on
+  MIGRATED_HOME_CLEANUP_WORK(JOB_ID);
+
+--------------------
+-- Schema Version --
+--------------------
+
+create table CALENDARSERVER (
+  NAME                          varchar(255) primary key, -- implicit index
+  VALUE                         varchar(255)
+);
+
+insert into CALENDARSERVER values ('VERSION', '55');
+insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '6');
+insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');
+insert into CALENDARSERVER values ('MIN-VALID-REVISION', '1');

Copied: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/postgres-dialect/v56.sql (from rev 15009, CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v56.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/postgres-dialect/v56.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/old/postgres-dialect/v56.sql	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,1289 @@
+-- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+
+----
+-- Copyright (c) 2010-2015 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
+);
+
+
+--------------------
+-- Jobs           --
+--------------------
+
+create sequence JOB_SEQ;
+
+create table JOB (
+  JOB_ID      integer primary key default nextval('JOB_SEQ'), --implicit index
+  WORK_TYPE   varchar(255) not null,
+  PRIORITY    integer default 0,
+  WEIGHT      integer default 0,
+  NOT_BEFORE  timestamp not null,
+  ASSIGNED    timestamp default null,
+  OVERDUE     timestamp default null,
+  FAILED      integer default 0,
+  PAUSE       integer default 0
+);
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null,                		                -- implicit index
+  STATUS           integer      default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION      integer      default 0 not null,
+  
+  unique (OWNER_UID, STATUS)	-- implicit index
+);
+
+-- Enumeration of statuses
+
+create table HOME_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into HOME_STATUS values (0, 'normal' );
+insert into HOME_STATUS values (1, 'external');
+insert into HOME_STATUS values (2, 'purging');
+insert into HOME_STATUS values (3, 'migrating');
+insert into HOME_STATUS values (4, 'disabled');
+
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+
+----------------------------
+-- Calendar Home Metadata --
+----------------------------
+
+create table CALENDAR_HOME_METADATA (
+  RESOURCE_ID              integer     primary key references CALENDAR_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES         integer     default 0 not null,
+  TRASH                    integer     default null references CALENDAR on delete set null,
+  DEFAULT_EVENTS           integer     default null references CALENDAR on delete set null,
+  DEFAULT_TASKS            integer     default null references CALENDAR on delete set null,
+  DEFAULT_POLLS            integer     default null references CALENDAR on delete set null,
+  ALARM_VEVENT_TIMED       text        default null,
+  ALARM_VEVENT_ALLDAY      text        default null,
+  ALARM_VTODO_TIMED        text        default null,
+  ALARM_VTODO_ALLDAY       text        default null,
+  AVAILABILITY             text        default null,
+  CREATED                  timestamp   default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                 timestamp   default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+create index CALENDAR_HOME_METADATA_TRASH on
+  CALENDAR_HOME_METADATA(TRASH);
+create index CALENDAR_HOME_METADATA_DEFAULT_EVENTS on
+  CALENDAR_HOME_METADATA(DEFAULT_EVENTS);
+create index CALENDAR_HOME_METADATA_DEFAULT_TASKS on
+  CALENDAR_HOME_METADATA(DEFAULT_TASKS);
+create index CALENDAR_HOME_METADATA_DEFAULT_POLLS on
+  CALENDAR_HOME_METADATA(DEFAULT_POLLS);
+
+
+-----------------------
+-- Calendar Metadata --
+-----------------------
+
+create table CALENDAR_METADATA (
+  RESOURCE_ID           integer      primary key references CALENDAR on delete cascade, -- implicit index
+  SUPPORTED_COMPONENTS  varchar(255) default null,
+  CHILD_TYPE            integer      default 0 not null,                             	-- enum CHILD_TYPE
+  TRASHED               timestamp    default null,
+  IS_IN_TRASH           boolean      default false not null, -- collection is in the trash
+  CREATED               timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+-- Enumeration of child type
+
+create table CHILD_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CHILD_TYPE values (0, 'normal');
+insert into CHILD_TYPE values (1, 'inbox');
+insert into CHILD_TYPE values (2, 'trash');
+
+
+------------------------
+-- Calendar Migration --
+------------------------
+
+create table CALENDAR_MIGRATION (
+  CALENDAR_HOME_RESOURCE_ID		integer references CALENDAR_HOME on delete cascade,
+  REMOTE_RESOURCE_ID			integer not null,
+  LOCAL_RESOURCE_ID				integer	references CALENDAR on delete cascade,
+  LAST_SYNC_TOKEN				varchar(255),
+   
+  primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
+);
+
+create index CALENDAR_MIGRATION_LOCAL_RESOURCE_ID on
+  CALENDAR_MIGRATION(LOCAL_RESOURCE_ID);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID   varchar(255) not null,	                                   -- implicit index
+  STATUS      integer      default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION integer      default 0 not null,
+    
+  unique (OWNER_UID, STATUS)	-- 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,
+  NOTIFICATION_TYPE             varchar(255) not null,
+  NOTIFICATION_DATA             text         not null,
+  MD5                           char(32)     not null,
+  CREATED                       timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID) -- implicit index
+);
+
+create index NOTIFICATION_NOTIFICATION_HOME_RESOURCE_ID on
+  NOTIFICATION(NOTIFICATION_HOME_RESOURCE_ID);
+
+
+-------------------
+-- Calendar Bind --
+-------------------
+
+-- Joins CALENDAR_HOME and CALENDAR
+
+create table CALENDAR_BIND (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      not null references CALENDAR on delete cascade,
+  CALENDAR_RESOURCE_NAME    varchar(255) not null,
+  BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION             integer      default 0 not null,
+  BIND_UID                  varchar(36)  default null,
+  MESSAGE                   text,
+  TRANSP                    integer      default 0 not null, -- enum CALENDAR_TRANSP
+  ALARM_VEVENT_TIMED        text         default null,
+  ALARM_VEVENT_ALLDAY       text         default null,
+  ALARM_VTODO_TIMED         text         default null,
+  ALARM_VTODO_ALLDAY        text         default null,
+  TIMEZONE                  text         default null,
+
+  primary key (CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID), -- implicit index
+  unique (CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)     -- implicit index
+);
+
+create index CALENDAR_BIND_RESOURCE_ID on
+  CALENDAR_BIND(CALENDAR_RESOURCE_ID);
+
+-- Enumeration of calendar bind modes
+
+create table CALENDAR_BIND_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_MODE values (0, 'own'  );
+insert into CALENDAR_BIND_MODE values (1, 'read' );
+insert into CALENDAR_BIND_MODE values (2, 'write');
+insert into CALENDAR_BIND_MODE values (3, 'direct');
+insert into CALENDAR_BIND_MODE values (4, 'indirect');
+insert into CALENDAR_BIND_MODE values (5, 'group');
+insert into CALENDAR_BIND_MODE values (6, 'group_read');
+insert into CALENDAR_BIND_MODE values (7, 'group_write');
+
+-- Enumeration of statuses
+
+create table CALENDAR_BIND_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_STATUS values (0, 'invited' );
+insert into CALENDAR_BIND_STATUS values (1, 'accepted');
+insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
+insert into CALENDAR_BIND_STATUS values (4, 'deleted');
+
+
+-- Enumeration of transparency
+
+create table CALENDAR_TRANSP (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_TRANSP values (0, 'opaque' );
+insert into CALENDAR_TRANSP values (1, 'transparent');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+  RESOURCE_ID          integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID integer      not null references CALENDAR on delete cascade,
+  RESOURCE_NAME        varchar(255) not null,
+  ICALENDAR_TEXT       text         not null,
+  ICALENDAR_UID        varchar(255) not null,
+  ICALENDAR_TYPE       varchar(255) not null,
+  ATTACHMENTS_MODE     integer      default 0 not null, -- enum CALENDAR_OBJ_ATTACHMENTS_MODE
+  DROPBOX_ID           varchar(255),
+  ORGANIZER            varchar(255),
+  RECURRANCE_MIN       date,        -- minimum date that recurrences have been expanded to.
+  RECURRANCE_MAX       date,        -- maximum date that recurrences have been expanded to.
+  ACCESS               integer      default 0 not null,
+  SCHEDULE_OBJECT      boolean      default false,
+  SCHEDULE_TAG         varchar(36)  default null,
+  SCHEDULE_ETAGS       text         default null,
+  PRIVATE_COMMENTS     boolean      default false not null,
+  MD5                  char(32)     not null,
+  TRASHED              timestamp    default null,
+  ORIGINAL_COLLECTION  integer      default null, -- calendar_resource_id prior to trash
+  CREATED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED             timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  DATAVERSION          integer      default 0 not null,
+
+  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_MIN on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, RECURRANCE_MAX, RECURRANCE_MIN);
+
+create index CALENDAR_OBJECT_ICALENDAR_UID on
+  CALENDAR_OBJECT(ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_DROPBOX_ID on
+  CALENDAR_OBJECT(DROPBOX_ID);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJ_ATTACHMENTS_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (0, 'none' );
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (1, 'read' );
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (2, 'write');
+
+
+-- Enumeration of calendar access types
+
+create table CALENDAR_ACCESS_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(32) not null unique
+);
+
+insert into CALENDAR_ACCESS_TYPE values (0, ''             );
+insert into CALENDAR_ACCESS_TYPE values (1, 'public'       );
+insert into CALENDAR_ACCESS_TYPE values (2, 'private'      );
+insert into CALENDAR_ACCESS_TYPE values (3, 'confidential' );
+insert into CALENDAR_ACCESS_TYPE values (4, 'restricted'   );
+
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+  INSTANCE_ID                 integer        primary key default nextval('INSTANCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID        integer        not null references CALENDAR on delete cascade,
+  CALENDAR_OBJECT_RESOURCE_ID integer        not null references CALENDAR_OBJECT on delete cascade,
+  FLOATING                    boolean        not null,
+  START_DATE                  timestamp      not null,
+  END_DATE                    timestamp      not null,
+  FBTYPE                      integer        not null,
+  TRANSPARENT                 boolean        not null
+);
+
+create index TIME_RANGE_CALENDAR_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_RESOURCE_ID);
+create index TIME_RANGE_CALENDAR_OBJECT_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_OBJECT_RESOURCE_ID);
+
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown'         );
+insert into FREE_BUSY_TYPE values (1, 'free'            );
+insert into FREE_BUSY_TYPE values (2, 'busy'            );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative'  );
+
+
+-------------------
+-- Per-user data --
+-------------------
+
+create table PERUSER (
+  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
+  USER_ID                     varchar(255) not null,
+  TRANSPARENT                 boolean      not null,
+  ADJUSTED_START_DATE         timestamp    default null,
+  ADJUSTED_END_DATE           timestamp    default null,
+
+  primary key (TIME_RANGE_INSTANCE_ID, USER_ID)    -- implicit index
+);
+
+
+-------------------------------
+-- Calendar Object Migration --
+-------------------------------
+
+create table CALENDAR_OBJECT_MIGRATION (
+  CALENDAR_HOME_RESOURCE_ID		integer references CALENDAR_HOME on delete cascade,
+  REMOTE_RESOURCE_ID			integer not null,
+  LOCAL_RESOURCE_ID				integer	references CALENDAR_OBJECT on delete cascade,
+   
+  primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
+);
+
+create index CALENDAR_OBJECT_MIGRATION_HOME_LOCAL on
+  CALENDAR_OBJECT_MIGRATION(CALENDAR_HOME_RESOURCE_ID, LOCAL_RESOURCE_ID);
+create index CALENDAR_OBJECT_MIGRATION_LOCAL_RESOURCE_ID on
+  CALENDAR_OBJECT_MIGRATION(LOCAL_RESOURCE_ID);
+
+
+----------------
+-- Attachment --
+----------------
+
+create sequence ATTACHMENT_ID_SEQ;
+
+create table ATTACHMENT (
+  ATTACHMENT_ID               integer           primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
+  CALENDAR_HOME_RESOURCE_ID   integer           not null references CALENDAR_HOME,
+  DROPBOX_ID                  varchar(255),
+  CONTENT_TYPE                varchar(255)      not null,
+  SIZE                        integer           not null,
+  MD5                         char(32)          not null,
+  CREATED                     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                    timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  PATH                        varchar(1024)     not null
+);
+
+create index ATTACHMENT_CALENDAR_HOME_RESOURCE_ID on
+  ATTACHMENT(CALENDAR_HOME_RESOURCE_ID);
+
+create index ATTACHMENT_DROPBOX_ID on
+  ATTACHMENT(DROPBOX_ID);
+
+-- Many-to-many relationship between attachments and calendar objects
+create table ATTACHMENT_CALENDAR_OBJECT (
+  ATTACHMENT_ID                  integer      not null references ATTACHMENT on delete cascade,
+  MANAGED_ID                     varchar(255) not null,
+  CALENDAR_OBJECT_RESOURCE_ID    integer      not null references CALENDAR_OBJECT on delete cascade,
+
+  primary key (ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID), -- implicit index
+  unique (MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
+);
+
+create index ATTACHMENT_CALENDAR_OBJECT_CALENDAR_OBJECT_RESOURCE_ID on
+  ATTACHMENT_CALENDAR_OBJECT(CALENDAR_OBJECT_RESOURCE_ID);
+
+-----------------------------------
+-- Calendar Attachment Migration --
+-----------------------------------
+
+create table ATTACHMENT_MIGRATION (
+  CALENDAR_HOME_RESOURCE_ID		integer references CALENDAR_HOME on delete cascade,
+  REMOTE_RESOURCE_ID			integer not null,
+  LOCAL_RESOURCE_ID				integer	references ATTACHMENT on delete cascade,
+   
+  primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
+);
+
+create index ATTACHMENT_MIGRATION_HOME_LOCAL on
+  ATTACHMENT_MIGRATION(CALENDAR_HOME_RESOURCE_ID, LOCAL_RESOURCE_ID);
+create index ATTACHMENT_MIGRATION_LOCAL_RESOURCE_ID on
+  ATTACHMENT_MIGRATION(LOCAL_RESOURCE_ID);
+
+
+-----------------------
+-- Resource Property --
+-----------------------
+
+create table RESOURCE_PROPERTY (
+  RESOURCE_ID integer      not null, -- foreign key: *.RESOURCE_ID
+  NAME        varchar(255) not null,
+  VALUE       text         not null, -- FIXME: xml?
+  VIEWER_UID  varchar(255),
+
+  primary key (RESOURCE_ID, NAME, VIEWER_UID) -- implicit index
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+  RESOURCE_ID                   integer         primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  ADDRESSBOOK_PROPERTY_STORE_ID integer         default nextval('RESOURCE_ID_SEQ') not null,    -- implicit index
+  OWNER_UID                     varchar(255)    not null,
+  STATUS                        integer         default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION                   integer         default 0 not null,
+    
+  unique (OWNER_UID, STATUS)	-- implicit index
+);
+
+
+-------------------------------
+-- AddressBook Home Metadata --
+-------------------------------
+
+create table ADDRESSBOOK_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references ADDRESSBOOK_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+-----------------------------
+-- Shared AddressBook Bind --
+-----------------------------
+
+-- Joins sharee ADDRESSBOOK_HOME and owner ADDRESSBOOK_HOME
+
+create table SHARED_ADDRESSBOOK_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID          integer         not null references ADDRESSBOOK_HOME,
+  OWNER_HOME_RESOURCE_ID                integer         not null references ADDRESSBOOK_HOME on delete cascade,
+  ADDRESSBOOK_RESOURCE_NAME             varchar(255)    not null,
+  BIND_MODE                             integer         not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                           integer         not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION                         integer         default 0 not null,
+  BIND_UID                              varchar(36)     default null,
+  MESSAGE                               text,                     -- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)     -- implicit index
+);
+
+create index SHARED_ADDRESSBOOK_BIND_RESOURCE_ID on
+  SHARED_ADDRESSBOOK_BIND(OWNER_HOME_RESOURCE_ID);
+
+
+------------------------
+-- AddressBook Object --
+------------------------
+
+create table ADDRESSBOOK_OBJECT (
+  RESOURCE_ID                   integer         primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  ADDRESSBOOK_HOME_RESOURCE_ID  integer         not null references ADDRESSBOOK_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255)    not null,
+  VCARD_TEXT                    text            not null,
+  VCARD_UID                     varchar(255)    not null,
+  KIND                          integer         not null,  -- enum ADDRESSBOOK_OBJECT_KIND
+  MD5                           char(32)        not null,
+  TRASHED                       timestamp       default null,
+  IS_IN_TRASH                   boolean         default false not null,
+  CREATED                       timestamp       default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp       default timezone('UTC', CURRENT_TIMESTAMP),
+  DATAVERSION                   integer         default 0 not null,
+
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, RESOURCE_NAME), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, VCARD_UID)      -- implicit index
+);
+
+
+-----------------------------
+-- AddressBook Object kind --
+-----------------------------
+
+create table ADDRESSBOOK_OBJECT_KIND (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND values (0, 'person');
+insert into ADDRESSBOOK_OBJECT_KIND values (1, 'group' );
+insert into ADDRESSBOOK_OBJECT_KIND values (2, 'resource');
+insert into ADDRESSBOOK_OBJECT_KIND values (3, 'location');
+
+
+----------------------------------
+-- Revisions, forward reference --
+----------------------------------
+
+create sequence REVISION_SEQ;
+
+---------------------------------
+-- Address Book Object Members --
+---------------------------------
+
+create table ABO_MEMBERS (
+  GROUP_ID        integer     not null, -- references ADDRESSBOOK_OBJECT on delete cascade,   -- AddressBook Object's (kind=='group') RESOURCE_ID
+  ADDRESSBOOK_ID  integer     not null references ADDRESSBOOK_HOME on delete cascade,
+  MEMBER_ID       integer     not null, -- references ADDRESSBOOK_OBJECT,                     -- member AddressBook Object's RESOURCE_ID
+  REVISION        integer     default nextval('REVISION_SEQ') not null,
+  REMOVED         boolean     default false not null,
+  MODIFIED        timestamp   default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (GROUP_ID, MEMBER_ID, REVISION) -- implicit index
+);
+
+create index ABO_MEMBERS_ADDRESSBOOK_ID on
+  ABO_MEMBERS(ADDRESSBOOK_ID);
+create index ABO_MEMBERS_MEMBER_ID on
+  ABO_MEMBERS(MEMBER_ID);
+
+------------------------------------------
+-- Address Book Object Foreign Members  --
+------------------------------------------
+
+create table ABO_FOREIGN_MEMBERS (
+  GROUP_ID           integer      not null references ADDRESSBOOK_OBJECT on delete cascade,  -- AddressBook Object's (kind=='group') RESOURCE_ID
+  ADDRESSBOOK_ID     integer      not null references ADDRESSBOOK_HOME on delete cascade,
+  MEMBER_ADDRESS     varchar(255) not null,                                                  -- member AddressBook Object's 'calendar' address
+
+  primary key (GROUP_ID, MEMBER_ADDRESS) -- implicit index
+);
+
+create index ABO_FOREIGN_MEMBERS_ADDRESSBOOK_ID on
+  ABO_FOREIGN_MEMBERS(ADDRESSBOOK_ID);
+
+-----------------------
+-- Shared Group Bind --
+-----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK_OBJECT (kind == group)
+
+create table SHARED_GROUP_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID      integer      not null references ADDRESSBOOK_HOME,
+  GROUP_RESOURCE_ID                 integer      not null references ADDRESSBOOK_OBJECT on delete cascade,
+  GROUP_ADDRESSBOOK_NAME            varchar(255) not null,
+  BIND_MODE                         integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                       integer      not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION                     integer      default 0 not null,
+  BIND_UID                          varchar(36)  default null,
+  MESSAGE                           text,                  -- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_ADDRESSBOOK_NAME)  -- implicit index
+);
+
+create index SHARED_GROUP_BIND_RESOURCE_ID on
+  SHARED_GROUP_BIND(GROUP_RESOURCE_ID);
+
+
+---------------
+-- Revisions --
+---------------
+
+-- create sequence REVISION_SEQ;
+
+
+-------------------------------
+-- Calendar Object Revisions --
+-------------------------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      references CALENDAR,
+  CALENDAR_NAME             varchar(255) default null,
+  RESOURCE_NAME             varchar(255),
+  REVISION                  integer      default nextval('REVISION_SEQ') not null,
+  DELETED                   boolean      not null,
+  MODIFIED                  timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID, CALENDAR_NAME, RESOURCE_NAME)    -- implicit index
+);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, REVISION);
+
+create index CALENDAR_OBJECT_REVISIONS_HOME_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_HOME_RESOURCE_ID, REVISION);
+
+
+----------------------------------
+-- AddressBook Object Revisions --
+----------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+  ADDRESSBOOK_HOME_RESOURCE_ID  integer      not null references ADDRESSBOOK_HOME,
+  OWNER_HOME_RESOURCE_ID        integer      references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_NAME              varchar(255) default null,
+  OBJECT_RESOURCE_ID            integer      default 0,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID, ADDRESSBOOK_NAME, RESOURCE_NAME)    -- implicit index
+);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, REVISION);
+
+
+-----------------------------------
+-- Notification Object Revisions --
+-----------------------------------
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (NOTIFICATION_HOME_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+);
+
+create index NOTIFICATION_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on NOTIFICATION_OBJECT_REVISIONS(NOTIFICATION_HOME_RESOURCE_ID, REVISION);
+
+
+-------------------------------------------
+-- Apple Push Notification Subscriptions --
+-------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer      not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  USER_AGENT                    varchar(255) default null,
+  IP_ADDR                       varchar(255) default null,
+
+  primary key (TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+  on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+
+-----------------
+-- IMIP Tokens --
+-----------------
+
+create table IMIP_TOKENS (
+  TOKEN                         varchar(255) not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALUID                       varchar(255) not null,
+  ACCESSED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (ORGANIZER, ATTENDEE, ICALUID) -- implicit index
+);
+
+create index IMIP_TOKENS_TOKEN
+  on IMIP_TOKENS(TOKEN);
+
+
+----------------
+-- Work Items --
+----------------
+
+create sequence WORKITEM_SEQ;
+
+---------------
+-- Test Work --
+---------------
+
+create table TEST_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  DELAY 						integer
+);
+
+create index TEST_WORK_JOB_ID on
+  TEST_WORK(JOB_ID);
+
+
+---------------------------
+-- IMIP Inivitation Work --
+---------------------------
+
+create table IMIP_INVITATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  FROM_ADDR                     varchar(255) not null,
+  TO_ADDR                       varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+create index IMIP_INVITATION_WORK_JOB_ID on
+  IMIP_INVITATION_WORK(JOB_ID);
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+create table IMIP_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index IMIP_POLLING_WORK_JOB_ID on
+  IMIP_POLLING_WORK(JOB_ID);
+
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+create index IMIP_REPLY_WORK_JOB_ID on
+  IMIP_REPLY_WORK(JOB_ID);
+
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  PUSH_ID                       varchar(255) not null,
+  PUSH_PRIORITY                 integer      not null -- 1:low 5:medium 10:high
+);
+
+create index PUSH_NOTIFICATION_WORK_JOB_ID on
+  PUSH_NOTIFICATION_WORK(JOB_ID);
+create index PUSH_NOTIFICATION_WORK_PUSH_ID on
+  PUSH_NOTIFICATION_WORK(PUSH_ID);
+
+-----------------
+-- GroupCacher --
+-----------------
+
+create table GROUP_CACHER_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index GROUP_CACHER_POLLING_WORK_JOB_ID on
+  GROUP_CACHER_POLLING_WORK(JOB_ID);
+
+create table GROUP_REFRESH_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  GROUP_UID                     varchar(255) not null
+);
+
+create index GROUP_REFRESH_WORK_JOB_ID on
+  GROUP_REFRESH_WORK(JOB_ID);
+create index GROUP_REFRESH_WORK_GROUP_UID on
+  GROUP_REFRESH_WORK(GROUP_UID);
+
+create table GROUP_DELEGATE_CHANGES_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  DELEGATOR_UID                 varchar(255) not null,
+  READ_DELEGATE_UID             varchar(255) not null,
+  WRITE_DELEGATE_UID            varchar(255) not null
+);
+
+create index GROUP_DELEGATE_CHANGES_WORK_JOB_ID on
+  GROUP_DELEGATE_CHANGES_WORK(JOB_ID);
+create index GROUP_DELEGATE_CHANGES_WORK_DELEGATOR_UID on
+  GROUP_DELEGATE_CHANGES_WORK(DELEGATOR_UID);
+
+create table GROUPS (
+  GROUP_ID                      integer      primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  NAME                          varchar(255) not null,
+  GROUP_UID                     varchar(255) not null unique,									-- implicit index
+  MEMBERSHIP_HASH               varchar(255) not null,
+  EXTANT                        integer default 1,
+  CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+create table GROUP_MEMBERSHIP (
+  GROUP_ID                     integer not null references GROUPS on delete cascade,
+  MEMBER_UID                   varchar(255) not null,
+
+  primary key (GROUP_ID, MEMBER_UID)
+);
+
+create index GROUP_MEMBERSHIP_MEMBER on
+  GROUP_MEMBERSHIP(MEMBER_UID);
+
+create table GROUP_ATTENDEE_RECONCILE_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer not null references JOB,
+  RESOURCE_ID                   integer not null references CALENDAR_OBJECT on delete cascade,
+  GROUP_ID                      integer not null references GROUPS on delete cascade
+);
+
+create index GROUP_ATTENDEE_RECONCILE_WORK_JOB_ID on
+  GROUP_ATTENDEE_RECONCILE_WORK(JOB_ID);
+create index GROUP_ATTENDEE_RECONCILE_WORK_RESOURCE_ID on
+  GROUP_ATTENDEE_RECONCILE_WORK(RESOURCE_ID);
+create index GROUP_ATTENDEE_RECONCILE_WORK_GROUP_ID on
+  GROUP_ATTENDEE_RECONCILE_WORK(GROUP_ID);
+
+
+create table GROUP_ATTENDEE (
+  GROUP_ID                      integer not null references GROUPS on delete cascade,
+  RESOURCE_ID                   integer not null references CALENDAR_OBJECT on delete cascade,
+  MEMBERSHIP_HASH               varchar(255) not null,
+
+  primary key (GROUP_ID, RESOURCE_ID)
+);
+
+create index GROUP_ATTENDEE_RESOURCE_ID on
+  GROUP_ATTENDEE(RESOURCE_ID);
+
+
+create table GROUP_SHAREE_RECONCILE_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer not null references JOB,
+  CALENDAR_ID                   integer	not null references CALENDAR on delete cascade,
+  GROUP_ID                      integer not null references GROUPS on delete cascade
+);
+
+create index GROUP_SHAREE_RECONCILE_WORK_JOB_ID on
+  GROUP_SHAREE_RECONCILE_WORK(JOB_ID);
+create index GROUP_SHAREE_RECONCILE_WORK_CALENDAR_ID on
+  GROUP_SHAREE_RECONCILE_WORK(CALENDAR_ID);
+create index GROUP_SHAREE_RECONCILE_WORK_GROUP_ID on
+  GROUP_SHAREE_RECONCILE_WORK(GROUP_ID);
+
+
+create table GROUP_SHAREE (
+  GROUP_ID                      integer not null references GROUPS on delete cascade,
+  CALENDAR_ID      				integer not null references CALENDAR on delete cascade,
+  GROUP_BIND_MODE               integer not null, -- enum CALENDAR_BIND_MODE
+  MEMBERSHIP_HASH               varchar(255) not null,
+
+  primary key (GROUP_ID, CALENDAR_ID)
+);
+
+create index GROUP_SHAREE_CALENDAR_ID on
+  GROUP_SHAREE(CALENDAR_ID);
+
+---------------
+-- Delegates --
+---------------
+
+create table DELEGATES (
+  DELEGATOR                     varchar(255) not null,
+  DELEGATE                      varchar(255) not null,
+  READ_WRITE                    integer      not null, -- 1 = ReadWrite, 0 = ReadOnly
+
+  primary key (DELEGATOR, READ_WRITE, DELEGATE)
+);
+create index DELEGATE_TO_DELEGATOR on
+  DELEGATES(DELEGATE, READ_WRITE, DELEGATOR);
+
+create table DELEGATE_GROUPS (
+  DELEGATOR                     varchar(255) not null,
+  GROUP_ID                      integer      not null references GROUPS on delete cascade,
+  READ_WRITE                    integer      not null, -- 1 = ReadWrite, 0 = ReadOnly
+  IS_EXTERNAL                   integer      not null, -- 1 = External, 0 = Internal
+
+  primary key (DELEGATOR, READ_WRITE, GROUP_ID)
+);
+create index DELEGATE_GROUPS_GROUP_ID on
+  DELEGATE_GROUPS(GROUP_ID);
+
+create table EXTERNAL_DELEGATE_GROUPS (
+  DELEGATOR                     varchar(255) primary key,
+  GROUP_UID_READ                varchar(255),
+  GROUP_UID_WRITE               varchar(255)
+);
+
+--------------------------
+-- Object Splitter Work --
+--------------------------
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade
+);
+
+create index CALENDAR_OBJECT_SPLITTER_WORK_RESOURCE_ID on
+  CALENDAR_OBJECT_SPLITTER_WORK(RESOURCE_ID);
+create index CALENDAR_OBJECT_SPLITTER_WORK_JOB_ID on
+  CALENDAR_OBJECT_SPLITTER_WORK(JOB_ID);
+
+-------------------------
+-- Object Upgrade Work --
+-------------------------
+
+create table CALENDAR_OBJECT_UPGRADE_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade
+);
+
+create index CALENDAR_OBJECT_UPGRADE_WORK_RESOURCE_ID on
+  CALENDAR_OBJECT_UPGRADE_WORK(RESOURCE_ID);
+create index CALENDAR_OBJECT_UPGRADE_WORK_JOB_ID on
+  CALENDAR_OBJECT_UPGRADE_WORK(JOB_ID);
+
+---------------------------
+-- Revision Cleanup Work --
+---------------------------
+
+create table FIND_MIN_VALID_REVISION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index FIND_MIN_VALID_REVISION_WORK_JOB_ID on
+  FIND_MIN_VALID_REVISION_WORK(JOB_ID);
+
+create table REVISION_CLEANUP_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index REVISION_CLEANUP_WORK_JOB_ID on
+  REVISION_CLEANUP_WORK(JOB_ID);
+
+------------------------
+-- Inbox Cleanup Work --
+------------------------
+
+create table INBOX_CLEANUP_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index INBOX_CLEANUP_WORK_JOB_ID on
+   INBOX_CLEANUP_WORK(JOB_ID);
+
+create table CLEANUP_ONE_INBOX_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_ID                       integer      not null unique references CALENDAR_HOME on delete cascade -- implicit index
+);
+
+create index CLEANUP_ONE_INBOX_WORK_JOB_ID on
+  CLEANUP_ONE_INBOX_WORK(JOB_ID);
+
+-------------------
+-- Schedule Work --
+-------------------
+
+create table SCHEDULE_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ICALENDAR_UID                 varchar(255) not null,
+  WORK_TYPE                     varchar(255) not null
+);
+
+create index SCHEDULE_WORK_JOB_ID on
+  SCHEDULE_WORK(JOB_ID);
+create index SCHEDULE_WORK_ICALENDAR_UID on
+  SCHEDULE_WORK(ICALENDAR_UID);
+
+---------------------------
+-- Schedule Refresh Work --
+---------------------------
+
+create table SCHEDULE_REFRESH_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  ATTENDEE_COUNT                integer
+);
+
+create index SCHEDULE_REFRESH_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_REFRESH_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_REFRESH_WORK_RESOURCE_ID on
+  SCHEDULE_REFRESH_WORK(RESOURCE_ID);
+
+create table SCHEDULE_REFRESH_ATTENDEES (
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  ATTENDEE                      varchar(255) not null,
+
+  primary key (RESOURCE_ID, ATTENDEE)
+);
+
+------------------------------
+-- Schedule Auto Reply Work --
+------------------------------
+
+create table SCHEDULE_AUTO_REPLY_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  PARTSTAT                      varchar(255) not null
+);
+
+create index SCHEDULE_AUTO_REPLY_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_AUTO_REPLY_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_AUTO_REPLY_WORK_RESOURCE_ID on
+  SCHEDULE_AUTO_REPLY_WORK(RESOURCE_ID);
+
+-----------------------------
+-- Schedule Organizer Work --
+-----------------------------
+
+create table SCHEDULE_ORGANIZER_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  SCHEDULE_ACTION               integer      not null, -- Enum SCHEDULE_ACTION
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer,     -- this references a possibly non-existent CALENDAR_OBJECT
+  ICALENDAR_TEXT_OLD            text,
+  ICALENDAR_TEXT_NEW            text,
+  ATTENDEE_COUNT                integer,
+  SMART_MERGE                   boolean
+);
+
+create index SCHEDULE_ORGANIZER_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_ORGANIZER_WORK_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_WORK(RESOURCE_ID);
+
+-- Enumeration of schedule actions
+
+create table SCHEDULE_ACTION (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into SCHEDULE_ACTION values (0, 'create');
+insert into SCHEDULE_ACTION values (1, 'modify');
+insert into SCHEDULE_ACTION values (2, 'modify-cancelled');
+insert into SCHEDULE_ACTION values (3, 'remove');
+
+----------------------------------
+-- Schedule Organizer Send Work --
+----------------------------------
+
+create table SCHEDULE_ORGANIZER_SEND_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  SCHEDULE_ACTION               integer      not null, -- Enum SCHEDULE_ACTION
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer,     -- this references a possibly non-existent CALENDAR_OBJECT
+  ATTENDEE                      varchar(255) not null,
+  ITIP_MSG                      text,
+  NO_REFRESH                    boolean
+);
+
+create index SCHEDULE_ORGANIZER_SEND_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_SEND_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_ORGANIZER_SEND_WORK_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_SEND_WORK(RESOURCE_ID);
+
+-------------------------
+-- Schedule Reply Work --
+-------------------------
+
+create table SCHEDULE_REPLY_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer,     -- this references a possibly non-existent CALENDAR_OBJECT
+  ITIP_MSG                      text
+);
+
+create index SCHEDULE_REPLY_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_REPLY_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_REPLY_WORK_RESOURCE_ID on
+  SCHEDULE_REPLY_WORK(RESOURCE_ID);
+
+----------------------------------
+-- Principal Purge Polling Work --
+----------------------------------
+
+create table PRINCIPAL_PURGE_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index PRINCIPAL_PURGE_POLLING_WORK_JOB_ID on
+  PRINCIPAL_PURGE_POLLING_WORK(JOB_ID);
+
+--------------------------------
+-- Principal Purge Check Work --
+--------------------------------
+
+create table PRINCIPAL_PURGE_CHECK_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  UID                           varchar(255) not null
+);
+
+create index PRINCIPAL_PURGE_CHECK_WORK_JOB_ID on
+  PRINCIPAL_PURGE_CHECK_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_CHECK_WORK_UID on
+  PRINCIPAL_PURGE_CHECK_WORK(UID);
+
+--------------------------
+-- Principal Purge Work --
+--------------------------
+
+create table PRINCIPAL_PURGE_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  UID                           varchar(255) not null
+);
+
+create index PRINCIPAL_PURGE_WORK_JOB_ID on
+  PRINCIPAL_PURGE_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_WORK_UID on
+  PRINCIPAL_PURGE_WORK(UID);
+
+
+--------------------------------
+-- Principal Home Remove Work --
+--------------------------------
+
+create table PRINCIPAL_PURGE_HOME_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade
+);
+
+create index PRINCIPAL_PURGE_HOME_WORK_JOB_ID on
+  PRINCIPAL_PURGE_HOME_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_HOME_HOME_RESOURCE_ID on
+  PRINCIPAL_PURGE_HOME_WORK(HOME_RESOURCE_ID);
+
+
+----------------------------
+-- Migration Cleanup Work --
+----------------------------
+
+create table MIGRATION_CLEANUP_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade
+);
+
+create index MIGRATION_CLEANUP_WORK_JOB_ID on
+  MIGRATION_CLEANUP_WORK(JOB_ID);
+create index MIGRATION_CLEANUP_WORK_HOME_RESOURCE_ID on
+  MIGRATION_CLEANUP_WORK(HOME_RESOURCE_ID);
+
+-----------------------
+-- Home Cleanup Work --
+-----------------------
+
+create table HOME_CLEANUP_WORK (
+  WORK_ID          integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID           integer      references JOB not null,
+  OWNER_UID        varchar(255) not null
+);
+
+create index HOME_CLEANUP_WORK_JOB_ID on
+  HOME_CLEANUP_WORK(JOB_ID);
+
+--------------------------------
+-- Migrated Home Cleanup Work --
+--------------------------------
+
+create table MIGRATED_HOME_CLEANUP_WORK (
+  WORK_ID          integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID           integer      references JOB not null,
+  OWNER_UID        varchar(255) not null
+);
+
+create index MIGRATED_HOME_CLEANUP_WORK_JOB_ID on
+  MIGRATED_HOME_CLEANUP_WORK(JOB_ID);
+
+--------------------
+-- Schema Version --
+--------------------
+
+create table CALENDARSERVER (
+  NAME                          varchar(255) primary key, -- implicit index
+  VALUE                         varchar(255)
+);
+
+insert into CALENDARSERVER values ('VERSION', '56');
+insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '6');
+insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');
+insert into CALENDARSERVER values ('MIN-VALID-REVISION', '1');

Copied: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_55_to_56.sql (from rev 15009, CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_55_to_56.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_55_to_56.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_55_to_56.sql	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,33 @@
+----
+-- Copyright (c) 2012-2015 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 55 to 56 --
+---------------------------------------------------
+
+-- New table
+create table TEST_WORK (
+    "WORK_ID" integer primary key,
+    "JOB_ID" integer not null references JOB,
+    "DELAY" integer
+);
+
+create index TEST_WORK_JOB_ID_228ede32 on TEST_WORK (
+    "JOB_ID"
+);
+
+-- update the version
+update CALENDARSERVER set VALUE = '56' where NAME = 'VERSION';

Copied: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_56_to_57.sql (from rev 15009, CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_56_to_57.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_56_to_57.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_56_to_57.sql	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,28 @@
+----
+-- Copyright (c) 2012-2015 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 56 to 57 --
+---------------------------------------------------
+
+-- pre-delete any that would conflict during the update
+delete from IMIP_TOKENS where (ORGANIZER, ATTENDEE, ICALUID) in (select concat('urn:uuid:', substr(ORGANIZER, 11)), ATTENDEE, ICALUID from IMIP_TOKENS where substr(ORGANIZER, 1, 10) = 'urn:x-uid:');
+
+-- convert the old-style urn:uuid: CUAs to new style urn:x-uid:
+update IMIP_TOKENS set ORGANIZER = concat('urn:x-uid:', substr(ORGANIZER, 10)) where substr(ORGANIZER, 1, 9) = 'urn:uuid:';
+
+-- update the version
+update CALENDARSERVER set VALUE = '57' where NAME = 'VERSION';

Copied: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_55_to_56.sql (from rev 15009, CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_55_to_56.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_55_to_56.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_55_to_56.sql	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,32 @@
+----
+-- Copyright (c) 2012-2015 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 55 to 56 --
+---------------------------------------------------
+
+-- New table
+create table TEST_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  DELAY 						integer
+);
+
+create index TEST_WORK_JOB_ID on
+  TEST_WORK(JOB_ID);
+
+-- update the version
+update CALENDARSERVER set VALUE = '56' where NAME = 'VERSION';

Copied: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_56_to_57.sql (from rev 15009, CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_56_to_57.sql)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_56_to_57.sql	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_56_to_57.sql	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,28 @@
+----
+-- Copyright (c) 2012-2015 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 56 to 57 --
+---------------------------------------------------
+
+-- pre-delete any that would conflict during the update
+delete from IMIP_TOKENS where (ORGANIZER, ATTENDEE, ICALUID) in (select concat('urn:uuid:', substr(ORGANIZER, 11)), ATTENDEE, ICALUID from IMIP_TOKENS where substr(ORGANIZER, 1, 10) = 'urn:x-uid:');
+
+-- convert the old-style urn:uuid: CUAs to new style urn:x-uid:
+update IMIP_TOKENS set ORGANIZER = concat('urn:x-uid:', substr(ORGANIZER, 10)) where substr(ORGANIZER, 1, 9) = 'urn:uuid:';
+
+-- update the version
+update CALENDARSERVER set VALUE = '57' where NAME = 'VERSION';

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_sharing.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_sharing.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -19,7 +19,7 @@
 from pycalendar.datetime import DateTime
 
 from twext.enterprise.dal.syntax import Insert, Parameter, Update, Delete, \
-    Select, Max
+    Select
 from twext.python.clsprop import classproperty
 from twext.python.log import Logger
 
@@ -1451,18 +1451,6 @@
         )
 
 
-    @classmethod
-    def _revisionsForResourceIDs(cls, resourceIDs):
-        rev = cls._revisionsSchema
-        return Select(
-            [rev.RESOURCE_ID, Max(rev.REVISION)],
-            From=rev,
-            Where=rev.RESOURCE_ID.In(Parameter("resourceIDs", len(resourceIDs))).And(
-                (rev.RESOURCE_NAME != None).Or(rev.DELETED == False)),
-            GroupBy=rev.RESOURCE_ID
-        )
-
-
     @inlineCallbacks
     def invalidateQueryCache(self):
         queryCacher = self._txn._queryCacher

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_util.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/sql_util.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -16,7 +16,7 @@
 ##
 
 from twext.enterprise.dal.syntax import Max, Select, Parameter, Delete, Insert, \
-    Update, ColumnSyntax, TableSyntax, Upper
+    Update, ColumnSyntax, TableSyntax, Upper, utcNowSQL
 from twext.python.clsprop import classproperty
 from twext.python.log import Logger
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue
@@ -66,6 +66,18 @@
                       Where=rev.RESOURCE_ID == Parameter("resourceID"))
 
 
+    @classmethod
+    def _revisionsForResourceIDs(cls, resourceIDs):
+        rev = cls._revisionsSchema
+        return Select(
+            [rev.RESOURCE_ID, Max(rev.REVISION)],
+            From=rev,
+            Where=rev.RESOURCE_ID.In(Parameter("resourceIDs", len(resourceIDs))).And(
+                (rev.RESOURCE_NAME != None).Or(rev.DELETED == False)),
+            GroupBy=rev.RESOURCE_ID
+        )
+
+
     def revisionFromToken(self, token):
         if token is None:
             return 0
@@ -91,6 +103,21 @@
         returnValue(revision)
 
 
+    @classmethod
+    @inlineCallbacks
+    def childSyncTokenRevisions(cls, home, childResourceIDs):
+        rows = (yield cls._revisionsForResourceIDs(childResourceIDs).on(home._txn, resourceIDs=childResourceIDs))
+        revisions = dict(rows)
+
+        # Add in any that were missing - this assumes that childResourceIDs were all valid to begin with
+        missingIDs = set(childResourceIDs) - set(revisions.keys())
+        if missingIDs:
+            min_revision = int((yield home._txn.calendarserverValue("MIN-VALID-REVISION")))
+            for resourceID in missingIDs:
+                revisions[resourceID] = min_revision
+        returnValue(revisions)
+
+
     def objectResourcesSinceToken(self, token):
         raise NotImplementedError()
 
@@ -206,7 +233,8 @@
         return Update(
             {
                 rev.REVISION: schema.REVISION_SEQ,
-                rev.COLLECTION_NAME: Parameter("name")
+                rev.COLLECTION_NAME: Parameter("name"),
+                rev.MODIFIED: utcNowSQL,
             },
             Where=(rev.RESOURCE_ID == Parameter("resourceID")).And
                   (rev.RESOURCE_NAME == None),
@@ -233,7 +261,10 @@
         """
         rev = cls._revisionsSchema
         return Update(
-            {rev.REVISION: schema.REVISION_SEQ, },
+            {
+                rev.REVISION: schema.REVISION_SEQ,
+                rev.MODIFIED: utcNowSQL,
+            },
             Where=(rev.RESOURCE_ID == Parameter("resourceID")).And
                   (rev.RESOURCE_NAME == None)
         )
@@ -276,7 +307,8 @@
             {
                 rev.RESOURCE_ID: None,
                 rev.REVISION: schema.REVISION_SEQ,
-                rev.DELETED: True
+                rev.DELETED: True,
+                rev.MODIFIED: utcNowSQL,
             },
             Where=(rev.HOME_RESOURCE_ID == Parameter("homeID")).And(
                 rev.RESOURCE_ID == Parameter("resourceID")).And(
@@ -294,7 +326,8 @@
             {
                 rev.RESOURCE_ID: None,
                 rev.REVISION: schema.REVISION_SEQ,
-                rev.DELETED: True
+                rev.DELETED: True,
+                rev.MODIFIED: utcNowSQL,
             },
             Where=(rev.RESOURCE_ID == Parameter("resourceID")).And(
                 rev.RESOURCE_NAME == None),
@@ -346,7 +379,11 @@
     def _deleteBumpTokenQuery(cls):
         rev = cls._revisionsSchema
         return Update(
-            {rev.REVISION: schema.REVISION_SEQ, rev.DELETED: True},
+            {
+                rev.REVISION: schema.REVISION_SEQ,
+                rev.DELETED: True,
+                rev.MODIFIED: utcNowSQL,
+            },
             Where=(rev.RESOURCE_ID == Parameter("resourceID")).And(
                 rev.RESOURCE_NAME == Parameter("name")),
             Return=rev.REVISION
@@ -357,7 +394,10 @@
     def _updateBumpTokenQuery(cls):
         rev = cls._revisionsSchema
         return Update(
-            {rev.REVISION: schema.REVISION_SEQ},
+            {
+                rev.REVISION: schema.REVISION_SEQ,
+                rev.MODIFIED: utcNowSQL,
+            },
             Where=(rev.RESOURCE_ID == Parameter("resourceID")).And(
                 rev.RESOURCE_NAME == Parameter("name")),
             Return=rev.REVISION
@@ -379,7 +419,11 @@
     def _updatePreviouslyNamedQuery(cls):
         rev = cls._revisionsSchema
         return Update(
-            {rev.REVISION: schema.REVISION_SEQ, rev.DELETED: False},
+            {
+                rev.REVISION: schema.REVISION_SEQ,
+                rev.DELETED: False,
+                rev.MODIFIED: utcNowSQL,
+            },
             Where=(rev.RESOURCE_ID == Parameter("resourceID")).And(
                 rev.RESOURCE_NAME == Parameter("name")),
             Return=rev.REVISION

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/test/accounts/accounts.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/test/accounts/accounts.xml	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/test/accounts/accounts.xml	2015-07-28 02:43:19 UTC (rev 15011)
@@ -30,7 +30,7 @@
     <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
     <short-name>wsanchez</short-name>
     <password>zehcnasw</password>
-    <full-name>Wilfredo Sanchez</full-name>
+    <full-name>Wilfredo Sanchez-Vega</full-name>
     <email>wsanchez at example.com</email>
   </record>
   <record type="user">

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/test/util.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/test/util.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -49,7 +49,7 @@
 
 from twistedcaldav import ical
 from twistedcaldav.config import config, ConfigDict
-from twistedcaldav.ical import Component as VComponent, Component
+from twistedcaldav.ical import Component as VComponent
 from twistedcaldav.stdconfig import DEFAULT_CONFIG
 from twistedcaldav.vcard import Component as ABComponent
 
@@ -260,10 +260,7 @@
             {"push": notifierFactory} if notifierFactory is not None else {},
             directoryService,
             attachmentRoot,
-            (
-                "https://example.com/calendars/__uids__/"
-                "%(home)s/attachments/%(name)s"
-            ),
+            "https://example.com/calendars/__uids__/%(home)s/dropbox/%(dropbox_id)s/%(name)s",
             quota=quota
         )
         store.label = currentTestID
@@ -549,40 +546,16 @@
     current year.
     """
 
+    subs = {}
     nowYear = DateTime.getToday().getYear()
-    return data % {"now": nowYear}
+    subs["now"] = nowYear
+    for i in range(1, 10):
+        subs["now-{}".format(i)] = nowYear - 1
+        subs["now+{}".format(i)] = nowYear + 1
+    return data % subs
 
 
-relativeDateSubstitutions = {}
 
-
-def componentUpdate(data):
-    """
-    Update the supplied iCalendar data so that all dates are updated to the
-    current year.
-    """
-
-    if len(relativeDateSubstitutions) == 0:
-        now = DateTime.getToday()
-
-        relativeDateSubstitutions["now"] = now
-
-        for i in range(30):
-            attrname = "now_back%s" % (i + 1,)
-            dt = now.duplicate()
-            dt.offsetDay(-(i + 1))
-            relativeDateSubstitutions[attrname] = dt
-
-        for i in range(30):
-            attrname = "now_fwd%s" % (i + 1,)
-            dt = now.duplicate()
-            dt.offsetDay(i + 1)
-            relativeDateSubstitutions[attrname] = dt
-
-    return Component.fromString(data.format(**relativeDateSubstitutions))
-
-
-
 @inlineCallbacks
 def resetCalendarMD5s(md5s, store):
     """

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/upgrade/sql/test/test_upgrade_with_data.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/upgrade/sql/test/test_upgrade_with_data.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/upgrade/sql/test/test_upgrade_with_data.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -25,6 +25,7 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.trial.unittest import TestCase
 
+from txdav.caldav.datastore.scheduling.imip.token import iMIPTokenRecord
 from txdav.caldav.datastore.scheduling.work import ScheduleReplyWork, \
     ScheduleWork
 from txdav.common.datastore.sql_tables import _populateSchema
@@ -298,3 +299,44 @@
         self.assertEqual(workers[0].workType, "SCHEDULE_REPLY_WORK")
 
         yield txn.commit()
+
+
+    @inlineCallbacks
+    def test_upgrade_imipTokens(self):
+        """
+        Old-style canonical CUAs (urn:uuid:) are converted to new style (urn:x-uid:)
+        """
+
+        # Load old schema and populate with data
+        yield self._loadOldSchema(self.upgradePath.child("v56.sql"))
+
+        # Add two tokens records crafted to simulate conflicting old-style and
+        # new style CUAs -- the result should be only the new-style copy.
+        txn = self.store.newTransaction("loadData")
+        yield iMIPTokenRecord.create(
+            txn,
+            token="123",
+            organizer="urn:uuid:PLUGH",
+            attendee="mailto:user at example.com",
+            icaluid="XYZZY"
+        )
+        yield iMIPTokenRecord.create(
+            txn,
+            token="456",
+            organizer="urn:x-uid:PLUGH",
+            attendee="mailto:user at example.com",
+            icaluid="XYZZY"
+        )
+        yield txn.commit()
+
+        upgrader = UpgradeDatabaseSchemaStep(self.store)
+        yield upgrader.databaseUpgrade()
+
+        txn = self.store.newTransaction("loadData")
+        tokens = yield iMIPTokenRecord.all(txn)
+
+        self.assertEqual(len(tokens), 1)
+        token = list(tokens)[0]
+        self.assertEqual(token.token, "456")
+        self.assertEqual(token.organizer, "urn:x-uid:PLUGH")
+        yield txn.commit()

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/upgrade/test/test_migrate.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/upgrade/test/test_migrate.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/upgrade/test/test_migrate.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -295,6 +295,15 @@
         L{UpgradeToDatabaseService.startService} will do the upgrade, then
         start its dependent service by adding it to its service hierarchy.
         """
+
+        # Create a fake directory in the same place as a home, but with a non-existent uid
+        fake_dir = self.filesPath.child("calendars").child("__uids__").child("ho").child("me").child("foobar")
+        fake_dir.makedirs()
+
+        # Create a fake file in the same place as a home,with a name that matches the hash uid prefix
+        fake_file = self.filesPath.child("calendars").child("__uids__").child("ho").child("me").child("home_file")
+        fake_file.setContent("")
+
         yield self.upgrader.stepWithResult(None)
         txn = self.sqlStore.newTransaction()
         self.addCleanup(txn.commit)

Copied: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/load_work.py (from rev 15009, CalendarServer/trunk/txdav/common/datastore/work/load_work.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/load_work.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/load_work.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,76 @@
+# -*- test-case-name: txdav.common.datastore.work.test.test_revision_cleanup -*-
+##
+# Copyright (c) 2013-2015 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 twext.enterprise.dal.record import fromTable
+from twext.enterprise.jobqueue import WorkItem
+from twext.python.log import Logger
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, Deferred
+from txdav.common.datastore.sql_tables import schema
+
+log = Logger()
+
+
+class TestWork(WorkItem, fromTable(schema.TEST_WORK)):
+    """
+    This work item is used solely for testing purposes to allow us to simulate different
+    types of work with varying priority, weight and notBefore, and taking a variable amount of
+    time to complete. This will allow us to load test the job queue.
+    """
+
+    @classmethod
+    def schedule(cls, store, delay, priority, weight, runtime):
+        """
+        Create a new L{TestWork} item.
+
+        @param store: the L{CommonStore} to use
+        @type store: L{CommonStore}
+        @param delay: seconds before work executes
+        @type delay: L{int}
+        @param priority: priority to use for this work
+        @type priority: L{int}
+        @param weight: weight to use for thus work
+        @type weight: L{int}
+        @param runtime: amount of time this work should take to execute in milliseconds
+        @type runtime: L{int}
+        """
+        def _enqueue(txn):
+            return TestWork.reschedule(
+                txn,
+                delay,
+                priority=priority,
+                weight=weight,
+                delay=runtime
+            )
+
+        return store.inTransaction("TestWork.schedule", _enqueue)
+
+
+    @inlineCallbacks
+    def doWork(self):
+        """
+        All this work does is wait for the specified amount of time.
+        """
+
+        log.debug("TestWork started: {}".format(self.jobID))
+        if self.delay != 0:
+            wait = Deferred()
+            def _timedDeferred():
+                wait.callback(True)
+            reactor.callLater(self.delay / 1000.0, _timedDeferred)
+            yield wait
+        log.debug("TestWork done: {}".format(self.jobID))

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/revision_cleanup.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/revision_cleanup.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/revision_cleanup.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -53,6 +53,10 @@
         return float(config.RevisionCleanup.CleanupPeriodDays) * 24 * 60 * 60
 
 
+    def dateCutoff(self):
+        return datetime.datetime.utcnow() - datetime.timedelta(days=float(config.RevisionCleanup.SyncTokenLifetimeDays))
+
+
     @inlineCallbacks
     def doWork(self):
 
@@ -60,10 +64,7 @@
         minValidRevision = int((yield self.transaction.calendarserverValue("MIN-VALID-REVISION")))
 
         # get max revision on table rows before dateLimit
-        dateLimit = (
-            datetime.datetime.utcnow() -
-            datetime.timedelta(days=float(config.RevisionCleanup.SyncTokenLifetimeDays))
-        )
+        dateLimit = self.dateCutoff()
         maxRevOlderThanDate = 0
 
         # TODO: Use one Select statement

Copied: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/test/test_load_work.py (from rev 15009, CalendarServer/trunk/txdav/common/datastore/work/test/test_load_work.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/test/test_load_work.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/test/test_load_work.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -0,0 +1,57 @@
+##
+# Copyright (c) 2013-2015 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 twext.enterprise.jobqueue import JobItem
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks
+from twisted.trial.unittest import TestCase
+from txdav.common.datastore.test.util import CommonCommonTests
+from txdav.common.datastore.work.load_work import TestWork
+
+
+
+class LoadWorkTests(CommonCommonTests, TestCase):
+    """
+    Test L{TestWork}.
+    """
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(LoadWorkTests, self).setUp()
+        yield self.buildStoreAndDirectory()
+
+
+    @inlineCallbacks
+    def test_basicWork(self):
+        """
+        Verify that an L{TestWork} item can be enqueued and executed.
+        """
+
+        # do FindMinValidRevisionWork
+        yield TestWork.schedule(self.storeUnderTest(), 0, 1, 2, 3)
+
+        work = yield TestWork.all(self.transactionUnderTest())
+        self.assertEqual(len(work), 1)
+        self.assertEqual(work[0].delay, 3)
+        job = yield JobItem.querysimple(self.transactionUnderTest(), jobID=work[0].jobID)
+        self.assertEqual(len(job), 1)
+        self.assertEqual(job[0].priority, 1)
+        self.assertEqual(job[0].weight, 2)
+        yield self.commit()
+
+        yield JobItem.waitEmpty(self.storeUnderTest().newTransaction, reactor, 60)

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/test/test_revision_cleanup.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/test/test_revision_cleanup.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/common/datastore/work/test/test_revision_cleanup.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -23,12 +23,14 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.trial.unittest import TestCase
 from twistedcaldav.config import config
+from twistedcaldav.ical import Component
 from twistedcaldav.vcard import Component as VCard
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_READ
 from txdav.common.datastore.test.util import CommonCommonTests, populateCalendarsFrom
 from txdav.common.datastore.work.revision_cleanup import FindMinValidRevisionWork, RevisionCleanupWork
 from txdav.common.icommondatastore import SyncTokenValidException
 import datetime
+import time
 
 
 
@@ -88,6 +90,21 @@
 END:VCALENDAR
 """
 
+    cal1_mod = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:20131122T140000
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+SUMMARY:event 1.1
+END:VEVENT
+END:VCALENDAR
+"""
+
     cal2 = """BEGIN:VCALENDAR
 VERSION:2.0
 CALSCALE:GREGORIAN
@@ -242,6 +259,10 @@
         Verify that all extra calendar object revisions are deleted by FindMinValidRevisionWork and RevisionCleanupWork
         """
 
+        # get home sync token
+        home = yield self.homeUnderTest(name="user01")
+        hometoken = yield home.syncToken()
+
         # get sync token
         calendar = yield self.calendarUnderTest(home="user01", name="calendar")
         token = yield calendar.syncToken()
@@ -267,7 +288,7 @@
 
         # Get the minimum valid revision and check it
         minValidRevision = yield self.transactionUnderTest().calendarserverValue("MIN-VALID-REVISION")
-        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]))
+        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]) + 1)
 
         # do RevisionCleanupWork
         yield self.transactionUnderTest().enqueue(RevisionCleanupWork, notBefore=datetime.datetime.utcnow())
@@ -280,14 +301,111 @@
             [rev.REVISION],
             From=rev,
         ).on(self.transactionUnderTest())
-        self.assertEqual(len(revisionRows), 1)  # deleteRevisionsBefore() leaves 1 revision behind
+        self.assertEqual(len(revisionRows), 0)
 
         # old sync token fails
         calendar = yield self.calendarUnderTest(home="user01", name="calendar")
         yield self.failUnlessFailure(calendar.resourceNamesSinceToken(token), SyncTokenValidException)
+        yield self.commit()
 
+        # old sync token fails
+        home = yield self.homeUnderTest(name="user01")
+        yield self.failUnlessFailure(home.resourceNamesSinceToken(hometoken, 1), SyncTokenValidException)
+        yield self.commit()
 
+        # calendar sync token changed
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        newtoken = yield calendar.syncToken()
+        self.assertGreater(newtoken, token)
+        yield self.commit()
+
+        # home sync token changed
+        home = yield self.homeUnderTest(name="user01")
+        newhometoken = yield home.syncToken()
+        self.assertGreater(newhometoken, hometoken)
+        yield self.commit()
+
+        # Depth:1 tokens match
+        home = yield self.homeUnderTest(name="user01")
+        yield home.loadChildren()
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        newtoken1 = yield calendar.syncToken()
+        self.assertEqual(newtoken1, newtoken)
+        yield self.commit()
+
+
     @inlineCallbacks
+    def test_calendarObjectRevisions_Modified(self):
+        """
+        Verify that a calendar object created before the revision cut-off, but modified after it is correctly reported as changed
+        after revision clean-up
+        """
+
+        # Need to add one non-event change that creates a revision after the last event change revisions in order
+        # for the logic in this test to work correctly
+        home = yield self.homeUnderTest(name="user01")
+        yield home.createCalendarWithName("_ignore_me")
+        yield self.commit()
+
+        # get initial sync token
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        initial_token = yield calendar.syncToken()
+        yield self.commit()
+
+        # Pause to give some space in the modified time
+        time.sleep(1)
+        modified = datetime.datetime.utcnow()
+        time.sleep(1)
+
+        # Patch the work item to use the modified cut-off we need
+        def _dateCutoff(self):
+            return modified
+        self.patch(FindMinValidRevisionWork, "dateCutoff", _dateCutoff)
+
+        # Make a change to get a pre-update token
+        cal2Object = yield self.calendarObjectUnderTest(self.transactionUnderTest(), name="cal2.ics", calendar_name="calendar", home="user01")
+        yield cal2Object.remove()
+        yield self.commit()
+
+        # get changed sync token
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        pre_update_token = yield calendar.syncToken()
+        yield self.commit()
+
+        # make changes
+        cal1Object = yield self.calendarObjectUnderTest(self.transactionUnderTest(), name="cal1.ics", calendar_name="calendar", home="user01")
+        yield cal1Object.setComponent(Component.fromString(self.cal1_mod))
+        yield self.commit()
+
+        # get changed sync token
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        update_token = yield calendar.syncToken()
+        yield self.commit()
+
+        # do FindMinValidRevisionWork and RevisionCleanupWork
+        yield FindMinValidRevisionWork.reschedule(self.transactionUnderTest(), 0)
+        yield self.commit()
+        yield JobItem.waitEmpty(self.storeUnderTest().newTransaction, reactor, 60)
+
+        # initial sync token fails
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield self.failUnlessFailure(calendar.resourceNamesSinceToken(initial_token), SyncTokenValidException)
+        yield self.commit()
+
+        # Pre-update sync token returns one item
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        names = yield calendar.resourceNamesSinceToken(pre_update_token)
+        self.assertEqual(names, (['cal1.ics'], [], []))
+        yield self.commit()
+
+        # Post-update sync token returns one item
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        names = yield calendar.resourceNamesSinceToken(update_token)
+        self.assertEqual(names, ([], [], []))
+        yield self.commit()
+
+
+    @inlineCallbacks
     def test_notificationObjectRevisions(self):
         """
         Verify that all extra notification object revisions are deleted by FindMinValidRevisionWork and RevisionCleanupWork
@@ -315,7 +433,7 @@
 
         # Get the minimum valid revision and check it
         minValidRevision = yield self.transactionUnderTest().calendarserverValue("MIN-VALID-REVISION")
-        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]))
+        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]) + 1)
 
         # do RevisionCleanupWork
         yield self.transactionUnderTest().enqueue(RevisionCleanupWork, notBefore=datetime.datetime.utcnow())
@@ -328,7 +446,7 @@
             [rev.REVISION],
             From=rev,
         ).on(self.transactionUnderTest())
-        self.assertEqual(len(revisionRows), 1)  # deleteRevisionsBefore() leaves 1 revision behind
+        self.assertEqual(len(revisionRows), 0)
 
         # old sync token fails
         home = yield self.homeUnderTest(name="user01")
@@ -367,7 +485,7 @@
 
         # Get the minimum valid revision and check it
         minValidRevision = yield self.transactionUnderTest().calendarserverValue("MIN-VALID-REVISION")
-        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]))
+        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]) + 1)
 
         # do RevisionCleanupWork
         yield self.transactionUnderTest().enqueue(RevisionCleanupWork, notBefore=datetime.datetime.utcnow())
@@ -380,7 +498,7 @@
             [rev.REVISION],
             From=rev,
         ).on(self.transactionUnderTest())
-        self.assertEqual(len(revisionRows), 1)  # deleteRevisionsBefore() leaves 1 revision behind
+        self.assertEqual(len(revisionRows), 0)
 
         # old sync token fails
         addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
@@ -440,7 +558,7 @@
 
         # Get the minimum valid revision and check it
         minValidRevision = yield self.transactionUnderTest().calendarserverValue("MIN-VALID-REVISION")
-        self.assertEqual(int(minValidRevision), max([row[3] for row in group1Rows + group2Rows]))
+        self.assertEqual(int(minValidRevision), max([row[3] for row in group1Rows + group2Rows]) + 1)
 
         # do RevisionCleanupWork
         yield self.transactionUnderTest().enqueue(RevisionCleanupWork, notBefore=datetime.datetime.utcnow())

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/dps/server.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/dps/server.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/dps/server.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -808,9 +808,12 @@
         else:
             setproctitle("CalendarServer Directory Proxy Service")
 
+        multiService = MultiService()
+
         try:
-            _ignore_pool, txnFactory = getDBPool(config)
+            pool, txnFactory = getDBPool(config)
             store = storeFromConfigWithDPSServer(config, txnFactory)
+            pool.setServiceParent(multiService)
         except Exception as e:
             log.error("Failed to create directory service", error=e)
             raise
@@ -832,10 +835,9 @@
             ),
             DirectoryProxyAMPFactory(store.directoryService())
         )
+        dpsService.setServiceParent(multiService)
 
         if config.Manhole.Enabled:
-            multiService = MultiService()
-            dpsService.setServiceParent(multiService)
             try:
                 from twisted.conch.manhole_tap import (
                     makeService as manholeMakeService
@@ -864,6 +866,4 @@
                     "manhole_tap could not be imported"
                 )
 
-            return multiService
-        else:
-            return dpsService
+        return multiService

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/who/directory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/who/directory.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/who/directory.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -200,10 +200,20 @@
         else:
             recordTypes = None
 
-        results = yield self.recordsFromExpression(
-            expression, recordTypes=recordTypes, limitResults=limitResults,
-            timeoutSeconds=timeoutSeconds
-        )
+        # If a filter has been set, pass self.recordsFromExpression to it for
+        # result processing
+        if getattr(self, "_resultFilter", None):
+            results = yield self._resultFilter(
+                self.recordsFromExpression, tokens, expression,
+                recordTypes=recordTypes, limitResults=limitResults,
+                timeoutSeconds=timeoutSeconds
+            )
+        else:
+            results = yield self.recordsFromExpression(
+                expression, recordTypes=recordTypes, limitResults=limitResults,
+                timeoutSeconds=timeoutSeconds
+            )
+
         log.debug(
             "Tokens ({t}) matched {n} records",
             t=tokens, n=len(results)
@@ -256,6 +266,18 @@
         )
 
 
+    def setFilter(self, filter):
+        """
+        Assign a filter for post-processing recordsMatchingTokens and
+        recordsMatchingFields results.
+
+        @param filter: a callable taking the following args:
+            method, tokens, expression, recordTypes, limitResults, timeoutSeconds
+            ...and returning a deferred firing with the list of records
+        """
+        self._resultFilter = filter
+
+
     _oldRecordTypeNames = {
         "address": "addresses",
         "group": "groups",

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/who/groups.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/who/groups.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/who/groups.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -184,8 +184,10 @@
             homeID = rows[0][0]
             home = yield self.transaction.calendarHomeWithResourceID(homeID)
             calendar = yield home.childWithID(self.calendarID)
-            group = (yield self.transaction.groupByID(self.groupID))
-            yield calendar.reconcileGroupSharee(group.groupUID)
+            # Might be None if the calendar is in the trash or was removed before the work started
+            if calendar is not None:
+                group = (yield self.transaction.groupByID(self.groupID))
+                yield calendar.reconcileGroupSharee(group.groupUID)
 
 
 

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/accounts/groupAccounts.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/accounts/groupAccounts.xml	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/accounts/groupAccounts.xml	2015-07-28 02:43:19 UTC (rev 15011)
@@ -136,4 +136,21 @@
 	    <member-uid>group03</member-uid>
 	    <member-uid>user10</member-uid>
 	</record>
+	<record type="group">
+	    <short-name>group05</short-name>
+	    <uid>group05</uid>
+	    <guid>20000000-0000-0000-0000-000000000005</guid>
+	    <full-name>Group 05</full-name>
+	    <email>group05 at example.com</email>
+	    <member-uid>user01</member-uid>
+	    <member-uid>user02</member-uid>
+	</record>
+	<record type="group">
+	    <short-name>group06</short-name>
+	    <uid>group06</uid>
+	    <guid>20000000-0000-0000-0000-000000000006</guid>
+	    <full-name>Group 06</full-name>
+	    <email>group06 at example.com</email>
+	    <member-uid>user02</member-uid>
+	</record>
 </directory>

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_directory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_directory.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_directory.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -23,6 +23,7 @@
 from twext.who.directory import DirectoryRecord
 from twext.who.idirectory import FieldName, RecordType
 from txdav.who.directory import CalendarDirectoryRecordMixin, AutoScheduleMode
+from txdav.who.util import startswithFilter
 from uuid import UUID
 from twext.who.expression import (
     MatchType, MatchFlags, MatchExpression
@@ -50,7 +51,7 @@
 
         expanded = yield record.expandedMembers()
         self.assertEquals(
-            set([u"Chris Lecroy", u"Cyrus Daboo", u"David Reid", u"Wilfredo Sanchez"]),
+            set([u"Chris Lecroy", u"Cyrus Daboo", u"David Reid", u"Wilfredo Sanchez-Vega"]),
             set([r.displayName for r in expanded])
         )
 
@@ -205,6 +206,53 @@
 
 
     @inlineCallbacks
+    def test_recordsMatchingTokensNoFilter(self):
+        """
+        Records with names containing the token are returned
+        """
+
+        records = (yield self.directory.recordsMatchingTokens(
+            [u"anche"]
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("dre" in matchingShortNames)
+        self.assertTrue("wsanchez" in matchingShortNames)
+
+
+    @inlineCallbacks
+    def test_recordsMatchingTokensStartswithFilter(self):
+        """
+        Records with names starting with the token are returned, because of
+        the filter installed.  Note that hyphens and spaces are used to split
+        fullname into names.
+        """
+        self.directory.setFilter(startswithFilter)
+
+        records = (yield self.directory.recordsMatchingTokens(
+            [u"anche"]
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("dre" not in matchingShortNames)
+        self.assertTrue("wsanchez" not in matchingShortNames)
+
+        records = (yield self.directory.recordsMatchingTokens(
+            [u"vega", u"wilf"]
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("dre" not in matchingShortNames)
+        self.assertTrue("wsanchez" in matchingShortNames)
+
+
+    @inlineCallbacks
     def test_getAutoScheduleMode(self):
 
         apollo = yield self.directory.recordWithUID(u"apollo")

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_group_attendees.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_group_attendees.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_group_attendees.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -27,13 +27,14 @@
 from twistedcaldav.config import config
 from twistedcaldav.ical import Component, normalize_iCalStr
 from txdav.caldav.datastore.sql_directory import GroupAttendeeRecord
-from txdav.caldav.datastore.test.util import populateCalendarsFrom, CommonCommonTests
+from txdav.caldav.datastore.test.util import populateCalendarsFrom, CommonCommonTests, \
+    DateTimeSubstitutionsMixin
 from txdav.who.directory import CalendarDirectoryRecordMixin
 from txdav.who.groups import GroupCacher
 import os
 
 
-class GroupAttendeeTestBase(CommonCommonTests, unittest.TestCase):
+class GroupAttendeeTestBase(CommonCommonTests, DateTimeSubstitutionsMixin, unittest.TestCase):
     """
     GroupAttendeeReconciliation tests
     """
@@ -50,6 +51,8 @@
         )
         yield self.populate()
 
+        self.setupDateTimeValues()
+
         self.paths = {}
 
 
@@ -84,23 +87,26 @@
 
     def _assertICalStrEqual(self, iCalStr1, iCalStr2):
 
-        def orderMemberValues(event):
+        def orderAttendeePropAndMemberValues(event):
 
             for component in event.subcomponents(ignore=True):
                 # remove all values and add them again
                 # this is sort of a hack, better pycalendar has ordering
-                for attendeeProp in tuple(component.properties("ATTENDEE")):
+                attendees = sorted(list(component.properties("ATTENDEE")), key=lambda x: x.value())
+                component.removeProperty("ATTENDEE")
+                for attendeeProp in attendees:
                     if attendeeProp.hasParameter("MEMBER"):
                         parameterValues = tuple(attendeeProp.parameterValues("MEMBER"))
                         for paramterValue in parameterValues:
                             attendeeProp.removeParameterValue("MEMBER", paramterValue)
                         attendeeProp.setParameter("MEMBER", sorted(parameterValues))
+                    component.addProperty(attendeeProp)
 
             return event
 
-        self.assertEqual(
-            orderMemberValues(Component.fromString(normalize_iCalStr(iCalStr1))),
-            orderMemberValues(Component.fromString(normalize_iCalStr(iCalStr2)))
+        self.assertEqualCalendarData(
+            orderAttendeePropAndMemberValues(Component.fromString(normalize_iCalStr(iCalStr1))),
+            orderAttendeePropAndMemberValues(Component.fromString(normalize_iCalStr(iCalStr2)))
         )
 
 
@@ -121,7 +127,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20140101T100000Z
+DTSTART:{nowDate}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -137,7 +143,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20140101T100000Z
+DTSTART:{nowDate}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;RSVP=TRUE:urn:x-uid:user01
 ATTENDEE;CN=Group 02;CUTYPE=X-SERVER-GROUP;EMAIL=group02 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group02
@@ -154,13 +160,13 @@
         yield self._verifyObjectResourceCount("user06", 0)
         yield self._verifyObjectResourceCount("user07", 0)
 
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self._assertICalStrEqual(vcalendar, data_get_1)
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user06", 1)
         yield self._verifyObjectResourceCount("user07", 1)
@@ -180,7 +186,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20140101T100000Z
+DTSTART:{nowDate_fwd1}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -196,7 +202,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20140101T100000Z
+DTSTART:{nowDate_fwd1}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;RSVP=TRUE:urn:x-uid:user01
 ATTENDEE;CUTYPE=X-SERVER-GROUP;SCHEDULE-STATUS=3.7:urn:uuid:FFFFFFFF-EEEE-DDDD-CCCC-BBBBBBBBBBBB
@@ -207,13 +213,13 @@
 END:VCALENDAR
 """
 
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_1))
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
 
     @inlineCallbacks
@@ -230,7 +236,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20140101T100000Z
+DTSTART:{nowDate}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -247,7 +253,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20140101T100000Z
+DTSTART:{nowDate}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;RSVP=TRUE:urn:x-uid:user01
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:user02
@@ -258,13 +264,13 @@
 END:VEVENT
 END:VCALENDAR
 """
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_1))
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
 
     @inlineCallbacks
@@ -287,7 +293,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20140101T100000Z
+DTSTART:{nowDate}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -303,7 +309,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20140101T100000Z
+DTSTART:{nowDate}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;RSVP=TRUE:urn:x-uid:user01
 ATTENDEE;CN=Group 04;CUTYPE=X-SERVER-GROUP;SCHEDULE-STATUS=2.7:urn:x-uid:group04
@@ -319,13 +325,13 @@
 END:VCALENDAR
 """
 
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self._assertICalStrEqual(vcalendar, data_get_1)
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user06", 1)
         yield self._verifyObjectResourceCount("user07", 1)
@@ -353,7 +359,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20140101T100000Z
+DTSTART:{nowDate}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -371,7 +377,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20140101T100000Z
+DTSTART:{nowDate}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;RSVP=TRUE:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -387,13 +393,13 @@
 END:VEVENT
 END:VCALENDAR"""
 
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self._assertICalStrEqual(vcalendar, data_get_1)
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user06", 1)
         yield self._verifyObjectResourceCount("user07", 1)
@@ -404,7 +410,7 @@
     @inlineCallbacks
     def test_groupPutOldEvent(self):
         """
-        Test that old event with group attendee is expaned but not linked to group update
+        Test that old event with group attendee is expanded but not linked to group update
         """
 
         data_put_1 = """BEGIN:VCALENDAR
@@ -414,7 +420,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20140101T100000Z
+DTSTART:{nowDate_back2}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -430,7 +436,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20140101T100000Z
+DTSTART:{nowDate_back2}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -444,7 +450,7 @@
         groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user02")
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
@@ -459,7 +465,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_1))
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
 
 
@@ -478,7 +484,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -494,7 +500,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -511,7 +517,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -530,7 +536,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -558,13 +564,13 @@
         self.assertEqual(len(wps), 0)
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user02")
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2))
+        self._assertICalStrEqual(vcalendar, data_get_2.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user01", 0)
         yield self.commit()
@@ -582,7 +588,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_3))
+        self._assertICalStrEqual(vcalendar, data_get_3.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user01", 1)
         yield self.commit()
@@ -595,7 +601,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_4))
+        self._assertICalStrEqual(vcalendar, data_get_4.format(**self.dtsubs))
 
         cal = yield self.calendarUnderTest(name="calendar", home="user01")
         cobjs = yield cal.objectResources()
@@ -617,7 +623,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 SUMMARY:event {0}
 UID:event{0}@ninevah.local
@@ -633,7 +639,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event{0}@ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 0{0};EMAIL=user0{0}@example.com;RSVP=TRUE:urn:x-uid:user0{0}
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -650,7 +656,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event{0}@ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 0{0};EMAIL=user0{0}@example.com;RSVP=TRUE:urn:x-uid:user0{0}
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -669,7 +675,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event{0}@ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 0{0};EMAIL=user0{0}@example.com;RSVP=TRUE:urn:x-uid:user0{0}
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -700,13 +706,13 @@
 
         for i in userRange:
             calendar = yield self.calendarUnderTest(name="calendar", home="user0{0}".format(i))
-            vcalendar = Component.fromString(data_put_1.format(i))
+            vcalendar = Component.fromString(data_put_1.format(i, **self.dtsubs))
             yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
             yield self.commit()
 
             cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user0{0}".format(i))
             vcalendar = yield cobj.component()
-            self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2.format(i)))
+            self._assertICalStrEqual(vcalendar, data_get_2.format(i, **self.dtsubs))
 
         yield self._verifyObjectResourceCount("user01", 0)
         yield self.commit()
@@ -725,7 +731,7 @@
         for i in userRange:
             cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user0{0}".format(i))
             vcalendar = yield cobj.component()
-            self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_3.format(i)))
+            self._assertICalStrEqual(vcalendar, data_get_3.format(i, **self.dtsubs))
 
         yield self._verifyObjectResourceCount("user01", len(userRange))
         yield self.commit()
@@ -739,7 +745,7 @@
         for i in userRange:
             cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user0{0}".format(i))
             vcalendar = yield cobj.component()
-            self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_4.format(i)))
+            self._assertICalStrEqual(vcalendar, data_get_4.format(i, **self.dtsubs))
 
         cal = yield self.calendarUnderTest(name="calendar", home="user01")
         cobjs = yield cal.objectResources()
@@ -762,7 +768,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -779,7 +785,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20140101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -795,7 +801,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -813,7 +819,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20140101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -834,7 +840,7 @@
         groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user02")
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
@@ -848,11 +854,11 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_1))
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user01", 1)
 
-        vcalendar = Component.fromString(data_put_2)
+        vcalendar = Component.fromString(data_put_2.format(**self.dtsubs))
         yield cobj.setComponent(vcalendar)
         yield self.commit()
 
@@ -860,7 +866,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2))
+        self._assertICalStrEqual(vcalendar, data_get_2.format(**self.dtsubs))
 
         wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group01")
         self.assertEqual(len(wps), 0)
@@ -884,7 +890,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2))
+        self._assertICalStrEqual(vcalendar, data_get_2.format(**self.dtsubs))
 
         '''
         cal = yield self.calendarUnderTest(name="calendar", home="user01")
@@ -916,9 +922,9 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20120101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
-RRULE:FREQ=DAILY;UNTIL=20240101T100000
+RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SUMMARY:event 1
 UID:event1 at ninevah.local
 ORGANIZER:MAILTO:user02 at example.com
@@ -934,9 +940,9 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20120101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
-RRULE:FREQ=DAILY;UNTIL=20140101T100000
+RRULE:FREQ=DAILY;UNTIL={nowDate_back1}T100000
 SUMMARY:event 1
 UID:event1 at ninevah.local
 ORGANIZER:MAILTO:user02 at example.com
@@ -951,14 +957,14 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20120101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;MEMBER="urn:x-uid:group01";PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:user01
 CREATED:20060101T150000Z
 ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:user02
-RRULE:FREQ=DAILY;UNTIL=20240101T100000
+RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SUMMARY:event 1
 END:VEVENT
 END:VCALENDAR
@@ -970,13 +976,13 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20120101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
 CREATED:20060101T150000Z
 ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:user02
-RRULE:FREQ=DAILY;UNTIL=20140101T100000
+RRULE:FREQ=DAILY;UNTIL={nowDate_back1}T100000
 SEQUENCE:1
 SUMMARY:event 1
 END:VEVENT
@@ -992,7 +998,7 @@
         groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user02")
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
@@ -1006,11 +1012,11 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_1))
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user01", 1)
 
-        vcalendar = Component.fromString(data_put_2)
+        vcalendar = Component.fromString(data_put_2.format(**self.dtsubs))
         yield cobj.setComponent(vcalendar)
         yield self.commit()
 
@@ -1018,7 +1024,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2))
+        self._assertICalStrEqual(vcalendar, data_get_2.format(**self.dtsubs))
 
         wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group01")
         if len(wps): # This is needed because the test currently fails and does actually create job items we have to wait for
@@ -1043,7 +1049,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2))
+        self._assertICalStrEqual(vcalendar, data_get_2.format(**self.dtsubs))
 
 
     @inlineCallbacks
@@ -1059,9 +1065,9 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20120101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
-RRULE:FREQ=DAILY;UNTIL=20240101T100000
+RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SUMMARY:event 1
 UID:event1 at ninevah.local
 ORGANIZER:MAILTO:user02 at example.com
@@ -1076,14 +1082,14 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20120101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;MEMBER="urn:x-uid:group01";PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:user01
 CREATED:20060101T150000Z
 ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:user02
-RRULE:FREQ=DAILY;UNTIL=20240101T100000
+RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SUMMARY:event 1
 END:VEVENT
 END:VCALENDAR
@@ -1095,14 +1101,14 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20120101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com:urn:x-uid:group01
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;MEMBER="urn:x-uid:group01";PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user01
 CREATED:20060101T150000Z
 ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:user02
-RRULE:FREQ=DAILY;UNTIL=20240101T100000
+RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SUMMARY:event 1
 TRANSP:TRANSPARENT
 END:VEVENT
@@ -1120,7 +1126,7 @@
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
 CREATED:20060101T150000Z
 ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:user02
-{relatedTo}RRULE:FREQ=DAILY;UNTIL=20240101T100000
+{relatedTo}RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SEQUENCE:2
 SUMMARY:event 1
 END:VEVENT
@@ -1131,7 +1137,7 @@
 CALSCALE:GREGORIAN
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
-{uid}DTSTART:20120101T100000Z
+{uid}DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -1156,7 +1162,7 @@
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;MEMBER="urn:x-uid:group01";PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user01
 CREATED:20060101T150000Z
 ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:user02
-{relatedTo}RRULE:FREQ=DAILY;UNTIL=20240101T100000
+{relatedTo}RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SEQUENCE:2
 STATUS:CANCELLED
 SUMMARY:event 1
@@ -1169,7 +1175,7 @@
 CALSCALE:GREGORIAN
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
-{uid}DTSTART:20120101T100000Z
+{uid}DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com:urn:x-uid:group01
@@ -1191,21 +1197,26 @@
         groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user02")
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_1))
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
         cal = yield self.calendarUnderTest(name="calendar", home="user01")
         cobjs = yield cal.objectResources()
         self.assertEqual(len(cobjs), 1)
         vcalendar = yield cobjs[0].componentForUser()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_1_user01))
+        self._assertICalStrEqual(vcalendar, data_get_1_user01.format(**self.dtsubs))
         user01_cname = cobjs[0].name()
 
+        cal = yield self.calendarUnderTest(name="inbox", home="user01")
+        cobjs = yield cal.objectResources()
+        self.assertEqual(len(cobjs), 1)
+        yield cobjs[0].remove()
+
         self.patch(CalendarDirectoryRecordMixin, "expandedMembers", expandedMembers)
 
         wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group01")
@@ -1225,22 +1236,13 @@
                     "uid": component.getProperty("UID"),
                 }
                 break
+            props.update(self.dtsubs)
 
             if cobj.name() == "data1.ics":
-                self.assertEqual(
-                    normalize_iCalStr(vcalendar),
-                    normalize_iCalStr(
-                        data_get_2.format(**props)
-                    )
-                )
+                self._assertICalStrEqual(vcalendar, data_get_2.format(**props))
                 props_orig = props
             else:
-                self.assertEqual(
-                    normalize_iCalStr(vcalendar),
-                    normalize_iCalStr(
-                        data_get_3.format(**props)
-                    )
-                )
+                self._assertICalStrEqual(vcalendar, data_get_3.format(**props))
                 props_new = props
 
         cal = yield self.calendarUnderTest(name="calendar", home="user01")
@@ -1248,21 +1250,17 @@
         for cobj in cobjs:
             vcalendar = yield cobj.componentForUser()
             if cobj.name() == user01_cname:
-                self.assertEqual(
-                    normalize_iCalStr(vcalendar),
-                    normalize_iCalStr(
-                        data_get_2_user01.format(**props_orig)
-                    )
-                )
+                self._assertICalStrEqual(vcalendar, data_get_2_user01.format(**props_orig))
             else:
-                self.assertEqual(
-                    normalize_iCalStr(vcalendar),
-                    normalize_iCalStr(
-                        data_get_3_user01.format(**props_new)
-                    )
-                )
+                self._assertICalStrEqual(vcalendar, data_get_3_user01.format(**props_new))
 
+        cal = yield self.calendarUnderTest(name="inbox", home="user01")
+        cobjs = yield cal.objectResources()
+        self.assertEqual(len(cobjs), 1)
+        comp = yield cobjs[0].componentForUser()
+        self.assertTrue("METHOD:CANCEL" in str(comp))
 
+
     @inlineCallbacks
     def test_groupChangeLargerSpanningEvent(self):
         """
@@ -1276,9 +1274,9 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20120101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
-RRULE:FREQ=DAILY;UNTIL=20240101T100000
+RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SUMMARY:event 1
 UID:event1 at ninevah.local
 ORGANIZER:MAILTO:user02 at example.com
@@ -1293,13 +1291,13 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20120101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
 CREATED:20060101T150000Z
 ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:user02
-RRULE:FREQ=DAILY;UNTIL=20240101T100000
+RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SUMMARY:event 1
 END:VEVENT
 END:VCALENDAR
@@ -1317,7 +1315,7 @@
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;MEMBER="urn:x-uid:group01";PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:user01
 CREATED:20060101T150000Z
 ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:user02
-{relatedTo}RRULE:FREQ=DAILY;UNTIL=20240101T100000
+{relatedTo}RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SEQUENCE:2
 SUMMARY:event 1
 END:VEVENT
@@ -1329,7 +1327,7 @@
 CALSCALE:GREGORIAN
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
-{uid}DTSTART:20120101T100000Z
+{uid}DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -1353,7 +1351,7 @@
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;MEMBER="urn:x-uid:group01";PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user01
 CREATED:20060101T150000Z
 ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:user02
-{relatedTo}RRULE:FREQ=DAILY;UNTIL=20240101T100000
+{relatedTo}RRULE:FREQ=DAILY;UNTIL={nowDate_fwd20}T100000
 SEQUENCE:2
 SUMMARY:event 1
 TRANSP:TRANSPARENT
@@ -1373,13 +1371,13 @@
         groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user02")
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_1))
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user01", 0)
 
@@ -1402,31 +1400,19 @@
                     "uid": component.getProperty("UID"),
                 }
                 break
+            props.update(self.dtsubs)
 
             if cobj.name() == "data1.ics":
-                self.assertEqual(
-                    normalize_iCalStr(vcalendar),
-                    normalize_iCalStr(
-                        data_get_2.format(**props)
-                    )
-                )
+                self._assertICalStrEqual(vcalendar, data_get_2.format(**props))
                 props_orig = props
             else:
-                self.assertEqual(
-                    normalize_iCalStr(vcalendar),
-                    normalize_iCalStr(
-                        data_get_3.format(**props)
-                    )
-                )
+                self._assertICalStrEqual(vcalendar, data_get_3.format(**props))
 
         cal = yield self.calendarUnderTest(name="calendar", home="user01")
         cobjs = yield cal.objectResources()
         self.assertEqual(len(cobjs), 1)
         vcalendar = yield cobjs[0].componentForUser()
-        self.assertEqual(
-            normalize_iCalStr(vcalendar),
-            normalize_iCalStr(data_get_2_user01.format(**props_orig))
-        )
+        self._assertICalStrEqual(vcalendar, data_get_2_user01.format(**props_orig))
 
 
     @inlineCallbacks
@@ -1452,7 +1438,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -1470,7 +1456,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;RSVP=TRUE:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -1492,7 +1478,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;RSVP=TRUE:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -1515,16 +1501,16 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;RSVP=TRUE:urn:x-uid:user01
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
 ATTENDEE;CN=Group 02;CUTYPE=X-SERVER-GROUP;EMAIL=group02 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group02
 ATTENDEE;CN=Group 03;CUTYPE=X-SERVER-GROUP;EMAIL=group03 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group03
-ATTENDEE;CN=User 06;EMAIL=user06 at example.com;MEMBER="urn:x-uid:group02";PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:user06
 ATTENDEE;CN=User 07;EMAIL=user07 at example.com;MEMBER="urn:x-uid:group02","urn:x-uid:group03";PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:user07
 ATTENDEE;CN=User 08;EMAIL=user08 at example.com;MEMBER="urn:x-uid:group02","urn:x-uid:group03";PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:user08
 ATTENDEE;CN=User 09;EMAIL=user09 at example.com;MEMBER="urn:x-uid:group03";PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:user09
+ATTENDEE;CN=User 06;EMAIL=user06 at example.com;MEMBER="urn:x-uid:group02";PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:user06
 CREATED:20060101T150000Z
 ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
 SEQUENCE:2
@@ -1543,13 +1529,13 @@
                 result = yield unpatchedRecordWithUID(self, uid)
             returnValue(result)
 
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self._assertICalStrEqual(vcalendar, data_get_1)
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user06", 1)
         yield self._verifyObjectResourceCount("user07", 1)
@@ -1567,7 +1553,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self._assertICalStrEqual(vcalendar, data_get_1)
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
         # remove group members run cacher again
         self.patch(DirectoryService, "recordWithUID", recordWithUID)
@@ -1579,7 +1565,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self._assertICalStrEqual(vcalendar, data_get_2)
+        self._assertICalStrEqual(vcalendar, data_get_2.format(**self.dtsubs))
 
         cal = yield self.calendarUnderTest(name="calendar", home="user06")
         cobjs = yield cal.objectResources()
@@ -1598,7 +1584,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self._assertICalStrEqual(vcalendar, data_get_3)
+        self._assertICalStrEqual(vcalendar, data_get_3.format(**self.dtsubs))
 
         cal = yield self.calendarUnderTest(name="calendar", home="user06")
         cobjs = yield cal.objectResources()
@@ -1631,7 +1617,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -1648,7 +1634,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;RSVP=TRUE:urn:x-uid:user01
 ATTENDEE;CN=Group 02;CUTYPE=X-SERVER-GROUP;EMAIL=group02 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group02
@@ -1670,7 +1656,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -1686,7 +1672,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 01;EMAIL=user01 at example.com;RSVP=TRUE:urn:x-uid:user01
 ATTENDEE;CN=Group 02;CUTYPE=X-SERVER-GROUP;EMAIL=group02 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group02
@@ -1701,13 +1687,13 @@
 END:VCALENDAR
 """
 
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self._assertICalStrEqual(vcalendar, data_get_1)
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user06", 1)
         yield self._verifyObjectResourceCount("user07", 1)
@@ -1728,15 +1714,15 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self._assertICalStrEqual(vcalendar, data_get_1)
+        self._assertICalStrEqual(vcalendar, data_get_1.format(**self.dtsubs))
 
-        vcalendar = Component.fromString(data_put_2)
+        vcalendar = Component.fromString(data_put_2.format(**self.dtsubs))
         yield cobj.setComponent(vcalendar)
         yield self.commit()
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
         vcalendar = yield cobj.component()
-        self._assertICalStrEqual(vcalendar, data_get_2)
+        self._assertICalStrEqual(vcalendar, data_get_2.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user06", 1)
         yield self._verifyObjectResourceCount("user07", 1)
@@ -1774,7 +1760,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 SUMMARY:event 1
 UID:event1 at ninevah.local
@@ -1790,7 +1776,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20240101T100000Z
+DTSTART:{nowDate_fwd20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -1805,7 +1791,7 @@
         groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user02")
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
 
         groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
@@ -1822,7 +1808,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2))
+        self._assertICalStrEqual(vcalendar, data_get_2.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user01", 1)
         groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
@@ -1876,7 +1862,7 @@
 BEGIN:VEVENT
 DTSTAMP:20051222T205953Z
 CREATED:20060101T150000Z
-DTSTART:20140101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 RRULE:FREQ=DAILY
 DURATION:PT1H
 SUMMARY:event 1
@@ -1893,7 +1879,7 @@
 PRODID:-//Example Inc.//Example Calendar//EN
 BEGIN:VEVENT
 UID:event1 at ninevah.local
-DTSTART:20140101T100000Z
+DTSTART:{nowDate_back20}T100000Z
 DURATION:PT1H
 ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:user02
 ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;SCHEDULE-STATUS=2.7:urn:x-uid:group01
@@ -1909,7 +1895,7 @@
         groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user02")
-        vcalendar = Component.fromString(data_put_1)
+        vcalendar = Component.fromString(data_put_1.format(**self.dtsubs))
         yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
 
         groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
@@ -1926,7 +1912,7 @@
 
         cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user02")
         vcalendar = yield cobj.component()
-        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2))
+        self._assertICalStrEqual(vcalendar, data_get_2.format(**self.dtsubs))
 
         yield self._verifyObjectResourceCount("user01", 1)
         groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_group_sharees.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_group_sharees.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_group_sharees.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -28,7 +28,8 @@
 from txdav.who.directory import CalendarDirectoryRecordMixin
 from txdav.who.groups import GroupCacher, GroupShareeReconciliationWork
 import os
-from txdav.common.datastore.sql_tables import _BIND_MODE_GROUP
+from txdav.common.datastore.sql_tables import _BIND_MODE_GROUP, _BIND_MODE_WRITE, \
+    _BIND_MODE_GROUP_READ
 from txdav.common.datastore.sql_tables import _BIND_MODE_READ
 from txdav.common.datastore.sql_tables import _BIND_STATUS_INVITED
 
@@ -68,6 +69,7 @@
     requirements = {
         "user01" : None,
         "user02" : None,
+        "user03" : None,
         "user06" : None,
         "user07" : None,
         "user08" : None,
@@ -348,3 +350,499 @@
         yield calendar.uninviteUIDFromShare("group02")
         noinvites = yield calendar.sharingInvites()
         self.assertEqual(len(noinvites), 3)
+
+
+    @inlineCallbacks
+    def test_no_self_invite(self):
+        """
+        Test that group shares where the group includes the sharee work. Then remove
+        the sharee from the group and make sure it works.
+        """
+
+        record02 = yield self.transactionUnderTest().directoryService().recordWithUID("user02")
+
+        @inlineCallbacks
+        def expandedMembers(self, records=None, seen=None):
+
+            if self.uid == "group05":
+                returnValue(frozenset((record02,)))
+            else:
+                returnValue((yield unpatchedExpandedMembers(self, records, seen)))
+
+        unpatchedExpandedMembers = CalendarDirectoryRecordMixin.expandedMembers
+
+        # setup group cacher
+        groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 0)
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group05")
+        self.assertEqual(len(wps), 0)
+
+        yield self._check_notifications("user01", [])
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        yield self._check_notifications("user01", [])
+        shareeViews = yield calendar.inviteUIDToShare("group05", _BIND_MODE_READ)
+        self.assertEqual(len(shareeViews), 1)
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user01")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_READ)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        # 1 group members
+        self.patch(CalendarDirectoryRecordMixin, "expandedMembers", expandedMembers)
+
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group05")
+        self.assertEqual(len(wps), 1)
+        yield self.commit()
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user01")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_READ)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        yield self._check_notifications("user01", [])
+
+        # Uninvite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield calendar.uninviteUIDFromShare("group05")
+        noinvites = yield calendar.sharingInvites()
+        self.assertEqual(len(noinvites), 0)
+
+
+    @inlineCallbacks
+    def test_no_self_invite_on_add(self):
+        """
+        Test that the sharee is not invited to their own share when they are added as a member
+        of a group to whom the calendar is shared.
+        """
+
+        record01 = yield self.transactionUnderTest().directoryService().recordWithUID("user01")
+        record02 = yield self.transactionUnderTest().directoryService().recordWithUID("user02")
+
+        @inlineCallbacks
+        def expandedMembers(self, records=None, seen=None):
+
+            if self.uid == "group06":
+                returnValue(frozenset((record01, record02,)))
+            else:
+                returnValue((yield unpatchedExpandedMembers(self, records, seen)))
+
+        unpatchedExpandedMembers = CalendarDirectoryRecordMixin.expandedMembers
+
+        # setup group cacher
+        groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 0)
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group06")
+        self.assertEqual(len(wps), 0)
+
+        yield self._check_notifications("user01", [])
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        yield self._check_notifications("user01", [])
+        shareeViews = yield calendar.inviteUIDToShare("group06", _BIND_MODE_READ)
+        self.assertEqual(len(shareeViews), 1)
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user01")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_READ)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        # 1 group members
+        self.patch(CalendarDirectoryRecordMixin, "expandedMembers", expandedMembers)
+
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group06")
+        self.assertEqual(len(wps), 1)
+        yield self.commit()
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user01")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_READ)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        yield self._check_notifications("user01", [])
+
+        # Uninvite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield calendar.uninviteUIDFromShare("group06")
+        noinvites = yield calendar.sharingInvites()
+        self.assertEqual(len(noinvites), 0)
+
+
+    @inlineCallbacks
+    def test_group_change_trashed_calendar(self):
+        """
+        Test that group shares are properly cleaned when a calendar is trashed.
+        """
+
+        self.patch(config, "EnableTrashCollection", True)
+
+        @inlineCallbacks
+        def expandedMembers(self, records=None, seen=None):
+
+            if self.uid == "group02":
+                returnValue(frozenset())
+            else:
+                returnValue((yield unpatchedExpandedMembers(self, records, seen)))
+
+        unpatchedExpandedMembers = CalendarDirectoryRecordMixin.expandedMembers
+
+        # setup group cacher
+        groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 0)
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group02")
+        self.assertEqual(len(wps), 0)
+
+        yield self._check_notifications("user01", [])
+
+        # New calendar for sharing
+        home = yield self.homeUnderTest(name="user01")
+        yield home.createCalendarWithName("shared")
+        yield self.commit()
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="shared")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        yield self._check_notifications("user01", [])
+        shareeViews = yield calendar.inviteUIDToShare("group02", _BIND_MODE_READ)
+        self.assertEqual(len(shareeViews), 3)
+        calendar = yield self.calendarUnderTest(home="user01", name="shared")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 3)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user01")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_READ)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 1)
+        yield self.commit()
+
+        # Trash the collection
+        calendar = yield self.calendarUnderTest(home="user01", name="shared")
+        yield calendar.remove()
+        self.assertTrue(calendar.isInTrash())
+        trash_id = calendar.id()
+        yield self.commit()
+
+        home = yield self.homeUnderTest(name="user01")
+        calendar = yield home.childWithID(trash_id, onlyInTrash=True)
+        self.assertTrue(calendar.isInTrash())
+        invitations = yield calendar.allInvitations()
+        self.assertEqual(len(invitations), 0)
+        yield self.commit()
+
+        # 1 group member
+        self.patch(CalendarDirectoryRecordMixin, "expandedMembers", expandedMembers)
+
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group02")
+        self.assertEqual(len(wps), 0)
+        yield self.commit()
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
+
+
+    @inlineCallbacks
+    def test_group_change_removed_calendar(self):
+        """
+        Test that group shares are properly cleaned when a calendar is removed (and not trashed).
+        """
+
+        self.patch(config, "EnableTrashCollection", False)
+
+        @inlineCallbacks
+        def expandedMembers(self, records=None, seen=None):
+
+            if self.uid == "group02":
+                returnValue(frozenset())
+            else:
+                returnValue((yield unpatchedExpandedMembers(self, records, seen)))
+
+        unpatchedExpandedMembers = CalendarDirectoryRecordMixin.expandedMembers
+
+        # setup group cacher
+        groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 0)
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group02")
+        self.assertEqual(len(wps), 0)
+
+        yield self._check_notifications("user01", [])
+
+        # New calendar for sharing
+        home = yield self.homeUnderTest(name="user01")
+        yield home.createCalendarWithName("shared")
+        yield self.commit()
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="shared")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        yield self._check_notifications("user01", [])
+        shareeViews = yield calendar.inviteUIDToShare("group02", _BIND_MODE_READ)
+        self.assertEqual(len(shareeViews), 3)
+        calendar = yield self.calendarUnderTest(home="user01", name="shared")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 3)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user01")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_READ)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 1)
+        yield self.commit()
+
+        # Remove the collection
+        calendar = yield self.calendarUnderTest(home="user01", name="shared")
+        remove_id = calendar.id()
+        yield calendar.remove()
+        yield self.commit()
+
+        home = yield self.homeUnderTest(name="user01")
+        calendar = yield home.childWithID(remove_id)
+        self.assertTrue(calendar is None)
+        calendar = yield home.childWithID(remove_id, onlyInTrash=True)
+        self.assertTrue(calendar is None)
+        yield self.commit()
+
+        # 1 group member
+        self.patch(CalendarDirectoryRecordMixin, "expandedMembers", expandedMembers)
+
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group02")
+        self.assertEqual(len(wps), 0)
+        yield self.commit()
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
+
+
+    @inlineCallbacks
+    def test_multiple_groups_remove_from_one_group(self):
+        """
+        Test that a multi-group share each containing the same user still lists the user
+        when they are removed from one group.
+        """
+
+        @inlineCallbacks
+        def expandedMembers(self, records=None, seen=None):
+
+            if self.uid == "group05":
+                returnValue(frozenset())
+            else:
+                returnValue((yield unpatchedExpandedMembers(self, records, seen)))
+
+        unpatchedExpandedMembers = CalendarDirectoryRecordMixin.expandedMembers
+
+        # setup group cacher
+        groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 0)
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group05")
+        self.assertEqual(len(wps), 0)
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group06")
+        self.assertEqual(len(wps), 0)
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user03", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeViews = yield calendar.inviteUIDToShare("group05", _BIND_MODE_WRITE)
+        self.assertEqual(len(shareeViews), 2)
+        shareeViews = yield calendar.inviteUIDToShare("group06", _BIND_MODE_READ)
+        self.assertEqual(len(shareeViews), 1)
+
+        calendar = yield self.calendarUnderTest(home="user03", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 2)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user03")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_WRITE)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 2)
+
+        # Change group membership
+        self.patch(CalendarDirectoryRecordMixin, "expandedMembers", expandedMembers)
+
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group05")
+        self.assertEqual(len(wps), 1)
+        yield self.commit()
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
+
+        calendar = yield self.calendarUnderTest(home="user03", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        for invite in invites:
+            self.assertEqual(invite.shareeUID, "user02")
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user03")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_READ)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        # Uninvite
+        calendar = yield self.calendarUnderTest(home="user03", name="calendar")
+        yield calendar.uninviteUIDFromShare("group05")
+        yield calendar.uninviteUIDFromShare("group06")
+        noinvites = yield calendar.sharingInvites()
+        self.assertEqual(len(noinvites), 0)
+
+
+    @inlineCallbacks
+    def test_multiple_groups_with_individual_remove_from_one_group(self):
+        """
+        Test that a multi-group share each containing the same user still lists the user
+        when they are removed from one group.
+        """
+
+        @inlineCallbacks
+        def expandedMembers(self, records=None, seen=None):
+
+            if self.uid == "group05":
+                returnValue(frozenset())
+            else:
+                returnValue((yield unpatchedExpandedMembers(self, records, seen)))
+
+        unpatchedExpandedMembers = CalendarDirectoryRecordMixin.expandedMembers
+
+        # setup group cacher
+        groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 0)
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group05")
+        self.assertEqual(len(wps), 0)
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group06")
+        self.assertEqual(len(wps), 0)
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user03", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUIDToShare("user01", _BIND_MODE_READ)
+        self.assertTrue(shareeView is not None)
+        shareeViews = yield calendar.inviteUIDToShare("group05", _BIND_MODE_WRITE)
+        self.assertEqual(len(shareeViews), 2)
+        shareeViews = yield calendar.inviteUIDToShare("group06", _BIND_MODE_READ)
+        self.assertEqual(len(shareeViews), 1)
+
+        calendar = yield self.calendarUnderTest(home="user03", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 2)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user03")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            if invite.shareeUID == "user01":
+                self.assertEqual(invite.mode, _BIND_MODE_GROUP_READ)
+            else:
+                self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_WRITE)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 2)
+
+        # Change group membership
+        self.patch(CalendarDirectoryRecordMixin, "expandedMembers", expandedMembers)
+
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group05")
+        self.assertEqual(len(wps), 1)
+        yield self.commit()
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
+
+        calendar = yield self.calendarUnderTest(home="user03", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 2)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user03")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            if invite.shareeUID == "user01":
+                self.assertEqual(invite.mode, _BIND_MODE_READ)
+            else:
+                self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_READ)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        # Uninvite
+        calendar = yield self.calendarUnderTest(home="user03", name="calendar")
+        yield calendar.uninviteUIDFromShare("user01")
+        yield calendar.uninviteUIDFromShare("group05")
+        yield calendar.uninviteUIDFromShare("group06")
+        noinvites = yield calendar.sharingInvites()
+        self.assertEqual(len(noinvites), 0)

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_util.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_util.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -114,6 +114,7 @@
                     "InProcessCachingSeconds": 60,
                     "InSidecarCachingSeconds": 120,
                 },
+                "DirectoryFilterStartsWith": False,
             }
         )
 

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_wiki.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_wiki.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/who/test/test_wiki.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -23,7 +23,7 @@
 
 
 from twisted.trial import unittest
-from twisted.internet.defer import inlineCallbacks, succeed
+from twisted.internet.defer import inlineCallbacks, succeed, TimeoutError
 from twistedcaldav.test.util import StoreTestCase
 
 from ..wiki import (
@@ -94,6 +94,8 @@
 
 
     def stubAccessForUserToWiki(self, *args, **kwds):
+        if hasattr(self, "raiseTimeout"):
+            raise TimeoutError
         return succeed(self.access)
 
 
@@ -117,8 +119,14 @@
         access = yield record.accessForRecord(None)
         self.assertEquals(access, WikiAccessLevel.write)
 
+        self.access = "write"
+        self.raiseTimeout = True
+        access = yield record.accessForRecord(None)
+        self.assertEquals(access, WikiAccessLevel.none)
 
 
+
+
 # Test getWikiACL()
 # Currently stubs out enough functionality to test that an unauthenticated
 # request can support read access when generating an ACL element

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/who/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/who/util.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/who/util.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -14,7 +14,8 @@
 # limitations under the License.
 ##
 
-
+import re
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twext.python.log import Logger
 from twext.python.types import MappingProxyType
 from twext.who.aggregate import DirectoryService as AggregateDirectoryService
@@ -69,14 +70,15 @@
         config.AugmentService,
         config.Authentication.Wiki,
         serversDB=serversDB,
-        cachingSeconds=config.DirectoryProxy.InSidecarCachingSeconds
+        cachingSeconds=config.DirectoryProxy.InSidecarCachingSeconds,
+        filterStartsWith=config.DirectoryFilterStartsWith
     )
 
 
 
 def buildDirectory(
     store, dataRoot, servicesInfo, augmentServiceInfo, wikiServiceInfo,
-    serversDB=None, cachingSeconds=0
+    serversDB=None, cachingSeconds=0, filterStartsWith=False
 ):
     """
     Return a directory without using a config object; suitable for tests
@@ -315,6 +317,9 @@
     if serversDB is not None:
         augmented.setServersDB(serversDB)
 
+    if filterStartsWith:
+        augmented.setFilter(startswithFilter)
+
     return augmented
 
 
@@ -327,3 +332,49 @@
 
 <augments/>
 """
+
+
+ at inlineCallbacks
+def startswithFilter(
+    method, tokens, expression, recordTypes=None, records=None,
+    limitResults=None, timeoutSeconds=None
+):
+    """
+    Call the passed-in method to retrieve records from the directory, but
+    further filter the results by only returning records whose email addresses
+    and names actually start with the tokens.  Without this filter, it's only
+    required that the record's fullname *contains* the tokens. "Names" are split
+    from the record's fullname, delimited by whitespace and hypens.
+    """
+
+    tokens = [t.lower() for t in tokens]
+
+    results = []
+    records = yield method(
+        expression, recordTypes=recordTypes, limitResults=1000,
+        timeoutSeconds=timeoutSeconds
+    )
+    count = 0
+    for record in records:
+        try:
+            names = list(record.emailAddresses)
+        except AttributeError:
+            names = []
+        for fullName in record.fullNames:
+            names.extend(re.split(' |-', fullName))
+        match = True # assume it will match
+        for token in tokens:
+            for name in names:
+                if name.lower().startswith(token):
+                    break
+            else:
+                # there was no match for this token
+                match = False
+                break
+        if match:
+            results.append(record)
+            count += 1
+            if limitResults and count == limitResults:
+                break
+
+    returnValue(results)

Modified: CalendarServer/branches/users/cdaboo/cfod/txdav/who/wiki.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cfod/txdav/who/wiki.py	2015-07-28 02:31:38 UTC (rev 15010)
+++ CalendarServer/branches/users/cdaboo/cfod/txdav/who/wiki.py	2015-07-28 02:43:19 UTC (rev 15011)
@@ -33,9 +33,11 @@
 from twext.who.idirectory import FieldName as BaseFieldName
 from twext.who.util import ConstantsContainer
 from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.internet.defer import (
+    inlineCallbacks, returnValue, succeed, TimeoutError
+)
 from twisted.python.constants import Names, NamedConstant
-from twisted.web.client import HTTPPageGetter, HTTPClientFactory
+from twisted.web.client import HTTPClientFactory
 from twisted.web.error import Error as WebError
 from txdav.who.directory import CalendarDirectoryRecordMixin
 from txdav.who.idirectory import FieldName
@@ -218,6 +220,12 @@
             )
             returnValue(WikiAccessLevel.none)
 
+        except TimeoutError as e:
+            self.log.error(
+                "Wiki request timed out"
+            )
+            returnValue(WikiAccessLevel.none)
+
         try:
             returnValue({
                 "no-access": WikiAccessLevel.none,
@@ -439,7 +447,6 @@
         otherwise a twisted.web.error.Error is the result.
     """
     point = endpoints.clientFromString(reactor, descriptor)
-    factory = HTTPClientFactory(url)
-    factory.protocol = HTTPPageGetter
+    factory = HTTPClientFactory(url, timeout=10)
     point.connect(factory)
     return factory.deferred
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150727/e34ab53f/attachment-0001.html>


More information about the calendarserver-changes mailing list