[CalendarServer-changes] [10757] CalendarServer/branches/users/gaya/sharedgroups

source_changes at macosforge.org source_changes at macosforge.org
Mon Feb 18 20:08:00 PST 2013


Revision: 10757
          http://trac.calendarserver.org//changeset/10757
Author:   gaya at apple.com
Date:     2013-02-18 20:08:00 -0800 (Mon, 18 Feb 2013)
Log Message:
-----------
checkpoint first pass merge from trunk

Modified Paths:
--------------
    CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_dkimtool
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/logAnalysis.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/root.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/test/test_root.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/amppush.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/applepush.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_amppush.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_applepush.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/caldav.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/util.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/test/test_logAnalysis.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/ampnotifications.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/calverify.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/changeip_calendar.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/cmdline.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dbinspect.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dkimtool.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/export.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/gateway.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/loadaugmentdb.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/managetimezones.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/migrate_verify.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/obliterate.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/purge.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/push.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/shell/cmd.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_changeip.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_export.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge_old_events.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/upgrade.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/util.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/validcalendardata.py
    CalendarServer/branches/users/gaya/sharedgroups/conf/auth/augments-test.xml
    CalendarServer/branches/users/gaya/sharedgroups/conf/auth/augments.dtd
    CalendarServer/branches/users/gaya/sharedgroups/conf/auth/resources-test.xml
    CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist
    CalendarServer/branches/users/gaya/sharedgroups/conf/localservers-test.xml
    CalendarServer/branches/users/gaya/sharedgroups/conf/localservers.xml
    CalendarServer/branches/users/gaya/sharedgroups/conf/remoteservers-test.xml
    CalendarServer/branches/users/gaya/sharedgroups/conf/remoteservers.xml
    CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarcommonextra.py
    CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarpromotion.py
    CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_commonextra.py
    CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_promotion.py
    CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/benchmarks/find_calendars.py
    CalendarServer/branches/users/gaya/sharedgroups/pyflakes
    CalendarServer/branches/users/gaya/sharedgroups/run
    CalendarServer/branches/users/gaya/sharedgroups/setup.py
    CalendarServer/branches/users/gaya/sharedgroups/support/build.sh
    CalendarServer/branches/users/gaya/sharedgroups/support/patchapply
    CalendarServer/branches/users/gaya/sharedgroups/support/patchmaker
    CalendarServer/branches/users/gaya/sharedgroups/twext/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/ienterprise.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/queue.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/test/test_queue.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/python/test/test_timezone.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/python/timezone.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/web2/auth/digest.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/method/propfind.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/test/test_auth.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/test/util.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_http_headers.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_httpauth.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_server.py
    CalendarServer/branches/users/gaya/sharedgroups/twisted/plugins/caldav.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/client/geturl.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/customxml.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/aggregate.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/digest.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_digest.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_directory.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_proxyprincipalmembers.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/util.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/wiki.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/instance.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/memcacheprops.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/propfind.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_common.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/resource.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/delivery.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/resource.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/scheduler.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_delivery.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_resource.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/delivery.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/mailgateway.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/resource.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/scheduler.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_delivery.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_mailgateway.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/implicit.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/delivery.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/dkim.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/localservers.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/remoteservers.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/resource.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/scheduler.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_delivery.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_dkim.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_localservers.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_resource.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_utils.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/utils.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/xml.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/itip.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/processing.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_icaldiff.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/utils.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/sharing.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/storebridge.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_config.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_resource.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_sharing.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_wrapping.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/util.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/upgrade.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/base/propertystore/appledouble_xattr.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/common.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_attachments.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/icalendarstore.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/sql.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/common.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/iaddressbookstore.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/common.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/file.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_legacy.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v11.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v12.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v13.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_12_to_13.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_12_to_13.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_13_to_14.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/test/test_sql_schema_files.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/test/util.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/upgrade/sql/others/attachment_migration.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/upgrade/test/test_migrate.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/icommondatastore.py

Added Paths:
-----------
    CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_icalendar_validate
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/notifier.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_notifier.py
    CalendarServer/branches/users/gaya/sharedgroups/conf/test/
    CalendarServer/branches/users/gaya/sharedgroups/conf/test/accounts.xml
    CalendarServer/branches/users/gaya/sharedgroups/doc/Client-Server/calendar-client.rst
    CalendarServer/branches/users/gaya/sharedgroups/lib-patches/setproctitle/
    CalendarServer/branches/users/gaya/sharedgroups/lib-patches/setproctitle/HAVE_DECL_STRLCPY.patch
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/directory.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/idirectory.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/index.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_directory.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_xml.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/util.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/xml.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/accounts.xml
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/resources.xml
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_pocessing.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_utils.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/3765A955-1B96-41EA-994D-335192BEDCCD.vcf
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/44745975-AE6D-4FB0-80A6-A298427E047A.vcf
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/44EE78BF-8814-4471-899C-92280CEFB098.vcf
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/8424B7F0-C878-4722-B522-EBB07CF48AD7.vcf
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/934731C6-1C95-4C40-BE1F-FA4215B2307B.vcf
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/AFBB77B8-0438-4825-A1DB-A75D76B6C3A8.vcf
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1.vcf
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E2.vcf
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/F0A6918D-8E09-43FA-9684-226810B8A96F.vcf
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/FCBA0FA3-00B2-4C95-B4EC-4CCC4843F8B1.vcf
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home_bad/addressbook/
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home_empty/
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home_empty/addressbook/
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/ReadMe.txt
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/oracle-dialect/v15.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v15.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_15_to_16.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_15_to_16.sql

Removed Paths:
-------------
    CalendarServer/branches/users/gaya/sharedgroups/conf/test/accounts.xml
    CalendarServer/branches/users/gaya/sharedgroups/lib-patches/setproctitle/HAVE_DECL_STRLCPY.patch
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/directory.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/idirectory.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/index.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_directory.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_xml.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/util.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/who/xml.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home_bad/addressbook_bad/

Property Changed:
----------------
    CalendarServer/branches/users/gaya/sharedgroups/


Property changes on: CalendarServer/branches/users/gaya/sharedgroups
___________________________________________________________________
Modified: svn:ignore
   - .dependencies
*.tgz
data
logs
build
*.pyc
*.pyo
_run
.settings
.project
.pydevproject

   + .dependencies
*.tgz
data
logs
build
*.pyc
*.pyo
_run
.settings
.project
.pydevproject
parsecalendardata.py
dopstats.py

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

Modified: CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_dkimtool
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_dkimtool	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_dkimtool	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Copied: CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_icalendar_validate (from rev 10753, CalendarServer/trunk/bin/calendarserver_icalendar_validate)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_icalendar_validate	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_icalendar_validate	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        try:
+            import _calendarserver_preamble
+        except ImportError:
+            sys.exc_clear()
+
+    from calendarserver.tools.validcalendardata import main
+    main()

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/logAnalysis.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/logAnalysis.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/logAnalysis.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/root.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/root.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/root.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -208,31 +208,6 @@
         for filter in self.contentFilters:
             request.addResponseFilter(filter[0], atEnd=filter[1])
 
-        # Examine headers for our special internal authorization, used for
-        # POSTing to /inbox between workers and mail gateway sidecar.
-        if not hasattr(request, "checkedInternalAuthHeader"):
-            request.checkedInternalAuthHeader = True
-            headerName = config.Scheduling.iMIP.Header
-            secrets = request.headers.getRawHeaders(headerName, None)
-            secretVerified = False
-            if secrets is not None:
-                log.debug("Internal authentication header (%s) detected" %
-                    (headerName,))
-                for secret in secrets:
-                    if secret == config.Scheduling.iMIP.Password:
-                        secretVerified = True
-                        break
-
-            if secretVerified:
-                log.debug("Internal authentication header (%s) verified" %
-                    (headerName,))
-                guid = config.Scheduling.iMIP.GUID
-                log.debug("Internal principal %s being assigned to authnUser and authzUser" % (guid,))
-                request.authzUser = request.authnUser = davxml.Principal(
-                    davxml.HRef.fromString("/principals/__uids__/%s/" % (guid,))
-                )
-
-
         # Examine cookies for wiki auth token; if there, ask the paired wiki
         # server for the corresponding record name.  If that maps to a
         # principal, assign that to authnuser.

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/test/test_root.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/test/test_root.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/test/test_root.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -36,7 +36,6 @@
 
 from calendarserver.provision.root import RootResource
 from twistedcaldav.directory import augment
-from twistedcaldav.config import config
 
 class FakeCheckSACL(object):
     def __init__(self, sacls=None):
@@ -298,35 +297,8 @@
         except HTTPError, e:
             self.assertEquals(e.response.code, 401)
 
-    @inlineCallbacks
-    def test_internalAuthHeader(self):
-        """
-        Test the behavior of locateChild when x-calendarserver-internal
-        header is set.
 
-        authnuser and authzuser will be set to the internal principal
-        """
-        self.patch(config.Scheduling.iMIP, "Password", "xyzzy")
 
-        headers = http_headers.Headers({})
-        headers.setRawHeaders("x-calendarserver-internal", ["xyzzy"])
-
-        request = SimpleRequest(
-            self.site,
-            "GET",
-            "/principals/",
-            headers=headers,
-        )
-
-        resrc, segments = (yield
-            RootResource.locateChild(self.root.resource, request, ["principals"]
-        ))
-
-        expected = "<?xml version='1.0' encoding='UTF-8'?>\n<principal xmlns='DAV:'>\r\n  <href>/principals/__uids__/%s/</href>\r\n</principal>" % (config.Scheduling.iMIP.GUID,)
-        self.assertEquals(request.authnUser.toxml(), expected)
-        self.assertEquals(request.authzUser.toxml(), expected)
-
-
     def test_DELETE(self):
         def do_test(response):
             response = IResponse(response)

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/amppush.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/amppush.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/amppush.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -16,13 +16,11 @@
 
 from calendarserver.push.util import PushScheduler
 from twext.python.log import Logger, LoggingMixIn
-from twext.python.log import LoggingMixIn
 from twisted.application.internet import StreamServerEndpointService
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.endpoints import TCP4ClientEndpoint, TCP4ServerEndpoint
 from twisted.internet.protocol import Factory, ServerFactory
 from twisted.protocols import amp
-from twistedcaldav.notify import getPubSubPath
 import time
 import uuid
 
@@ -51,6 +49,7 @@
 
 # Server classes
 
+
 class AMPPushNotifierService(StreamServerEndpointService, LoggingMixIn):
     """
     AMPPushNotifierService allows clients to use AMP to subscribe to,
@@ -58,16 +57,17 @@
     """
 
     @classmethod
-    def makeService(cls, settings, ignored, serverHostName, reactor=None):
-        return cls(settings, serverHostName, reactor=reactor)
+    def makeService(cls, settings, ignored, reactor=None):
+        return cls(settings, reactor=reactor)
 
-    def __init__(self, settings, serverHostName, reactor=None):
+    def __init__(self, settings, reactor=None):
         if reactor is None:
             from twisted.internet import reactor
         factory = AMPPushNotifierFactory(self)
         endpoint = TCP4ServerEndpoint(reactor, settings["Port"])
         super(AMPPushNotifierService, self).__init__(endpoint, factory)
         self.subscribers = []
+        self.dataHost = settings["DataHost"]
 
         if settings["EnableStaggering"]:
             self.scheduler = PushScheduler(reactor, self.sendNotification,
@@ -75,8 +75,6 @@
         else:
             self.scheduler = None
 
-        self.serverHostName = serverHostName
-
     def addSubscriber(self, p):
         self.log_debug("Added subscriber")
         self.subscribers.append(p)
@@ -85,13 +83,10 @@
         self.log_debug("Removed subscriber")
         self.subscribers.remove(p)
 
-    def enqueue(self, op, id, dataChangedTimestamp=None):
+    def enqueue(self, id, dataChangedTimestamp=None):
         """
         Sends an AMP push notification to any clients subscribing to this id.
 
-        @param op: The operation that took place, either "create" or "update"
-            (ignored in this implementation)
-        @type op: C{str}
         @param id: The identifier of the resource that was updated, including
             a prefix indicating whether this is CalDAV or CardDAV related.
             The prefix is separated from the id with "|", e.g.:
@@ -108,18 +103,18 @@
         """
 
         try:
-            id.split("|", 1)
+            protocol, id = id.split("|", 1)
         except ValueError:
             # id has no protocol, so we can't do anything with it
             self.log_error("Notification id '%s' is missing protocol" % (id,))
             return
 
-        id = getPubSubPath(id, {"host": self.serverHostName})
-
         # Unit tests can pass this value in; otherwise it defaults to now
         if dataChangedTimestamp is None:
             dataChangedTimestamp = int(time.time())
 
+        id = "/%s/%s/%s/" % (protocol, self.dataHost, id)
+
         tokens = []
         for subscriber in self.subscribers:
             token = subscriber.subscribedToID(id)

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/applepush.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/applepush.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/applepush.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -59,7 +59,7 @@
     """
 
     @classmethod
-    def makeService(cls, settings, store, serverHostName, testConnectorClass=None,
+    def makeService(cls, settings, store, testConnectorClass=None,
         reactor=None):
         """
         Creates the various "subservices" that work together to implement
@@ -177,14 +177,11 @@
 
 
     @inlineCallbacks
-    def enqueue(self, op, id, dataChangedTimestamp=None):
+    def enqueue(self, id, dataChangedTimestamp=None):
         """
         Sends an Apple Push Notification to any device token subscribed to
         this id.
 
-        @param op: The operation that took place, either "create" or "update"
-            (ignored in this implementation)
-        @type op: C{str}
         @param id: The identifier of the resource that was updated, including
             a prefix indicating whether this is CalDAV or CardDAV related.
             The prefix is separated from the id with "|", e.g.:

Copied: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/notifier.py (from rev 10753, CalendarServer/trunk/calendarserver/push/notifier.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/notifier.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/notifier.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,209 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Notification framework for Calendar Server
+"""
+
+from twext.python.log import LoggingMixIn, Logger
+
+from twisted.internet.defer import inlineCallbacks, succeed
+from twext.enterprise.dal.record import fromTable
+from twext.enterprise.queue import WorkItem
+from txdav.common.datastore.sql_tables import schema
+from twisted.application import service
+from twisted.python.reflect import namedClass
+
+
+log = Logger()
+
+
+class PushNotificationWork(WorkItem, fromTable(schema.PUSH_NOTIFICATION_WORK)):
+
+    @inlineCallbacks
+    def doWork(self):
+
+        # FIXME: Coalescing goes here?
+
+        pushService = self.transaction._pushService
+        if pushService is not None:
+            yield pushService.enqueue(self.pushID)
+
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Classes used within calendarserver itself
+#
+
+class Notifier(LoggingMixIn):
+    """
+    Provides a hook for sending change notifications to the
+    L{NotifierFactory}.
+    """
+
+    def __init__(self, notifierFactory, label="default", id=None, prefix=None):
+        self._notifierFactory = notifierFactory
+        self._ids = { label : self.normalizeID(id) }
+        self._notify = True
+        self._prefix = prefix
+
+    def normalizeID(self, id):
+        urn = "urn:uuid:"
+        try:
+            if id.startswith(urn):
+                return id[len(urn):]
+        except AttributeError:
+            pass
+        return id
+
+    def enableNotify(self, arg):
+        self.log_debug("enableNotify: %s" % (self._ids['default'][1],))
+        self._notify = True
+
+    def disableNotify(self):
+        self.log_debug("disableNotify: %s" % (self._ids['default'][1],))
+        self._notify = False
+
+    @inlineCallbacks
+    def notify(self):
+        for label in self._ids.iterkeys():
+            id = self.getID(label=label)
+            if id is not None:
+                if self._notify:
+                    self.log_debug("Notifications are enabled: %s %s" %
+                        (label, id))
+                    yield self._notifierFactory.send(id)
+                else:
+                    self.log_debug("Skipping notification for: %s" % (id,))
+
+    def clone(self, label="default", id=None):
+        newNotifier = self.__class__(self._notifierFactory)
+        newNotifier._ids = self._ids.copy()
+        newNotifier._ids[label] = id
+        newNotifier._prefix = self._prefix
+        return newNotifier
+
+    def addID(self, label="default", id=None):
+        self._ids[label] = self.normalizeID(id)
+
+    def getID(self, label="default"):
+        id = self._ids.get(label, None)
+        if self._prefix is None:
+            return id
+        else:
+            return "%s|%s" % (self._prefix, id)
+
+    def nodeName(self, label="default"):
+        id = self.getID(label=label)
+        return succeed(self._notifierFactory.pushKeyForId(id))
+
+
+class NotifierFactory(LoggingMixIn):
+    """
+    Notifier Factory
+
+    Creates Notifier instances and forwards notifications from them to the
+    work queue.
+    """
+
+    def __init__(self, store, hostname, reactor=None):
+        self.store = store
+        self.hostname = hostname
+
+        if reactor is None:
+            from twisted.internet import reactor
+        self.reactor = reactor
+
+    @inlineCallbacks
+    def send(self, id):
+        txn = self.store.newTransaction()
+        yield txn.enqueue(PushNotificationWork, pushID=id)
+        yield txn.commit()
+
+    def newNotifier(self, label="default", id=None, prefix=None):
+        return Notifier(self, label=label, id=id, prefix=prefix)
+
+    def pushKeyForId(self, id):
+        path = "/"
+
+        try:
+            prefix, id = id.split("|", 1)
+            path += "%s/" % (prefix,)
+        except ValueError:
+            # id has no prefix
+            pass
+
+        path += "%s/" % (self.hostname,)
+        if id:
+            path += "%s/" % (id,)
+        return path
+
+
+
+def getPubSubAPSConfiguration(id, config):
+    """
+    Returns the Apple push notification settings specific to the notifier
+    ID, which includes a prefix that is either "CalDAV" or "CardDAV"
+    """
+    try:
+        prefix, id = id.split("|", 1)
+    except ValueError:
+        # id has no prefix, so we can't look up APS config
+        return None
+
+    # If we are directly talking to apple push, advertise those settings
+    applePushSettings = config.Notifications.Services.ApplePushNotifier
+    if applePushSettings.Enabled:
+        settings = {}
+        settings["APSBundleID"] = applePushSettings[prefix]["Topic"]
+        if config.EnableSSL:
+            url = "https://%s:%s/%s" % (config.ServerHostName, config.SSLPort,
+                applePushSettings.SubscriptionURL)
+        else:
+            url = "http://%s:%s/%s" % (config.ServerHostName, config.HTTPPort,
+                applePushSettings.SubscriptionURL)
+        settings["SubscriptionURL"] = url
+        settings["SubscriptionRefreshIntervalSeconds"] = applePushSettings.SubscriptionRefreshIntervalSeconds
+        settings["APSEnvironment"] = applePushSettings.Environment
+        return settings
+
+    return None
+
+
+class PushService(service.MultiService):
+    """
+    A Service which passes along notifications to the protocol-specific subservices
+    """
+
+    @classmethod
+    def makeService(cls, settings, store):
+        multiService = cls()
+        for key, subSettings in settings.Services.iteritems():
+            if subSettings["Enabled"]:
+                subService = namedClass(subSettings["Service"]).makeService(
+                    subSettings, store)
+                subService.setServiceParent(multiService)
+                multiService.subServices.append(subService)            
+        return multiService
+
+    def __init__(self):
+        service.MultiService.__init__(self)
+        self.subServices = []
+
+    @inlineCallbacks
+    def enqueue(self, id):
+        for subService in self.subServices:
+            yield subService.enqueue(id)

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_amppush.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_amppush.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_amppush.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -31,12 +31,13 @@
             "Port" : 62311,
             "EnableStaggering" : True,
             "StaggerSeconds" : 3,
+            "DataHost" : "localhost",
         }
 
         # Set up the service
         clock = Clock()
         service = (yield AMPPushNotifierService.makeService(settings,
-            None, "localhost", reactor=clock))
+            None, reactor=clock))
 
         self.assertEquals(service.subscribers, [])
 
@@ -68,7 +69,7 @@
         self.assertTrue(client3.subscribedToID("/CalDAV/localhost/user03/"))
 
         dataChangedTimestamp = 1354815999
-        service.enqueue("update", "CalDAV|user01", dataChangedTimestamp=dataChangedTimestamp)
+        service.enqueue("CalDAV|user01", dataChangedTimestamp=dataChangedTimestamp)
         self.assertEquals(len(client1.history), 0)
         self.assertEquals(len(client2.history), 0)
         self.assertEquals(len(client3.history), 0)
@@ -85,7 +86,7 @@
         client1.reset()
         client2.reset()
         client2.unsubscribe("token2", "/CalDAV/localhost/user01/")
-        service.enqueue("update", "CalDAV|user01", dataChangedTimestamp=dataChangedTimestamp)
+        service.enqueue("CalDAV|user01", dataChangedTimestamp=dataChangedTimestamp)
         self.assertEquals(len(client1.history), 0)
         clock.advance(1)
         self.assertEquals(client1.history, [(NotificationForID, {'id': '/CalDAV/localhost/user01/', 'dataChangedTimestamp' : 1354815999})])
@@ -98,7 +99,7 @@
         client1.reset()
         client2.reset()
         client2.subscribe("token2", "/CalDAV/localhost/user01/")
-        service.enqueue("update", "CalDAV|user01", dataChangedTimestamp=dataChangedTimestamp)
+        service.enqueue("CalDAV|user01", dataChangedTimestamp=dataChangedTimestamp)
         self.assertEquals(client1.history, [(NotificationForID, {'id': '/CalDAV/localhost/user01/', 'dataChangedTimestamp' : 1354815999})])
         self.assertEquals(client2.history, [(NotificationForID, {'id': '/CalDAV/localhost/user01/', 'dataChangedTimestamp' : 1354815999})])
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_applepush.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_applepush.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_applepush.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -118,7 +118,7 @@
         # Set up the service
         clock = Clock()
         service = (yield ApplePushNotifierService.makeService(settings,
-            self.store, "localhost", testConnectorClass=TestConnector, reactor=clock))
+            self.store, testConnectorClass=TestConnector, reactor=clock))
         self.assertEquals(set(service.providers.keys()), set(["CalDAV","CardDAV"]))
         self.assertEquals(set(service.feedbacks.keys()), set(["CalDAV","CardDAV"]))
 
@@ -127,7 +127,7 @@
 
         # Notification arrives from calendar server
         dataChangedTimestamp = 1354815999
-        yield service.enqueue("update", "CalDAV|user01/calendar",
+        yield service.enqueue("CalDAV|user01/calendar",
             dataChangedTimestamp=dataChangedTimestamp)
 
         # The notifications should be in the queue
@@ -167,7 +167,7 @@
         # Reset sent data
         providerConnector.transport.data = None
         # Send notification while service is connected
-        yield service.enqueue("update", "CalDAV|user01/calendar")
+        yield service.enqueue("CalDAV|user01/calendar")
         clock.advance(1) # so that first push is sent
         self.assertEquals(len(providerConnector.transport.data), 183)
         # Reset sent data

Copied: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_notifier.py (from rev 10753, CalendarServer/trunk/calendarserver/push/test/test_notifier.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_notifier.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/push/test/test_notifier.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,57 @@
+##
+# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.test.util import TestCase
+from twistedcaldav.config import ConfigDict
+from calendarserver.push.notifier import PushService
+from twisted.internet.defer import inlineCallbacks, succeed
+from twisted.application import service
+
+class StubService(service.Service):
+    def __init__(self, settings, store):
+        self.settings = settings
+        self.store = store
+        self.reset()
+
+    def reset(self):
+        self.history = []
+
+    def enqueue(self, id):
+        self.history.append(id)
+        return(succeed(None))
+
+    @classmethod
+    def makeService(cls, settings, store):
+        return cls(settings, store)
+
+class PushServiceTests(TestCase):
+
+    @inlineCallbacks
+    def test_enqueue(self):
+        settings = ConfigDict({
+            "Services" : {
+                "Stub" : {
+                    "Service" : "calendarserver.push.test.test_notifier.StubService",
+                    "Enabled" : True,
+                    "Foo" : "Bar",
+                },
+            },
+        })
+        svc = PushService.makeService(settings, None)
+        yield svc.enqueue("testing")
+        self.assertEquals(svc.subServices[0].history, ["testing"])
+
+

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/caldav.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/caldav.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -52,7 +52,6 @@
 from twisted.application.service import MultiService, IServiceMaker
 from twisted.application.service import Service
 
-import twext
 from twext.web2.server import Site
 from twext.python.log import Logger, LoggingMixIn
 from twext.python.log import logLevelForNamespace, setLogLevelForNamespace
@@ -71,7 +70,6 @@
 from twistedcaldav.config import ConfigurationError
 from twistedcaldav.config import config
 from twistedcaldav.localization import processLocalizationFiles
-from twistedcaldav.scheduling.imip.resource import IMIPReplyInboxResource
 from twistedcaldav import memcachepool
 from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
 from twistedcaldav.upgrade import UpgradeFileSystemFormatService, PostDBImportService
@@ -107,6 +105,8 @@
 from calendarserver.tap.util import oracleConnectorFromConfig
 from calendarserver.tap.cfgchild import ConfiguredChildSpawner
 from calendarserver.tools.util import checkDirectory
+from calendarserver.push.notifier import PushService
+from twistedcaldav.scheduling.imip.inbound import MailRetriever
 
 try:
     from calendarserver.version import version
@@ -118,8 +118,9 @@
     from version import version as getVersion
     version = "%s (%s*)" % getVersion()
 
-twext.web2.server.VERSION = "CalendarServer/%s %s" % (
-    version.replace(" ", ""), twext.web2.server.VERSION,
+from twext.web2.server import VERSION as TWISTED_VERSION
+TWISTED_VERSION = "CalendarServer/%s %s" % (
+    version.replace(" ", ""), TWISTED_VERSION,
 )
 
 log = Logger()
@@ -510,8 +511,6 @@
     L{DelayedStartupProcessMonitor}:
 
         - regular slave processes (CalDAV workers)
-        - notifier
-        - mail gateway
     """
 
     def __init__(self, maker, monitor, dispenser, dispatcher, configPath,
@@ -540,51 +539,7 @@
             )
             self.monitor.addProcessObject(process, PARENT_ENVIRONMENT)
 
-        if (
-            config.Notifications.Enabled and
-            config.Notifications.InternalNotificationHost == "localhost"
-        ):
-            self.maker.log_info("Adding notification service")
 
-            notificationsArgv = [
-                sys.executable,
-                sys.argv[0],
-            ]
-            if config.UserName:
-                notificationsArgv.extend(("-u", config.UserName))
-            if config.GroupName:
-                notificationsArgv.extend(("-g", config.GroupName))
-            notificationsArgv.extend((
-                "--reactor=%s" % (config.Twisted.reactor,),
-                "-n", self.maker.notifierTapName,
-                "-f", self.configPath,
-            ))
-            self.monitor.addProcess("notifications", notificationsArgv,
-                env=PARENT_ENVIRONMENT)
-
-        if (
-            config.Scheduling.iMIP.Enabled and
-            config.Scheduling.iMIP.MailGatewayServer == "localhost"
-        ):
-            self.maker.log_info("Adding mail gateway service")
-
-            mailGatewayArgv = [
-                sys.executable,
-                sys.argv[0],
-            ]
-            if config.UserName:
-                mailGatewayArgv.extend(("-u", config.UserName))
-            if config.GroupName:
-                mailGatewayArgv.extend(("-g", config.GroupName))
-            mailGatewayArgv.extend((
-                "--reactor=%s" % (config.Twisted.reactor,),
-                "-n", self.maker.mailGatewayTapName,
-                "-f", self.configPath,
-            ))
-
-            self.monitor.addProcess("mailgateway", mailGatewayArgv,
-                               env=PARENT_ENVIRONMENT)
-
         if config.GroupCaching.Enabled and config.GroupCaching.EnableUpdater:
             self.maker.log_info("Adding group caching service")
 
@@ -667,8 +622,6 @@
     #
     # Default tap names
     #
-    mailGatewayTapName = "caldav_mailgateway"
-    notifierTapName = "caldav_notifier"
     groupMembershipCacherTapName = "caldav_groupcacher"
 
 
@@ -758,9 +711,33 @@
         pool, txnFactory = getDBPool(config)
         store = storeFromConfig(config, txnFactory)
         result = self.requestProcessingService(options, store)
+        directory = result.rootResource.getDirectory()
         if pool is not None:
             pool.setServiceParent(result)
 
+
+        # Optionally set up push notifications
+        if config.Notifications.Enabled:
+            pushService = PushService.makeService(config.Notifications, store)
+            pushService.setServiceParent(result)
+        else:
+            pushService = None
+
+        # Optionally set up mail retrieval
+        if config.Scheduling.iMIP.Enabled:
+            mailRetriever = MailRetriever(store, directory,
+                config.Scheduling.iMIP.Receiving)
+            mailRetriever.setServiceParent(result)
+        else:
+            mailRetriever = None
+
+        def decorateTransaction(txn):
+            txn._pushService = pushService
+            txn._rootResource = result.rootResource
+            txn._mailRetriever = mailRetriever
+
+        store.callWithNewTransactions(decorateTransaction)
+
         # Optionally enable Manhole access
         if config.Manhole.Enabled:
             try:
@@ -773,7 +750,7 @@
                         "config" : config,
                         "service" : result,
                         "store" : store,
-                        "directory" : result.rootResource.getDirectory(),
+                        "directory" : directory,
                         },
                     "passwd" : config.Manhole.PasswordFilePath,
                 })
@@ -801,9 +778,9 @@
         oldLogLevel = logLevelForNamespace(None)
         setLogLevelForNamespace(None, "info")
 
+        # Note: 'additional' was used for IMIP reply resource, and perhaps
+        # we can remove this
         additional = []
-        if config.Scheduling.iMIP.Enabled:
-            additional.append(("inbox", IMIPReplyInboxResource, [], ("digest",)))
 
         #
         # Configure the service
@@ -836,7 +813,7 @@
             from txdav.common.datastore.sql import CommonDataStore as SQLStore
             if isinstance(store, SQLStore):
                 def queueMasterAvailable(connectionFromMaster):
-                    store.queuer = connectionFromMaster
+                    store.queuer = store.queuer.transferProposalCallbacks(connectionFromMaster)
                 queueFactory = QueueWorkerFactory(store.newTransaction, schema,
                                                   queueMasterAvailable)
                 controlSocketClient.addFactory(_QUEUE_ROUTE, queueFactory)
@@ -1352,6 +1329,7 @@
             except ImportError:
                 print "Manhole access could not enabled because manhole_tap could not be imported"
 
+
         # Finally, let's get the real show on the road.  Create a service that
         # will spawn all of our worker processes when started, and wrap that
         # service in zero to two necessary layers before it's started: first,

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/util.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/util.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -57,7 +57,7 @@
 from twistedcaldav.directory.internal import InternalDirectoryService
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
 from twistedcaldav.directory.wiki import WikiDirectoryService
-from twistedcaldav.notify import NotifierFactory, getPubSubConfiguration
+from calendarserver.push.notifier import NotifierFactory 
 from calendarserver.push.applepush import APNSubscriptionResource
 from twistedcaldav.directorybackedaddressbook import DirectoryBackedAddressBookResource
 from twistedcaldav.resource import AuthenticationWrapper
@@ -223,11 +223,9 @@
     # Configure NotifierFactory
     #
     if config.Notifications.Enabled:
-        notifierFactory = NotifierFactory(
-            config.Notifications.InternalNotificationHost,
-            config.Notifications.InternalNotificationPort,
-            pubSubConfig=getPubSubConfiguration(config)
-        )
+        # FIXME: NotifierFactory needs reference to the store in order
+        # to get a txn in order to create a Work item
+        notifierFactory = NotifierFactory(None, config.ServerHostName)
     else:
         notifierFactory = None
     quota = config.UserQuota
@@ -239,10 +237,11 @@
         else:
             uri = "http://%s:%s" % (config.ServerHostName, config.HTTPPort,)
         attachments_uri = uri + "/calendars/__uids__/%(home)s/dropbox/%(dropbox_id)s/%(name)s"
-        return CommonSQLDataStore(
+        store = CommonSQLDataStore(
             txnFactory, notifierFactory,
             FilePath(config.AttachmentsRoot), attachments_uri,
             config.EnableCalDAV, config.EnableCardDAV,
+            config.EnableManagedAttachments,
             quota=quota,
             logLabels=config.LogDatabase.LabelsInSQL,
             logStats=config.LogDatabase.Statistics,
@@ -255,11 +254,14 @@
             cacheExpireSeconds=config.QueryCaching.ExpireSeconds
         )
     else:
-        return CommonFileDataStore(
+        store = CommonFileDataStore(
             FilePath(config.DocumentRoot),
             notifierFactory, config.EnableCalDAV, config.EnableCardDAV,
             quota=quota
         )
+    if notifierFactory is not None:
+        notifierFactory.store = store
+    return store
 
 
 
@@ -267,7 +269,6 @@
     """
     Create an L{AggregateDirectoryService} from the given configuration.
     """
-
     #
     # Setup the Augment Service
     #
@@ -361,7 +362,7 @@
 
 
 
-def getRootResource(config, newStore, resources=None):
+def getRootResource(config, newStore, resources=None, directory=None):
     """
     Set up directory service and resource hierarchy based on config.
     Return root resource.
@@ -397,7 +398,8 @@
     directoryBackedAddressBookResourceClass = DirectoryBackedAddressBookResource
     apnSubscriptionResourceClass = APNSubscriptionResource
 
-    directory = directoryFromConfig(config)
+    if directory is None:
+        directory = directoryFromConfig(config)
 
     #
     # Setup the ProxyDB Service
@@ -705,6 +707,10 @@
         directory,
     )
 
+    # FIXME:  Storing a reference to the root resource on the store
+    # until scheduling no longer needs resource objects
+    newStore.rootResource = root
+
     return logWrapper
 
 
@@ -787,7 +793,7 @@
 
 class FakeRequest(object):
 
-    def __init__(self, rootResource, method, path, uri='/'):
+    def __init__(self, rootResource, method, path, uri='/', transaction=None):
         self.rootResource = rootResource
         self.method = method
         self.path = path
@@ -795,6 +801,8 @@
         self._resourcesByURL = {}
         self._urlsByResource = {}
         self.headers = Headers()
+        if transaction is not None:
+            self._newStoreTransaction = transaction
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/test/test_logAnalysis.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/test/test_logAnalysis.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/test/test_logAnalysis.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/ampnotifications.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/ampnotifications.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/ampnotifications.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -15,19 +15,20 @@
 # limitations under the License.
 ##
 
+from calendarserver.push.amppush import subscribeToIDs
 from calendarserver.tools.cmdline import utilityMain
+
 from getopt import getopt, GetoptError
+
 from twext.python.log import Logger
+
 from twisted.application.service import Service
 from twisted.internet.defer import inlineCallbacks, succeed
 from twistedcaldav.config import ConfigurationError
+
 import os
 import sys
 
-from twisted.internet.defer import inlineCallbacks, succeed
-
-from calendarserver.push.amppush import subscribeToIDs
-
 log = Logger()
 
 def usage(e=None):
@@ -42,6 +43,7 @@
     print "  -f --config <path>: Specify caldavd.plist configuration path"
     print "  -p --port <port>: AMP port to connect to"
     print "  -s --server <hostname>: AMP server to connect to"
+    print "  --debug: verbose logging"
     print ""
 
     if e:
@@ -51,11 +53,13 @@
         sys.exit(0)
 
 
+
 class WorkerService(Service):
 
     def __init__(self, store):
         self._store = store
 
+
     @inlineCallbacks
     def startService(self):
         try:
@@ -67,6 +71,7 @@
             raise
 
 
+
 class MonitorAMPNotifications(WorkerService):
 
     ids = []
@@ -77,12 +82,14 @@
         return monitorAMPNotifications(self.hostname, self.port, self.ids)
 
 
+
 def main():
 
     try:
         (optargs, args) = getopt(
-            sys.argv[1:], "f:hp:s:", [
+            sys.argv[1:], "f:hp:s:v", [
                 "config=",
+                "debug",
                 "help",
                 "port=",
                 "server=",
@@ -97,6 +104,7 @@
     configFileName = None
     hostname = "localhost"
     port = 62311
+    debug = False
 
     for opt, arg in optargs:
         if opt in ("-h", "--help"):
@@ -111,13 +119,15 @@
         elif opt in ("-s", "--server"):
             hostname = arg
 
+        elif opt in ("--debug"):
+            debug = True
+
         else:
             raise NotImplementedError(opt)
 
     if not args:
         usage("Not enough arguments")
 
-
     MonitorAMPNotifications.ids = args
     MonitorAMPNotifications.hostname = hostname
     MonitorAMPNotifications.port = port
@@ -125,12 +135,17 @@
     utilityMain(
         configFileName,
         MonitorAMPNotifications,
+        verbose=debug
     )
 
+
+
 def notificationCallback(id, dataChangedTimestamp):
     print "Received notification for:", id
     return succeed(True)
 
+
+
 @inlineCallbacks
 def monitorAMPNotifications(hostname, port, ids):
     print "Subscribing to notifications..."

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/calverify.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/calverify.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/calverify.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -272,6 +272,7 @@
     optFlags = [
         ['ical', 'i', "Calendar data check."],
         ['badcua', 'b', "Calendar data check for bad CALENDARSERVER-OLD-CUA only."],
+        ['debug', 'D', "Debug logging."],
         ['nobase64', 'n', "Do not apply CALENDARSERVER-OLD-CUA base64 transform when fixing."],
         ['mismatch', 's', "Detect organizer/attendee mismatches."],
         ['missing', 'm', "Show 'orphaned' homes."],
@@ -1659,7 +1660,7 @@
             calendar = yield home.childWithID(calendarID)
             calendarObj = yield calendar.objectResourceWithID(resid)
             objname = calendarObj.name()
-            yield calendar._removeObjectResource(calendarObj)
+            yield calendar.removeObjectResource(calendarObj)
             yield self.txn.commit()
             self.txn = self.store.newTransaction()
 
@@ -1927,7 +1928,7 @@
         config.TransactionTimeoutSeconds = 0
         return CalVerifyService(store, options, output, reactor, config)
 
-    utilityMain(options['config'], makeService, reactor)
+    utilityMain(options['config'], makeService, reactor, verbose=options['debug'])
 
 if __name__ == '__main__':
     main()

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/changeip_calendar.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/changeip_calendar.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/changeip_calendar.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -98,9 +98,6 @@
     keys = (
         ("Authentication", "Wiki", "Hostname"),
         ("BindAddresses",),
-        ("Notifications", "Services", "XMPPNotifier", "Host"),
-        ("Notifications", "Services", "XMPPNotifier", "JID"),
-        ("Notifications", "Services", "XMPPNotifier", "ServiceAddress"),
         ("Scheduling", "iMIP", "Receiving", "Server"),
         ("Scheduling", "iMIP", "Sending", "Server"),
         ("Scheduling", "iMIP", "Sending", "Address"),

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/cmdline.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/cmdline.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/cmdline.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -18,15 +18,19 @@
 Shared main-point between utilities.
 """
 
-import os, sys
-
 from calendarserver.tap.caldav import CalDAVServiceMaker, CalDAVOptions
 from calendarserver.tools.util import loadConfig, autoDisableMemcached
+
+from twext.python.log import StandardIOObserver
+
 from twistedcaldav.config import ConfigurationError
 
-# TODO: direct unit tests for this function.
+import os
+import sys
 
-def utilityMain(configFileName, serviceClass, reactor=None, serviceMaker=CalDAVServiceMaker):
+# TODO: direct unit tests for these functions.
+
+def utilityMain(configFileName, serviceClass, reactor=None, serviceMaker=CalDAVServiceMaker, verbose=False):
     """
     Shared main-point for utilities.
 
@@ -52,6 +56,12 @@
         L{IReactorTCP} (etc) provider to use.  If C{None}, the default reactor
         will be imported and used.
     """
+
+    # Install std i/o observer
+    if verbose:
+        observer = StandardIOObserver()
+        observer.start()
+
     if reactor is None:
         from twisted.internet import reactor
     try:

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dbinspect.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dbinspect.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dbinspect.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -79,6 +79,7 @@
 
     optFlags = [
         ['verbose', 'v', "Verbose logging."],
+        ['debug', 'D', "Debug logging."],
         ['purging', 'p', "Enable Purge command."],
     ]
 
@@ -90,10 +91,14 @@
         super(DBInspectOptions, self).__init__()
         self.outputName = '-'
 
+
+
 def UserNameFromUID(txn, uid):
     record = txn._directory.recordWithGUID(uid)
     return record.shortNames[0] if record else "(%s)" % (uid,)
 
+
+
 def UIDFromInput(txn, value):
     try:
         return str(UUID(value)).upper()
@@ -109,6 +114,8 @@
         record = txn._directory.recordWithShortName(recordType_groups, value)
     return record.guid if record else None
 
+
+
 class Cmd(object):
 
     _name = None
@@ -117,10 +124,12 @@
     def name(cls):
         return cls._name
 
+
     def doIt(self, txn):
         raise NotImplementedError
 
 
+
 class TableSizes(Cmd):
 
     _name = "Show Size of each Table"
@@ -147,6 +156,7 @@
         print "Database Tables (total=%d):\n" % (len(results),)
         table.printTable()
 
+
     @inlineCallbacks
     def getTableSize(self, txn, dbtable):
         rows = (yield Select(
@@ -156,6 +166,7 @@
         returnValue(rows[0][0])
 
 
+
 class CalendarHomes(Cmd):
 
     _name = "List Calendar Homes"
@@ -182,6 +193,7 @@
         print "Calendar Homes (total=%d, missing=%d):\n" % (len(uids), missing,)
         table.printTable()
 
+
     @inlineCallbacks
     def getAllHomeUIDs(self, txn):
         ch = schema.CALENDAR_HOME
@@ -192,6 +204,7 @@
         returnValue(tuple([row[0] for row in rows]))
 
 
+
 class CalendarHomesSummary(Cmd):
 
     _name = "List Calendar Homes with summary information"
@@ -202,7 +215,7 @@
         uids = yield self.getCalendars(txn)
 
         results = {}
-        for uid, calname, count in sorted(uids, key=lambda x:x[0]):
+        for uid, calname, count in sorted(uids, key=lambda x: x[0]):
             totalname, totalcount = results.get(uid, (0, 0,))
             if calname != "inbox":
                 totalname += 1
@@ -236,6 +249,7 @@
         print "Calendars with resource count (total=%d):\n" % (len(results),)
         table.printTable()
 
+
     @inlineCallbacks
     def getCalendars(self, txn):
         ch = schema.CALENDAR_HOME
@@ -256,6 +270,7 @@
         returnValue(tuple(rows))
 
 
+
 class Calendars(Cmd):
 
     _name = "List Calendars"
@@ -268,7 +283,7 @@
         # Print table of results
         table = tables.Table()
         table.addHeader(("Owner UID", "Short Name", "Calendar", "Resources"))
-        for uid, calname, count in sorted(uids, key=lambda x:(x[0], x[1])):
+        for uid, calname, count in sorted(uids, key=lambda x: (x[0], x[1])):
             shortname = UserNameFromUID(txn, uid)
             table.addRow((
                 uid,
@@ -281,6 +296,7 @@
         print "Calendars with resource count (total=%d):\n" % (len(uids),)
         table.printTable()
 
+
     @inlineCallbacks
     def getCalendars(self, txn):
         ch = schema.CALENDAR_HOME
@@ -301,6 +317,7 @@
         returnValue(tuple(rows))
 
 
+
 class CalendarsByOwner(Cmd):
 
     _name = "List Calendars for Owner UID/Short Name"
@@ -316,7 +333,7 @@
         table = tables.Table()
         table.addHeader(("Owner UID", "Short Name", "Calendars", "ID", "Resources"))
         totals = [0, 0, ]
-        for uid, calname, resid, count in sorted(uids, key=lambda x:x[1]):
+        for uid, calname, resid, count in sorted(uids, key=lambda x: x[1]):
             shortname = UserNameFromUID(txn, uid)
             table.addRow((
                 uid if totals[0] == 0 else "",
@@ -333,6 +350,7 @@
         print "Calendars with resource count (total=%d):\n" % (len(uids),)
         table.printTable()
 
+
     @inlineCallbacks
     def getCalendars(self, txn, uid):
         ch = schema.CALENDAR_HOME
@@ -351,10 +369,11 @@
                 co, type="left", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
             Where=(ch.OWNER_UID == Parameter("UID")),
             GroupBy=(ch.OWNER_UID, cb.CALENDAR_RESOURCE_NAME, co.CALENDAR_RESOURCE_ID)
-        ).on(txn, **{"UID":uid}))
+        ).on(txn, **{"UID": uid}))
         returnValue(tuple(rows))
 
 
+
 class Events(Cmd):
 
     _name = "List Events"
@@ -367,7 +386,7 @@
         # Print table of results
         table = tables.Table()
         table.addHeader(("Owner UID", "Short Name", "Calendar", "ID", "Type", "UID"))
-        for uid, calname, id, caltype, caluid in sorted(uids, key=lambda x:(x[0], x[1])):
+        for uid, calname, id, caltype, caluid in sorted(uids, key=lambda x: (x[0], x[1])):
             shortname = UserNameFromUID(txn, uid)
             table.addRow((
                 uid,
@@ -382,6 +401,7 @@
         print "Calendar events (total=%d):\n" % (len(uids),)
         table.printTable()
 
+
     @inlineCallbacks
     def getEvents(self, txn):
         ch = schema.CALENDAR_HOME
@@ -402,6 +422,8 @@
         ).on(txn))
         returnValue(tuple(rows))
 
+
+
 class EventsByCalendar(Cmd):
 
     _name = "List Events for a specific calendar"
@@ -420,7 +442,7 @@
         # Print table of results
         table = tables.Table()
         table.addHeader(("Type", "UID", "Resource Name", "Resource ID",))
-        for caltype, caluid, rname, rid in sorted(uids, key=lambda x:x[1]):
+        for caltype, caluid, rname, rid in sorted(uids, key=lambda x: x[1]):
             table.addRow((
                 caltype,
                 caluid,
@@ -432,6 +454,7 @@
         print "Calendar events (total=%d):\n" % (len(uids),)
         table.printTable()
 
+
     @inlineCallbacks
     def getEvents(self, txn, rid):
         ch = schema.CALENDAR_HOME
@@ -449,9 +472,11 @@
                     cb.BIND_MODE == _BIND_MODE_OWN)).join(
                 co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
             Where=(co.CALENDAR_RESOURCE_ID == Parameter("RID")),
-        ).on(txn, **{"RID":rid}))
+        ).on(txn, **{"RID": rid}))
         returnValue(tuple(rows))
 
+
+
 class EventDetails(Cmd):
     """
     Base class for common event details commands.
@@ -471,6 +496,7 @@
         table.printTable()
         print data
 
+
     @inlineCallbacks
     def getEventData(self, txn, whereClause, whereParams):
         ch = schema.CALENDAR_HOME
@@ -494,6 +520,8 @@
         ).on(txn, **whereParams))
         returnValue(tuple(rows))
 
+
+
 class Event(EventDetails):
 
     _name = "Get Event Data by Resource-ID"
@@ -501,7 +529,6 @@
     @inlineCallbacks
     def doIt(self, txn):
 
-
         rid = raw_input("Resource-ID: ")
         try:
             int(rid)
@@ -514,11 +541,13 @@
         else:
             print "Could not find resource"
 
+
     def getData(self, txn, rid):
         co = schema.CALENDAR_OBJECT
-        return self.getEventData(txn, (co.RESOURCE_ID == Parameter("RID")), {"RID":rid})
+        return self.getEventData(txn, (co.RESOURCE_ID == Parameter("RID")), {"RID": rid})
 
 
+
 class EventsByUID(EventDetails):
 
     _name = "Get Event Data by iCalendar UID"
@@ -526,7 +555,6 @@
     @inlineCallbacks
     def doIt(self, txn):
 
-
         uid = raw_input("UID: ")
         rows = yield self.getData(txn, uid)
         if rows:
@@ -535,11 +563,13 @@
         else:
             print "Could not find icalendar data"
 
+
     def getData(self, txn, uid):
         co = schema.CALENDAR_OBJECT
-        return self.getEventData(txn, (co.ICALENDAR_UID == Parameter("UID")), {"UID":uid})
+        return self.getEventData(txn, (co.ICALENDAR_UID == Parameter("UID")), {"UID": uid})
 
 
+
 class EventsByName(EventDetails):
 
     _name = "Get Event Data by resource name"
@@ -547,7 +577,6 @@
     @inlineCallbacks
     def doIt(self, txn):
 
-
         name = raw_input("Resource Name: ")
         rows = yield self.getData(txn, name)
         if rows:
@@ -556,11 +585,13 @@
         else:
             print "Could not find icalendar data"
 
+
     def getData(self, txn, name):
         co = schema.CALENDAR_OBJECT
-        return self.getEventData(txn, (co.RESOURCE_NAME == Parameter("NAME")), {"NAME":name})
+        return self.getEventData(txn, (co.RESOURCE_NAME == Parameter("NAME")), {"NAME": name})
 
 
+
 class EventsByOwner(EventDetails):
 
     _name = "Get Event Data by Owner UID/Short Name"
@@ -568,7 +599,6 @@
     @inlineCallbacks
     def doIt(self, txn):
 
-
         uid = raw_input("Owner UID/Name: ")
         uid = UIDFromInput(txn, uid)
         rows = yield self.getData(txn, uid)
@@ -578,11 +608,13 @@
         else:
             print "Could not find icalendar data"
 
+
     def getData(self, txn, uid):
         ch = schema.CALENDAR_HOME
-        return self.getEventData(txn, (ch.OWNER_UID == Parameter("UID")), {"UID":uid})
+        return self.getEventData(txn, (ch.OWNER_UID == Parameter("UID")), {"UID": uid})
 
 
+
 class EventsByOwnerCalendar(EventDetails):
 
     _name = "Get Event Data by Owner UID/Short Name and calendar name"
@@ -590,7 +622,6 @@
     @inlineCallbacks
     def doIt(self, txn):
 
-
         uid = raw_input("Owner UID/Name: ")
         uid = UIDFromInput(txn, uid)
         name = raw_input("Calendar resource name: ")
@@ -601,12 +632,14 @@
         else:
             print "Could not find icalendar data"
 
+
     def getData(self, txn, uid, name):
         ch = schema.CALENDAR_HOME
         cb = schema.CALENDAR_BIND
         return self.getEventData(txn, (ch.OWNER_UID == Parameter("UID")).And(cb.CALENDAR_RESOURCE_NAME == Parameter("NAME")), {"UID": uid, "NAME": name})
 
 
+
 class EventsByPath(EventDetails):
 
     _name = "Get Event Data by HTTP Path"
@@ -614,7 +647,6 @@
     @inlineCallbacks
     def doIt(self, txn):
 
-
         path = raw_input("Path: ")
         pathbits = path.split("/")
         if len(pathbits) != 6:
@@ -630,6 +662,7 @@
         else:
             print "Could not find icalendar data"
 
+
     def getData(self, txn, homeName, calendarName, resourceName):
         ch = schema.CALENDAR_HOME
         cb = schema.CALENDAR_BIND
@@ -645,6 +678,7 @@
         )
 
 
+
 class EventsByContent(EventDetails):
 
     _name = "Get Event Data by Searching its Text Data"
@@ -652,7 +686,6 @@
     @inlineCallbacks
     def doIt(self, txn):
 
-
         uid = raw_input("Search for: ")
         rows = yield self.getData(txn, uid)
         if rows:
@@ -661,11 +694,13 @@
         else:
             print "Could not find icalendar data"
 
+
     def getData(self, txn, text):
         co = schema.CALENDAR_OBJECT
-        return self.getEventData(txn, (co.ICALENDAR_TEXT.Contains(Parameter("TEXT"))), {"TEXT":text})
+        return self.getEventData(txn, (co.ICALENDAR_TEXT.Contains(Parameter("TEXT"))), {"TEXT": text})
 
 
+
 class EventsInTimerange(Cmd):
 
     _name = "Get Event Data within a specified time range"
@@ -673,7 +708,6 @@
     @inlineCallbacks
     def doIt(self, txn):
 
-
         uid = raw_input("Owner UID/Name: ")
         start = raw_input("Start Time (UTC YYYYMMDDTHHMMSSZ or YYYYMMDD): ")
         if len(start) == 8:
@@ -701,6 +735,7 @@
 
         yield self.eventsForEachCalendar(home, uid, timerange)
 
+
     @inlineCallbacks
     def eventsForEachCalendar(self, home, uid, timerange):
 
@@ -710,6 +745,7 @@
                 continue
             yield self.eventsInTimeRange(calendar, uid, timerange)
 
+
     @inlineCallbacks
     def eventsInTimeRange(self, calendar, uid, timerange):
 
@@ -746,6 +782,7 @@
             print ical_data.getTextWithTimezones(includeTimezones=False)
 
 
+
 class Purge(Cmd):
 
     _name = "Purge all data from tables"
@@ -753,13 +790,12 @@
     @inlineCallbacks
     def doIt(self, txn):
 
-
         if raw_input("Do you really want to remove all data [y/n]: ")[0].lower() != 'y':
             print "No data removed"
             returnValue(None)
 
         wipeout = (
-            # These are ordered in such a way as to ensure key constraints are not 
+            # These are ordered in such a way as to ensure key constraints are not
             # violated as data is removed
 
             schema.RESOURCE_PROPERTY,
@@ -780,10 +816,8 @@
             schema.ADDRESSBOOK_OBJECT_REVISIONS,
 
             schema.ADDRESSBOOK,
-            schema.ABO_MEMBERS,
-            schema.ADDRESSBOOK_OBJECT,
             #schema.ADDRESSBOOK_BIND, - cascades
-            #schema.ABO_FOREIGN_MEMBERS, - cascades
+            #schema.ADDRESSBOOK_OBJECT, - cascades
 
             schema.ADDRESSBOOK_HOME,
             #schema.ADDRESSBOOK_HOME_METADATA, - cascades
@@ -811,6 +845,7 @@
         else:
             print "No attachments path to delete."
 
+
     @inlineCallbacks
     def removeTableData(self, txn, tableschema):
         yield Delete(
@@ -819,6 +854,7 @@
         ).on(txn)
 
 
+
 class DBInspectService(Service, object):
     """
     Service which runs, exports the appropriate records, then stops the reactor.
@@ -864,6 +900,7 @@
         self.commands.append(cmd.name())
         self.commandMap[cmd.name()] = cmd
 
+
     @inlineCallbacks
     def runCommandByPosition(self, position):
         try:
@@ -872,6 +909,7 @@
             print "Position %d not available" % (position,)
             returnValue(None)
 
+
     @inlineCallbacks
     def runCommandByName(self, name):
         try:
@@ -879,6 +917,7 @@
         except IndexError:
             print "Unknown command: '%s'" % (name,)
 
+
     @inlineCallbacks
     def runCommand(self, cmd):
         txn = self.store.newTransaction()
@@ -891,6 +930,7 @@
             print "Command '%s' failed because of: %s" % (cmd.name(), e,)
             yield txn.abort()
 
+
     def printCommands(self):
 
         print "\n<---- Commands ---->"
@@ -900,6 +940,7 @@
             print "P. Purge\n"
         print "Q. Quit\n"
 
+
     @inlineCallbacks
     def doDBInspect(self):
         """
@@ -951,6 +992,7 @@
         # anyway).
 
 
+
 def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
     """
     Do the export.
@@ -961,7 +1003,7 @@
     options.parseOptions(argv[1:])
     def makeService(store):
         return DBInspectService(store, options, reactor, config)
-    utilityMain(options['config'], makeService, reactor)
+    utilityMain(options['config'], makeService, reactor, verbose=options['debug'])
 
 if __name__ == '__main__':
     main()

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dkimtool.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dkimtool.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dkimtool.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -17,14 +17,19 @@
 
 from Crypto.PublicKey import RSA
 from StringIO import StringIO
+
+from twext.python.log import setLogLevelForNamespace, StandardIOObserver
 from twext.web2.client.http import ClientRequest
 from twext.web2.http_headers import Headers
 from twext.web2.stream import MemoryStream
+
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks
 from twisted.python.usage import Options
+
 from twistedcaldav.scheduling.ischedule.dkim import RSA256, DKIMRequest, \
     PublicKeyLookup, DKIMVerifier, DKIMVerificationError
+
 import sys
 
 
@@ -60,6 +65,9 @@
 @inlineCallbacks
 def _doRequest(options):
 
+    if options["verbose"]:
+        setLogLevelForNamespace("twistedcaldav.scheduling.ischedule.dkim", "debug")
+
     # Parse the HTTP file
     request = open(options["request"]).read()
     method, uri, headers, stream = _parseRequest(request)
@@ -118,7 +126,7 @@
 
     dkim = DKIMVerifier(request, lookup)
     if options["fake-time"]:
-        dkim.time = 100
+        dkim.time = 0
 
     try:
         yield dkim.verify()
@@ -284,6 +292,15 @@
     options = DKIMToolOptions()
     options.parseOptions(argv[1:])
 
+    #
+    # Send logging output to stdout
+    #
+    observer = StandardIOObserver()
+    observer.start()
+
+    if options["verbose"]:
+        setLogLevelForNamespace("twistedcaldav.scheduling.ischedule.dkim", "debug")
+
     if options["key-gen"]:
         _doKeyGeneration(options)
     elif options["request"]:

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/export.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/export.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/export.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -31,7 +31,6 @@
 data associated with users and calendars beyond the iCalendar as visible to the
 owner of that calendar, including DAV properties, information about sharing, and
 per-user data such as alarms.
-
 """
 
 import os
@@ -85,9 +84,14 @@
 
     synopsis = description
 
-    optParameters = [['config', 'f', DEFAULT_CONFIG_FILE,
-                      "Specify caldavd.plist configuration path."]]
+    optFlags = [
+        ['debug', 'D', "Debug logging."],
+    ]
 
+    optParameters = [
+        ['config', 'f', DEFAULT_CONFIG_FILE, "Specify caldavd.plist configuration path."],
+    ]
+
     def __init__(self):
         super(ExportOptions, self).__init__()
         self.exporters = []
@@ -285,9 +289,9 @@
 
     def __init__(self, store, options, output, reactor, config):
         super(ExporterService, self).__init__()
-        self.store   = store
+        self.store = store
         self.options = options
-        self.output  = output
+        self.output = output
         self.reactor = reactor
         self.config = config
         self._directory = None
@@ -366,8 +370,9 @@
                      (e))
         sys.exit(1)
 
+
     def makeService(store):
         from twistedcaldav.config import config
         return ExporterService(store, options, output, reactor, config)
 
-    utilityMain(options["config"], makeService, reactor)
+    utilityMain(options["config"], makeService, reactor, verbose=options["debug"])

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/gateway.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/gateway.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/gateway.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -34,7 +34,6 @@
 from calendarserver.tools.purge import WorkerService, PurgeOldEventsService, DEFAULT_BATCH_SIZE, DEFAULT_RETAIN_DAYS
 from calendarserver.tools.cmdline import utilityMain
 
-from twext.python.log import StandardIOObserver
 from pycalendar.datetime import PyCalendarDateTime
 
 
@@ -95,14 +94,14 @@
     # Get configuration
     #
     configFileName = None
+    debug = False
 
     for opt, arg in optargs:
         if opt in ("-h", "--help"):
             usage()
 
         if opt in ("-e", "--error"):
-            observer = StandardIOObserver()
-            observer.start()
+            debug = True
 
         elif opt in ("-f", "--config"):
             configFileName = arg
@@ -128,7 +127,7 @@
         commands = [plist]
 
     RunnerService.commands = commands
-    utilityMain(configFileName, RunnerService)
+    utilityMain(configFileName, RunnerService, verbose=debug)
 
 
 attrMap = {

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/loadaugmentdb.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/loadaugmentdb.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/loadaugmentdb.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -15,56 +15,32 @@
 # limitations under the License.
 ##
 
-import os
-import sys
+from calendarserver.tools.managetimezones import StandardIOObserver
+from calendarserver.tools.util import loadConfig, getDirectory, \
+    autoDisableMemcached
 
 from getopt import getopt, GetoptError
 from grp import getgrnam
 from pwd import getpwnam
-from sys import stdout, stderr
 
 from twext.python.log import setLogLevelForNamespace
 
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks
-from twisted.python.log import addObserver, removeObserver
 from twisted.python.util import switchUID
+
 from twistedcaldav.config import config, ConfigurationError
 from twistedcaldav.directory import augment
 from twistedcaldav.directory.augment import AugmentXMLDB
 
-from calendarserver.tools.util import loadConfig, getDirectory,\
-    autoDisableMemcached
+import os
+import sys
 
 class UsageError (StandardError):
     pass
 
-class StandardIOObserver (object):
-    """
-    Log observer that writes to standard I/O.
-    """
-    def emit(self, eventDict):
-        text = None
 
-        if eventDict["isError"]:
-            output = stderr
-            if "failure" in eventDict:
-                text = eventDict["failure"].getTraceback()
-        else:
-            output = stdout
 
-        if not text:
-            text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
-
-        output.write(text)
-        output.flush()
-
-    def start(self):
-        addObserver(self.emit)
-
-    def stop(self):
-        removeObserver(self.emit)
-
 def usage(e=None):
     if e:
         print e
@@ -87,6 +63,8 @@
     else:
         sys.exit(0)
 
+
+
 def main():
     try:
         (optargs, args) = getopt(
@@ -156,16 +134,18 @@
     reactor.callLater(0, run, dbxml)
     reactor.run()
 
+
+
 @inlineCallbacks
 def run(dbxml):
-    
+
     try:
         uids = set((yield augment.AugmentService.getAllUIDs()))
         added = 0
         updated = 0
         removed = 0
         if dbxml:
-            yield augment.AugmentService.addAugmentRecords(dbxml.db.values(), )
+            yield augment.AugmentService.addAugmentRecords(dbxml.db.values(),)
             add_records = list()
             modify_records = list()
             for record in dbxml.db.values():
@@ -179,7 +159,7 @@
             remove_uids = uids.difference(dbxml.db.keys())
             yield augment.AugmentService.removeAugmentRecords(remove_uids)
             removed = len(remove_uids)
-            
+
         print "Changes:"
         print "  Added: %d" % (added,)
         print "  Changed: %d" % (updated,)

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/managetimezones.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/managetimezones.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/managetimezones.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -15,22 +15,26 @@
 # limitations under the License.
 ##
 
+from pycalendar.calendar import PyCalendar
+from pycalendar.datetime import PyCalendarDateTime
+
+
+from twext.python.log import StandardIOObserver
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.filepath import FilePath
+
 from twistedcaldav.timezonestdservice import PrimaryTimezoneDatabase, \
     SecondaryTimezoneDatabase
-from sys import stdout, stderr
+
+from zonal.tzconvert import tzconvert
+
 import getopt
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks
-from twisted.python.log import addObserver, removeObserver
-import sys
 import os
-import urllib
+import sys
 import tarfile
 import tempfile
-from pycalendar.calendar import PyCalendar
-from zonal.tzconvert import tzconvert
-from twisted.python.filepath import FilePath
-from pycalendar.datetime import PyCalendarDateTime
+import urllib
 
 
 def _doPrimaryActions(action, tzpath, xmlfile, changed, tzvers):
@@ -165,36 +169,6 @@
 
 
 
-class StandardIOObserver (object):
-    """
-    Log observer that writes to standard I/O.
-    """
-    def emit(self, eventDict):
-        text = None
-
-        if eventDict["isError"]:
-            output = stderr
-            if "failure" in eventDict:
-                text = eventDict["failure"].getTraceback()
-        else:
-            output = stdout
-
-        if not text:
-            text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
-
-        output.write(text)
-        output.flush()
-
-
-    def start(self):
-        addObserver(self.emit)
-
-
-    def stop(self):
-        removeObserver(self.emit)
-
-
-
 @inlineCallbacks
 def _runInReactor(tzdb):
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/migrate_verify.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/migrate_verify.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/migrate_verify.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -75,6 +75,7 @@
     synopsis = description
 
     optFlags = [
+        ['debug', 'D', "Debug logging."],
     ]
 
     optParameters = [
@@ -107,6 +108,7 @@
             return open(self.outputName, 'wb')
 
 
+
 class MigrateVerifyService(Service, object):
     """
     Service which runs, does its stuff, then stops the reactor.
@@ -129,6 +131,7 @@
         self.missingCalendars = []
         self.missingResources = []
 
+
     def startService(self):
         """
         Start the service.
@@ -157,7 +160,7 @@
 
 
     def readPaths(self):
-        
+
         self.output.write("-- Reading data file: %s\n" % (self.options["data"]))
 
         datafile = open(os.path.expanduser(self.options["data"]))
@@ -183,7 +186,7 @@
                 elif len(segments) > 6:
                     self.badPaths.append(line)
                     invalidGUIDs.add(guid)
-                else:                
+                else:
                     self.pathsByGUID.setdefault(guid, {}).setdefault(calendar, set()).add(resource)
                     self.validPaths += 1
             else:
@@ -210,27 +213,27 @@
         for invalidGUID in sorted(invalidGUIDs):
             self.output.write("Invalid GUID: %s\n" % (invalidGUID,))
 
-
         self.output.write("\n-- Bad paths\n")
         for badPath in sorted(self.badPaths):
             self.output.write("Bad path: %s\n" % (badPath,))
 
+
     @inlineCallbacks
     def doCheck(self):
         """
         Check path data against the SQL store.
         """
-        
+
         self.output.write("\n-- Scanning database for missed migrations\n")
 
         # Get list of distinct resource_property resource_ids to delete
         self.txn = self.store.newTransaction()
-        
+
         total = len(self.pathsByGUID)
         totalMissingCalendarResources = 0
         count = 0
         for guid in self.pathsByGUID:
-            
+
             if divmod(count, 10)[1] == 0:
                 self.output.write(("\r%d of %d (%d%%)" % (
                     count,
@@ -244,7 +247,7 @@
             if homeID is None:
                 self.missingGUIDs.append(guid)
                 continue
-            
+
             # Now get the list of calendar names and calendar resource IDs
             results = (yield self.calendarsForUser(homeID))
             if results is None:
@@ -261,7 +264,7 @@
                         results = []
                     results = [result[0] for result in results]
                     db_resources = set(results)
-                    
+
                     # Also check for split calendar
                     if "%s-vtodo" % (calendar,) in calendars:
                         results = (yield self.resourcesForCalendar(calendars["%s-vtodo" % (calendar,)]))
@@ -269,7 +272,7 @@
                             results = []
                         results = [result[0] for result in results]
                         db_resources.update(results)
-                    
+
                     # Also check for split calendar
                     if "%s-vevent" % (calendar,) in calendars:
                         results = (yield self.resourcesForCalendar(calendars["%s-vevent" % (calendar,)]))
@@ -277,7 +280,7 @@
                             results = []
                         results = [result[0] for result in results]
                         db_resources.update(results)
-                    
+
                     old_resources = set(self.pathsByGUID[guid][calendar])
                     self.missingResources.extend(["%s/%s/%s" % (guid, calendar, resource,) for resource in old_resources.difference(db_resources)])
 
@@ -285,7 +288,7 @@
             if divmod(count + 1, 10)[1] == 0:
                 yield self.txn.commit()
                 self.txn = self.store.newTransaction()
-            
+
             count += 1
 
         yield self.txn.commit()
@@ -302,12 +305,12 @@
         self.output.write("\nTotal missing Resources: %d\n" % (len(self.missingResources),))
         for resource in sorted(self.missingResources):
             self.output.write("%s\n" % (resource,))
-                    
 
+
     @inlineCallbacks
     def guid2ResourceID(self, guid):
         ch = schema.CALENDAR_HOME
-        kwds = { "GUID" : guid }
+        kwds = {"GUID" : guid}
         rows = (yield Select(
             [
                 ch.RESOURCE_ID,
@@ -324,7 +327,7 @@
     @inlineCallbacks
     def calendarsForUser(self, rid):
         cb = schema.CALENDAR_BIND
-        kwds = { "RID" : rid }
+        kwds = {"RID" : rid}
         rows = (yield Select(
             [
                 cb.CALENDAR_RESOURCE_NAME,
@@ -338,11 +341,11 @@
 
         returnValue(rows)
 
-    
+
     @inlineCallbacks
     def resourcesForCalendar(self, rid):
         co = schema.CALENDAR_OBJECT
-        kwds = { "RID" : rid }
+        kwds = {"RID" : rid}
         rows = (yield Select(
             [
                 co.RESOURCE_NAME,
@@ -355,7 +358,7 @@
 
         returnValue(rows)
 
-    
+
     def stopService(self):
         """
         Stop the service.  Nothing to do; everything should be finished by this
@@ -378,12 +381,13 @@
         stderr.write("Unable to open output file for writing: %s\n" % (e))
         sys.exit(1)
 
+
     def makeService(store):
         from twistedcaldav.config import config
         config.TransactionTimeoutSeconds = 0
         return MigrateVerifyService(store, options, output, reactor, config)
 
-    utilityMain(options['config'], makeService, reactor)
+    utilityMain(options['config'], makeService, reactor, verbose=options["debug"])
 
 if __name__ == '__main__':
     main()

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/obliterate.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/obliterate.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/obliterate.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -26,7 +26,7 @@
 """
 
 from calendarserver.tools.cmdline import utilityMain
-from twext.enterprise.dal.syntax import Parameter, Delete, Select, Union,\
+from twext.enterprise.dal.syntax import Parameter, Delete, Select, Union, \
     CompoundComparison, ExpressionSyntax, Count
 from twisted.application.service import Service
 from twisted.internet.defer import inlineCallbacks, returnValue
@@ -81,6 +81,7 @@
 
     optFlags = [
         ['verbose', 'v', "Verbose logging."],
+        ['debug', 'D', "Debug logging."],
         ['fix-props', 'p', "Fix orphaned resource properties only."],
         ['dry-run', 'n', "Do not make any changes."],
     ]
@@ -116,6 +117,7 @@
             return open(self.outputName, 'wb')
 
 
+
 # Need to patch this in if not present in actual server code
 def NotIn(self, subselect):
     # Can't be Select.__contains__ because __contains__ gets __nonzero__
@@ -126,6 +128,7 @@
     ExpressionSyntax.NotIn = NotIn
 
 
+
 class ObliterateService(Service, object):
     """
     Service which runs, does its stuff, then stops the reactor.
@@ -184,10 +187,10 @@
         """
         Obliterate orphaned data in RESOURCE_PROPERTIES table.
         """
-        
+
         # Get list of distinct resource_property resource_ids to delete
         self.txn = self.store.newTransaction()
-        
+
         ch = schema.CALENDAR_HOME
         ca = schema.CALENDAR
         co = schema.CALENDAR_OBJECT
@@ -313,7 +316,7 @@
 
         # Get the resource-id for the home
         ch = schema.CALENDAR_HOME
-        kwds = { "UUID" : uuid }
+        kwds = {"UUID" : uuid}
         rows = (yield Select(
             [ch.RESOURCE_ID, ],
             From=ch,
@@ -363,7 +366,7 @@
         ch = schema.CALENDAR_HOME
         cb = schema.CALENDAR_BIND
         co = schema.CALENDAR_OBJECT
-        kwds = { "UUID" : uuid }
+        kwds = {"UUID" : uuid}
         rows = (yield Select(
             [
                 Count(co.RESOURCE_ID),
@@ -385,7 +388,7 @@
 
         # Get list of binds and bind mode
         cb = schema.CALENDAR_BIND
-        kwds = { "resourceID" : homeID }
+        kwds = {"resourceID" : homeID}
         rows = (yield Select(
             [cb.CALENDAR_RESOURCE_ID, cb.BIND_MODE, ],
             From=cb,
@@ -449,7 +452,7 @@
     def removeRevisionsForHomeResourceID(self, resourceID):
         if not self.options["dry-run"]:
             rev = schema.CALENDAR_OBJECT_REVISIONS
-            kwds = { "ResourceID" : resourceID }
+            kwds = {"ResourceID" : resourceID}
             yield Delete(
                 From=rev,
                 Where=(
@@ -462,7 +465,7 @@
     def removeRevisionsForCalendarResourceID(self, resourceID):
         if not self.options["dry-run"]:
             rev = schema.CALENDAR_OBJECT_REVISIONS
-            kwds = { "ResourceID" : resourceID }
+            kwds = {"ResourceID" : resourceID}
             yield Delete(
                 From=rev,
                 Where=(
@@ -475,7 +478,7 @@
     def removePropertiesForResourceID(self, resourceID):
         if not self.options["dry-run"]:
             props = schema.RESOURCE_PROPERTY
-            kwds = { "ResourceID" : resourceID }
+            kwds = {"ResourceID" : resourceID}
             yield Delete(
                 From=props,
                 Where=(
@@ -489,7 +492,7 @@
 
         # Get NOTIFICATION_HOME.RESOURCE_ID
         nh = schema.NOTIFICATION_HOME
-        kwds = { "UUID" : uuid }
+        kwds = {"UUID" : uuid}
         rows = (yield Select(
             [nh.RESOURCE_ID, ],
             From=nh,
@@ -504,7 +507,7 @@
             # Delete NOTIFICATION rows
             if not self.options["dry-run"]:
                 no = schema.NOTIFICATION
-                kwds = { "ResourceID" : resourceID }
+                kwds = {"ResourceID" : resourceID}
                 yield Delete(
                     From=no,
                     Where=(
@@ -514,7 +517,7 @@
 
             # Delete NOTIFICATION_HOME (will cascade to NOTIFICATION_OBJECT_REVISIONS)
             if not self.options["dry-run"]:
-                kwds = { "UUID" : uuid }
+                kwds = {"UUID" : uuid}
                 yield Delete(
                     From=nh,
                     Where=(
@@ -528,7 +531,7 @@
 
         # Get ATTACHMENT paths
         at = schema.ATTACHMENT
-        kwds = { "resourceID" : resourceID }
+        kwds = {"resourceID" : resourceID}
         rows = (yield Select(
             [at.PATH, ],
             From=at,
@@ -543,14 +546,14 @@
             # Delete ATTACHMENT rows
             if not self.options["dry-run"]:
                 at = schema.ATTACHMENT
-                kwds = { "resourceID" : resourceID }
+                kwds = {"resourceID" : resourceID}
                 yield Delete(
                     From=at,
                     Where=(
                         at.CALENDAR_HOME_RESOURCE_ID == Parameter("resourceID")
                     ),
                 ).on(self.txn, **kwds)
-        
+
         returnValue(len(rows) if rows else 0)
 
 
@@ -558,7 +561,7 @@
     def removeHomeForResourceID(self, resourceID):
         if not self.options["dry-run"]:
             ch = schema.CALENDAR_HOME
-            kwds = { "ResourceID" : resourceID }
+            kwds = {"ResourceID" : resourceID}
             yield Delete(
                 From=ch,
                 Where=(
@@ -589,12 +592,13 @@
         stderr.write("Unable to open output file for writing: %s\n" % (e))
         sys.exit(1)
 
+
     def makeService(store):
         from twistedcaldav.config import config
         config.TransactionTimeoutSeconds = 0
         return ObliterateService(store, options, output, reactor, config)
 
-    utilityMain(options['config'], makeService, reactor)
+    utilityMain(options['config'], makeService, reactor, verbose=options["debug"])
 
 if __name__ == '__main__':
     main()

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/purge.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/purge.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -118,6 +118,7 @@
         #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"
         print ""
 
         if e:
@@ -132,13 +133,14 @@
 
         try:
             (optargs, args) = getopt(
-                sys.argv[1:], "d:b:f:hnv", [
+                sys.argv[1:], "Dd:b:f:hnv", [
                     "days=",
                     "batch=",
                     "dry-run",
                     "config=",
                     "help",
                     "verbose",
+                    "debug",
                 ],
             )
         except GetoptError, e:
@@ -152,6 +154,7 @@
         batchSize = DEFAULT_BATCH_SIZE
         dryrun = False
         verbose = False
+        debug = False
 
         for opt, arg in optargs:
             if opt in ("-h", "--help"):
@@ -174,6 +177,9 @@
             elif opt in ("-v", "--verbose"):
                 verbose = True
 
+            elif opt in ("-D", "--debug"):
+                debug = True
+
             elif opt in ("-n", "--dry-run"):
                 dryrun = True
 
@@ -200,6 +206,7 @@
         utilityMain(
             configFileName,
             cls,
+            verbose=debug,
         )
 
 
@@ -285,6 +292,7 @@
         print "  -d --days <number>: specify how many days in the past to retain (default=%d) zero means no removal of old attachments" % (DEFAULT_RETAIN_DAYS,)
         print "  -n --dry-run: calculate how many attachments to purge, but do not purge data"
         print "  -v --verbose: print progress information"
+        print "  -D --debug: debug logging"
         print ""
 
         if e:
@@ -299,7 +307,7 @@
 
         try:
             (optargs, args) = getopt(
-                sys.argv[1:], "d:b:f:hnu:v", [
+                sys.argv[1:], "Dd:b:f:hnu:v", [
                     "uuid=",
                     "days=",
                     "batch=",
@@ -307,6 +315,7 @@
                     "config=",
                     "help",
                     "verbose",
+                    "debug",
                 ],
             )
         except GetoptError, e:
@@ -321,6 +330,7 @@
         batchSize = DEFAULT_BATCH_SIZE
         dryrun = False
         verbose = False
+        debug = False
 
         for opt, arg in optargs:
             if opt in ("-h", "--help"):
@@ -346,6 +356,9 @@
             elif opt in ("-v", "--verbose"):
                 verbose = True
 
+            elif opt in ("-D", "--debug"):
+                debug = True
+
             elif opt in ("-n", "--dry-run"):
                 dryrun = True
 
@@ -376,6 +389,7 @@
         utilityMain(
             configFileName,
             cls,
+            verbose=debug,
         )
 
 
@@ -640,6 +654,7 @@
         print "  -f --config <path>: Specify caldavd.plist configuration path"
         print "  -n --dry-run: calculate how many events and contacts to purge, but do not purge data"
         print "  -v --verbose: print progress information"
+        print "  -D --debug: debug logging"
         print ""
 
         if e:
@@ -654,12 +669,13 @@
 
         try:
             (optargs, args) = getopt(
-                sys.argv[1:], "cf:hnv", [
+                sys.argv[1:], "cDf:hnv", [
                     "completely",
                     "dry-run",
                     "config=",
                     "help",
                     "verbose",
+                    "debug",
                     "noimplicit",
                 ],
             )
@@ -672,6 +688,7 @@
         configFileName = None
         dryrun = False
         verbose = False
+        debug = False
         completely = False
         doimplicit = True
 
@@ -685,6 +702,9 @@
             elif opt in ("-v", "--verbose"):
                 verbose = True
 
+            elif opt in ("-D", "--debug"):
+                debug = True
+
             elif opt in ("-n", "--dry-run"):
                 dryrun = True
 
@@ -706,7 +726,8 @@
 
         utilityMain(
             configFileName,
-            cls
+            cls,
+            verbose=debug,
         )
 
 
@@ -733,7 +754,7 @@
     def doWork(self):
 
         if self.root is None:
-            self.root = self.getRootResource()
+            self.root = self.rootResource()
         if self.directory is None:
             self.directory = self.root.getDirectory()
 
@@ -851,7 +872,8 @@
                         for childName in childNames:
 
                             childResource = (yield collection.getChild(childName))
-                            if self.completely:
+                            # Allways delete inbox items
+                            if self.completely or collName == "inbox":
                                 action = self.CANCELEVENT_SHOULD_DELETE
                             else:
                                 event = (yield childResource.iCalendar())

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/push.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/push.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/push.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -40,6 +40,7 @@
     print "options:"
     print "  -h --help: print this help and exit"
     print "  -f --config <path>: Specify caldavd.plist configuration path"
+    print "  -D --debug: debug logging"
     print ""
 
     if e:
@@ -49,11 +50,13 @@
         sys.exit(0)
 
 
+
 class WorkerService(Service):
 
     def __init__(self, store):
         self._store = store
 
+
     def rootResource(self):
         try:
             rootResource = getRootResource(config, self._store)
@@ -90,6 +93,7 @@
             reactor.stop()
 
 
+
 class DisplayAPNSubscriptions(WorkerService):
 
     users = []
@@ -101,13 +105,15 @@
             self.users)
 
 
+
 def main():
 
     try:
         (optargs, args) = getopt(
-            sys.argv[1:], "f:h", [
+            sys.argv[1:], "Df:h", [
                 "config=",
                 "help",
+                "debug",
             ],
         )
     except GetoptError, e:
@@ -117,6 +123,7 @@
     # Get configuration
     #
     configFileName = None
+    debug = False
 
     for opt, arg in optargs:
         if opt in ("-h", "--help"):
@@ -125,23 +132,25 @@
         elif opt in ("-f", "--config"):
             configFileName = arg
 
+        if opt in ("-d", "--debug"):
+            debug = True
+
         else:
             raise NotImplementedError(opt)
 
     if not args:
         usage("Not enough arguments")
 
-
     DisplayAPNSubscriptions.users = args
 
     utilityMain(
         configFileName,
         DisplayAPNSubscriptions,
+        verbose=debug,
     )
 
 
 
-
 @inlineCallbacks
 def displayAPNSubscriptions(store, directory, root, users):
     for user in users:
@@ -153,12 +162,12 @@
             subscriptions = (yield txn.apnSubscriptionsBySubscriber(record.uid))
             (yield txn.commit())
             if subscriptions:
-                byKey = { }
+                byKey = {}
                 for token, key, timestamp, userAgent, ipAddr in subscriptions:
                     byKey.setdefault(key, []).append((token, timestamp, userAgent, ipAddr))
                 for key, tokens in byKey.iteritems():
                     print
-                    protocol, host, path = key.strip("/").split("/", 2)
+                    protocol, _ignore_host, path = key.strip("/").split("/", 2)
                     resource = {
                         "CalDAV" : "calendar",
                         "CardDAV" : "addressbook",

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/shell/cmd.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/shell/cmd.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/shell/cmd.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -858,3 +858,11 @@
         self.protocol.reloadCommands()
 
     cmd_reload.hidden = "test tool"
+
+    def cmd_xyzzy(self, tokens):
+        """
+        """
+        self.terminal.write("Nothing happens.")
+        self.terminal.nextLine()
+        
+    cmd_sql.hidden = ""

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_changeip.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_changeip.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_changeip.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -35,14 +35,6 @@
                 "original_hostname",
             ],
             "ServerHostName" : "",
-            "Notifications" : {
-                "Services" : {
-                    "XMPPNotifier" : {
-                        "Host" : "original_hostname",
-                        "JID" : "com.apple.notificationuser at original_hostname",
-                    },
-                },
-            },
             "Scheduling" : {
                 "iMIP" : {
                     "Receiving" : {
@@ -74,14 +66,6 @@
                     "new_hostname",
                 ],
                 "ServerHostName" : "",
-                "Notifications" : {
-                    "Services" : {
-                        "XMPPNotifier" : {
-                            "Host" : "new_hostname",
-                            "JID" : "com.apple.notificationuser at new_hostname",
-                        },
-                    },
-                },
                 "Scheduling" : {
                     "iMIP" : {
                         "Receiving" : {

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_export.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_export.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_export.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -53,6 +53,8 @@
             .getContent()
     )
 
+
+
 def sample(name):
     return (
         getModule("twistedcaldav.test").filePath
@@ -195,7 +197,6 @@
         self.mainCalled = False
         self.patch(export, "utilityMain", self.fakeUtilityMain)
 
-
         self.store = yield buildStore(self, None)
         self.waitToStop = Deferred()
 
@@ -208,7 +209,7 @@
         self.waitToStop.callback(None)
 
 
-    def fakeUtilityMain(self, configFileName, serviceClass, reactor=None):
+    def fakeUtilityMain(self, configFileName, serviceClass, reactor=None, verbose=False):
         """
         Verify a few basic things.
         """
@@ -484,5 +485,3 @@
             Component.fromString(resultForUser2),
             Component.fromString(output.getContent())
         )
-
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -828,6 +828,7 @@
         txn = self._sqlCalendarStore.newTransaction()
 
         # Add attachment to attachment.ics
+        self._sqlCalendarStore._dropbox_ok = True
         home = (yield txn.calendarHomeWithUID(self.uid))
         calendar = (yield home.calendarWithName("calendar1"))
         event = (yield calendar.calendarObjectWithName("attachment.ics"))
@@ -836,12 +837,13 @@
         t.write("attachment")
         t.write(" text")
         (yield t.loseConnection())
+        self._sqlCalendarStore._dropbox_ok = False
 
         # Share calendars each way
         home2 = (yield txn.calendarHomeWithUID(self.uid2))
         calendar2 = (yield home2.calendarWithName("calendar2"))
-        self.sharedName = (yield calendar2.shareWith(home, _BIND_MODE_WRITE)).name()
-        self.sharedName2 = (yield calendar.shareWith(home2, _BIND_MODE_WRITE)).name()
+        self.sharedName = yield calendar2.shareWith(home, _BIND_MODE_WRITE)
+        self.sharedName2 = yield calendar.shareWith(home2, _BIND_MODE_WRITE)
 
         (yield txn.commit())
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge_old_events.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge_old_events.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge_old_events.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -512,6 +512,7 @@
     @inlineCallbacks
     def _addAttachment(self, home, calendar, event, name):
 
+        self._sqlCalendarStore._dropbox_ok = True
         txn = self._sqlCalendarStore.newTransaction()
 
         # Create an event with an attachment
@@ -525,6 +526,7 @@
         (yield t.loseConnection())
 
         (yield txn.commit())
+        self._sqlCalendarStore._dropbox_ok = False
 
         returnValue(attachment)
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/upgrade.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/upgrade.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/upgrade.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -70,10 +70,14 @@
 
     synopsis = description
 
-    optParameters = [['config', 'f', DEFAULT_CONFIG_FILE,
-                      "Specify caldavd.plist configuration path."]]
+    optFlags = [
+        ['postprocess', 'p', "Perform post-database-import processing."],
+        ['debug', 'D', "Debug logging."],
+    ]
 
-    optFlags = [['postprocess', 'p', "Perform post-database-import processing."]]
+    optParameters = [
+        ['config', 'f', DEFAULT_CONFIG_FILE, "Specify caldavd.plist configuration path."],
+    ]
 
     def __init__(self):
         super(UpgradeOptions, self).__init__()
@@ -119,9 +123,9 @@
 
     def __init__(self, store, options, output, reactor, config):
         super(UpgraderService, self).__init__()
-        self.store   = store
+        self.store = store
         self.options = options
-        self.output  = output
+        self.output = output
         self.reactor = reactor
         self.config = config
         self._directory = None
@@ -148,6 +152,7 @@
         """
 
 
+
 def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
     """
     Do the export.
@@ -174,11 +179,13 @@
             data.MergeUpgrades = True
         config.addPostUpdateHooks([setMerge])
 
+
     def makeService(store):
         return UpgraderService(store, options, output, reactor, config)
 
+
     def onlyUpgradeEvents(event):
-        output.write(logDateString()+' '+log.textFromEventDict(event)+"\n")
+        output.write(logDateString() + ' ' + log.textFromEventDict(event) + "\n")
         output.flush()
 
     setLogLevelForNamespace(None, "debug")
@@ -187,8 +194,10 @@
         customService = CalDAVServiceMaker()
         customService.doPostImport = options["postprocess"]
         return customService
-    utilityMain(options["config"], makeService, reactor, customServiceMaker)
+    utilityMain(options["config"], makeService, reactor, customServiceMaker, verbose=options["debug"])
 
+
+
 def logDateString():
     logtime = time.localtime()
     Y, M, D, h, m, s = logtime[:6]
@@ -196,6 +205,8 @@
 
     return '%02d-%02d-%02d %02d:%02d:%02d%s' % (Y, M, D, h, m, s, tz)
 
+
+
 def computeTimezoneForLog(tz):
     if tz > 0:
         neg = 1

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/util.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/util.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/util.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -44,7 +44,7 @@
 from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.notify import NotifierFactory
+from calendarserver.push.notifier import NotifierFactory
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 
 from txdav.common.datastore.file import CommonDataStore
@@ -77,9 +77,10 @@
             if not hasattr(self, "_principalCollection"):
 
                 if config.Notifications.Enabled:
+                    # FIXME: NotifierFactory needs reference to the store in order
+                    # to get a txn in order to create a Work item
                     notifierFactory = NotifierFactory(
-                        config.Notifications.InternalNotificationHost,
-                        config.Notifications.InternalNotificationPort,
+                        None, config.ServerHostName,
                     )
                 else:
                     notifierFactory = None
@@ -87,6 +88,8 @@
                 # Need a data store
                 _newStore = CommonDataStore(FilePath(config.DocumentRoot), 
                     notifierFactory, True, False)
+                if notifierFactory is not None:
+                    notifierFactory.store = _newStore
 
                 #
                 # Instantiating a DirectoryCalendarHomeProvisioningResource with a directory

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/validcalendardata.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/validcalendardata.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/validcalendardata.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -62,6 +62,7 @@
 
     optFlags = [
         ['verbose', 'v', "Verbose logging."],
+        ['debug', 'D', "Debug logging."],
         ['parse-only', 'p', "Only validate parsing of the data."],
     ]
 
@@ -227,7 +228,7 @@
     def makeService(store):
         return ValidService(store, options, output, input, reactor, config)
 
-    utilityMain(options["config"], makeService, reactor)
+    utilityMain(options["config"], makeService, reactor, verbose=options["debug"])
 
 
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/auth/augments-test.xml
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/auth/augments-test.xml	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/auth/augments-test.xml	2013-02-19 04:08:00 UTC (rev 10757)
@@ -87,6 +87,15 @@
     <auto-schedule>true</auto-schedule>
     <auto-schedule-mode>automatic</auto-schedule-mode>
   </record>
+  <record>
+    <uid>resource11</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>decline-always</auto-schedule-mode>
+    <auto-accept-group>group01</auto-accept-group>
+  </record>
   <record repeat="10">
     <uid>group%02d</uid>
     <enable>true</enable>

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/auth/augments.dtd
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/auth/augments.dtd	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/auth/augments.dtd	2013-02-19 04:08:00 UTC (rev 10757)
@@ -16,7 +16,7 @@
 
 <!ELEMENT augments (record*) >
 
-  <!ELEMENT record (uid, enable, (server-id, partition-id?)?, enable-calendar?, enable-addressbook?, auto-schedule?, auto-schedule-mode?)>
+  <!ELEMENT record (uid, enable, (server-id, partition-id?)?, enable-calendar?, enable-addressbook?, auto-schedule?, auto-schedule-mode?, auto-accept-group?)>
     <!ATTLIST record repeat CDATA "1">
 
   <!ELEMENT uid                (#PCDATA)>
@@ -27,4 +27,5 @@
   <!ELEMENT enable-addressbook (#PCDATA)>
   <!ELEMENT auto-schedule      (#PCDATA)>
   <!ELEMENT auto-schedule-mode (#PCDATA)>
+  <!ELEMENT auto-accept-group  (#PCDATA)>
 >

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/auth/resources-test.xml
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/auth/resources-test.xml	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/auth/resources-test.xml	2013-02-19 04:08:00 UTC (rev 10757)
@@ -25,7 +25,7 @@
     <password>location%02d</password>
     <name>Room %02d</name>
   </location>
-  <resource repeat="10">
+  <resource repeat="20">
     <uid>resource%02d</uid>
     <guid>resource%02d</guid>
     <password>resource%02d</password>

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist	2013-02-19 04:08:00 UTC (rev 10757)
@@ -905,10 +905,6 @@
     <key>EnableManagedAttachments</key>
     <true/>
 
-    <!-- Calendar Managed Attachments -->
-    <key>EnableManagedAttachments</key>
-    <true/>
-
     <!-- Private Events -->
     <key>EnablePrivateEvents</key>
     <true/>

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/localservers-test.xml
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/localservers-test.xml	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/localservers-test.xml	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2011 Apple Inc. All rights reserved.
+Copyright (c) 2011-2013 Apple Inc. All rights reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/localservers.xml
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/localservers.xml	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/localservers.xml	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2011 Apple Inc. All rights reserved.
+Copyright (c) 2011-2013 Apple Inc. All rights reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/remoteservers-test.xml
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/remoteservers-test.xml	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/remoteservers-test.xml	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+Copyright (c) 2006-2013 Apple Inc. All rights reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/remoteservers.xml
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/remoteservers.xml	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/remoteservers.xml	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+Copyright (c) 2006-2013 Apple Inc. All rights reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

Deleted: CalendarServer/branches/users/gaya/sharedgroups/conf/test/accounts.xml
===================================================================
--- CalendarServer/trunk/conf/test/accounts.xml	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/test/accounts.xml	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,153 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
- -->
-
-<!-- <!DOCTYPE accounts SYSTEM "accounts.dtd"> -->
-
-<directory realm="Test Realm">
-  <record type="user">
-    <uid>admin</uid>
-    <short-name>admin</short-name>
-    <password>admin</password>
-    <full-name>Super User</full-name>
-  </record>
-  <record type="user">
-    <uid>apprentice</uid>
-    <short-name>apprentice</short-name>
-    <full-name>Apprentice Super User</full-name>
-    <password>apprentice</password>
-  </record>
-  <record type="user">
-    <uid>wsanchez</uid>
-    <short-name>wsanchez</short-name>
-    <email-address>wsanchez at example.com</email-address>
-    <full-name>Wilfredo Sanchez Vega</full-name>
-    <password>test</password>
-  </record>
-  <record type="user">
-    <uid>cdaboo</uid>
-    <short-name>cdaboo</short-name>
-    <email-address>cdaboo at example.com</email-address>
-    <full-name>Cyrus Daboo</full-name>
-    <password>test</password>
-  </record>
-  <record type="user">
-    <uid>sagen</uid>
-    <short-name>sagen</short-name>
-    <email-address>sagen at example.com</email-address>
-    <full-name>Morgen Sagen</full-name>
-    <password>test</password>
-  </record>
-  <record type="user">
-    <uid>andre</uid>
-    <short-name>dre</short-name>
-    <email-address>dre at example.com</email-address>
-    <full-name>Andre LaBranche</full-name>
-    <password>test</password>
-  </record>
-  <record type="user">
-    <uid>glyph</uid>
-    <short-name>glyph</short-name>
-    <email-address>glyph at example.com</email-address>
-    <full-name>Glyph Lefkowitz</full-name>
-    <password>test</password>
-  </record>
-  <record type="user">
-    <uid>i18nuser</uid>
-    <short-name>i18nuser</short-name>
-    <email-address>i18nuser at example.com</email-address>
-    <full-name>まだ</full-name>
-    <password>i18nuser</password>
-  </record>
-  <record type="user" repeat="99">
-    <uid>user%02d</uid>
-    <short-name>user%02d</short-name>
-    <short-name>User %02d</short-name>
-    <full-name>User %02d</full-name>
-    <email-address>user%02d at example.com</email-address>
-    <password>user%02d</password>
-  </record>
-  <record type="user" repeat="10">
-    <uid>public%02d</uid>
-    <short-name>public%02d</short-name>
-    <full-name>Public %02d</full-name>
-    <password>public%02d</password>
-  </record>
-  <record type="group">
-    <uid>group01</uid>
-    <short-name>group01</short-name>
-    <full-name>Group 01</full-name>
-    <password>group01</password>
-    <member-uid>user01</member>
-  </record>
-  <record type="group">
-    <uid>group02</uid>
-    <short-name>group02</short-name>
-    <full-name>Group 02</full-name>
-    <password>group02</password>
-    <member-uid>user06</member>
-    <member-uid>user07</member>
-  </record>
-  <record type="group">
-    <uid>group03</uid>
-    <short-name>group03</short-name>
-    <full-name>Group 03</full-name>
-    <password>group03</password>
-    <member-uid>user08</member>
-    <member-uid>user09</member>
-  </record>
-  <record type="group">
-    <uid>group04</uid>
-    <short-name>group04</short-name>
-    <full-name>Group 04</full-name>
-    <password>group04</password>
-    <member-uid>group02</member>
-    <member-uid>group03</member>
-    <member-uid>user10</member>
-  </record>
-  <record type="group"> <!-- delegategroup -->
-    <uid>group05</uid>
-    <short-name>group05</short-name>
-    <full-name>Group 05</full-name>
-    <password>group05</password>
-    <member-uid>group06</member>
-    <member-uid>user20</member>
-  </record>
-  <record type="group"> <!-- delegatesubgroup -->
-    <uid>group06</uid>
-    <short-name>group06</short-name>
-    <full-name>Group 06</full-name>
-    <password>group06</password>
-    <member-uid>user21</member>
-  </record>
-  <record type="group"> <!-- readonlydelegategroup -->
-    <uid>group07</uid>
-    <short-name>group07</short-name>
-    <full-name>Group 07</full-name>
-    <password>group07</password>
-    <member-uid>user22</member>
-    <member-uid>user23</member>
-    <member-uid>user24</member>
-  </record>
-  <record type="group">
-    <uid>disabledgroup</uid>
-    <short-name>disabledgroup</short-name>
-    <full-name>Disabled Group</full-name>
-    <password>disabledgroup</password>
-    <member-uid>user01</member>
-  </record>
-</directory>

Copied: CalendarServer/branches/users/gaya/sharedgroups/conf/test/accounts.xml (from rev 10753, CalendarServer/trunk/conf/test/accounts.xml)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/test/accounts.xml	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/test/accounts.xml	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!-- <!DOCTYPE accounts SYSTEM "accounts.dtd"> -->
+
+<directory realm="Test Realm">
+  <record type="user">
+    <uid>admin</uid>
+    <short-name>admin</short-name>
+    <password>admin</password>
+    <full-name>Super User</full-name>
+  </record>
+  <record type="user">
+    <uid>apprentice</uid>
+    <short-name>apprentice</short-name>
+    <full-name>Apprentice Super User</full-name>
+    <password>apprentice</password>
+  </record>
+  <record type="user">
+    <uid>wsanchez</uid>
+    <short-name>wsanchez</short-name>
+    <email-address>wsanchez at example.com</email-address>
+    <full-name>Wilfredo Sanchez Vega</full-name>
+    <password>test</password>
+  </record>
+  <record type="user">
+    <uid>cdaboo</uid>
+    <short-name>cdaboo</short-name>
+    <email-address>cdaboo at example.com</email-address>
+    <full-name>Cyrus Daboo</full-name>
+    <password>test</password>
+  </record>
+  <record type="user">
+    <uid>sagen</uid>
+    <short-name>sagen</short-name>
+    <email-address>sagen at example.com</email-address>
+    <full-name>Morgen Sagen</full-name>
+    <password>test</password>
+  </record>
+  <record type="user">
+    <uid>andre</uid>
+    <short-name>dre</short-name>
+    <email-address>dre at example.com</email-address>
+    <full-name>Andre LaBranche</full-name>
+    <password>test</password>
+  </record>
+  <record type="user">
+    <uid>glyph</uid>
+    <short-name>glyph</short-name>
+    <email-address>glyph at example.com</email-address>
+    <full-name>Glyph Lefkowitz</full-name>
+    <password>test</password>
+  </record>
+  <record type="user">
+    <uid>i18nuser</uid>
+    <short-name>i18nuser</short-name>
+    <email-address>i18nuser at example.com</email-address>
+    <full-name>まだ</full-name>
+    <password>i18nuser</password>
+  </record>
+  <record type="user" repeat="99">
+    <uid>user%02d</uid>
+    <short-name>user%02d</short-name>
+    <short-name>User %02d</short-name>
+    <full-name>User %02d</full-name>
+    <email-address>user%02d at example.com</email-address>
+    <password>user%02d</password>
+  </record>
+  <record type="user" repeat="10">
+    <uid>public%02d</uid>
+    <short-name>public%02d</short-name>
+    <full-name>Public %02d</full-name>
+    <password>public%02d</password>
+  </record>
+  <record type="group">
+    <uid>group01</uid>
+    <short-name>group01</short-name>
+    <full-name>Group 01</full-name>
+    <password>group01</password>
+    <member-uid>user01</member>
+  </record>
+  <record type="group">
+    <uid>group02</uid>
+    <short-name>group02</short-name>
+    <full-name>Group 02</full-name>
+    <password>group02</password>
+    <member-uid>user06</member>
+    <member-uid>user07</member>
+  </record>
+  <record type="group">
+    <uid>group03</uid>
+    <short-name>group03</short-name>
+    <full-name>Group 03</full-name>
+    <password>group03</password>
+    <member-uid>user08</member>
+    <member-uid>user09</member>
+  </record>
+  <record type="group">
+    <uid>group04</uid>
+    <short-name>group04</short-name>
+    <full-name>Group 04</full-name>
+    <password>group04</password>
+    <member-uid>group02</member>
+    <member-uid>group03</member>
+    <member-uid>user10</member>
+  </record>
+  <record type="group"> <!-- delegategroup -->
+    <uid>group05</uid>
+    <short-name>group05</short-name>
+    <full-name>Group 05</full-name>
+    <password>group05</password>
+    <member-uid>group06</member>
+    <member-uid>user20</member>
+  </record>
+  <record type="group"> <!-- delegatesubgroup -->
+    <uid>group06</uid>
+    <short-name>group06</short-name>
+    <full-name>Group 06</full-name>
+    <password>group06</password>
+    <member-uid>user21</member>
+  </record>
+  <record type="group"> <!-- readonlydelegategroup -->
+    <uid>group07</uid>
+    <short-name>group07</short-name>
+    <full-name>Group 07</full-name>
+    <password>group07</password>
+    <member-uid>user22</member>
+    <member-uid>user23</member>
+    <member-uid>user24</member>
+  </record>
+  <record type="group">
+    <uid>disabledgroup</uid>
+    <short-name>disabledgroup</short-name>
+    <full-name>Disabled Group</full-name>
+    <password>disabledgroup</password>
+    <member-uid>user01</member>
+  </record>
+</directory>

Modified: CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarcommonextra.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarcommonextra.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarcommonextra.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -2,7 +2,7 @@
 #
 # CommonExtra script for calendar server.
 #
-# Copyright (c) 2012 Apple Inc.  All Rights Reserved.
+# Copyright (c) 2012-2013 Apple Inc.  All Rights Reserved.
 #
 # IMPORTANT NOTE:  This file is licensed only for use on Apple-labeled
 # computers and is subject to the terms and conditions of the Apple

Modified: CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarpromotion.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarpromotion.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarpromotion.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -19,10 +19,12 @@
 SRC_CONFIG_DIR = "/Applications/Server.app/Contents/ServerRoot/private/etc/caldavd"
 CALENDAR_SERVER_ROOT = "/Library/Server/Calendar and Contacts"
 DEST_CONFIG_DIR = "%s/Config" % (CALENDAR_SERVER_ROOT,)
+DEST_DATA_DIR = "%s/Data" % (CALENDAR_SERVER_ROOT,)
 CALDAVD_PLIST = "caldavd.plist"
 USER_NAME = "calendar"
 GROUP_NAME = "calendar"
 LOG_DIR = "/var/log/caldavd"
+RUN_DIR = "/var/run/caldavd"
 
 
 def updatePlist(plistData):
@@ -57,20 +59,27 @@
 
 def main():
 
-    try:
-        # Create calendar ServerRoot
-        os.mkdir(CALENDAR_SERVER_ROOT)
-    except OSError:
-        # Already exists
-        pass
+    for dirName in (
+        CALENDAR_SERVER_ROOT,
+        DEST_CONFIG_DIR,
+        DEST_DATA_DIR,
+        LOG_DIR,
+        RUN_DIR
+    ):
+        try:
+            os.mkdir(dirName)
+        except OSError:
+            # Already exists
+            pass
 
-    try:
-        # Create calendar ConfigRoot
-        os.mkdir(DEST_CONFIG_DIR)
-    except OSError:
-        # Already exists
-        pass
+        try:
+            uid = getpwnam(USER_NAME).pw_uid
+            gid = getgrnam(GROUP_NAME).gr_gid
+            os.chown(dirName, uid, gid)
+        except Exception, e:
+            print "Unable to chown %s: %s" % (dirName, e)
 
+
     plistPath = os.path.join(DEST_CONFIG_DIR, CALDAVD_PLIST)
 
     if os.path.exists(plistPath):
@@ -87,20 +96,6 @@
         srcPlistPath = os.path.join(SRC_CONFIG_DIR, CALDAVD_PLIST)
         shutil.copy(srcPlistPath, DEST_CONFIG_DIR)
 
-    # Create log directory
-    try:
-        os.mkdir(LOG_DIR, 0755)
-    except OSError:
-        # Already exists
-        pass
 
-    # Set ownership on log directory
-    try:
-        uid = getpwnam(USER_NAME).pw_uid
-        gid = getgrnam(GROUP_NAME).gr_gid
-        os.chown(LOG_DIR, uid, gid)
-    except Exception, e:
-        print "Unable to chown %s: %s" % (LOG_DIR, e)
-
 if __name__ == '__main__':
     main()

Modified: CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_commonextra.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_commonextra.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_commonextra.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_promotion.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_promotion.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_promotion.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/benchmarks/find_calendars.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/benchmarks/find_calendars.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/benchmarks/find_calendars.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -32,8 +32,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <x0:propfind xmlns:x0="DAV:" xmlns:x3="http://apple.com/ns/ical/" xmlns:x1="http://calendarserver.org/ns/" xmlns:x2="urn:ietf:params:xml:ns:caldav">
  <x0:prop>
-  <x1:xmpp-server/>
-  <x1:xmpp-uri/>
   <x1:getctag/>
   <x0:displayname/>
   <x2:calendar-description/>

Copied: CalendarServer/branches/users/gaya/sharedgroups/doc/Client-Server/calendar-client.rst (from rev 10753, CalendarServer/trunk/doc/Client-Server/calendar-client.rst)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/doc/Client-Server/calendar-client.rst	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/doc/Client-Server/calendar-client.rst	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,78 @@
+==========================================
+Using Calendar client with Calendar Server
+==========================================
+
+OS X includes a CalDAV client called "Calendar", formerly known as iCal. As of this writing, the current OS X version is 10.8.2 Mountain Lion, and modern Calendar Server versions support old Calendar or iCal versions going back to Mac OS X 10.6.x Snow Leopard. iCal in 10.5.x Leopard and prior does not support implicit scheduling, and will not function correctly with Calendar Server 3 or later.
+
+---------------------
+Account Setup
+---------------------
+
+New CalDAV accounts for Calendar can be created either in the Calendar preferences, or in the "Mail, Contacts & Calendars" prefpane. The latter method is preferred, as it consolidates various account types into a single interface.
+
+To add a CalDAV account using the Mail, Contacts & Calendars prefpane, follow these steps:
+
+#. Open System Preferences and select the "Mail, Contacts & Calendars" prefpane.
+#. Scroll to the bottom of the list of account types and click "Add Other Account".
+#. Select "Add a CalDAV account", then click "Create...".
+#. Supply a valid username, password, and server address, then click "Create".
+
+To add a CalDAV account using Calendar preferences, follow these steps:
+
+#. In Calendar, choose "Preferences..." from the Calendar menu.
+#. Click the plus button at the bottom of the accounts list to add a new account.
+#. Set the Account Type to CalDAV. Automatic might work depending on your configuration (see Discovery section), but CalDAV should always work.
+#. Supply a valid user name, password, and server address.
+
+Alternatively, CalDAV accounts for Calendar may be provisioned using the Profile Manager service in `OS X Server <http://www.apple.com/osx/server/>`_.
+
+------------------------------------
+Account Discovery Details
+------------------------------------
+
+When adding a CalDAV account in "Automatic" mode using Calendar preferences, the client looks for a `DNS SRV record for the _caldavs_tcp or _caldav_tcp services <http://tools.ietf.org/html/draft-daboo-srv-caldav-10>`_, to discover the CalDAV server for the provided domain name. If such a record is not available, Automatic setup can still succeed if the CalDAV server name is the same as the provided domain name.
+
+Regardless of which setup mode is used, Calendar will attempt an https connection first on port 8443, and if that fails, will then attempt to connect using http on port 8008. To see a detailed log of exactly what Calendar does when discovering a new account, enable all debug logging (see below) and filter the results for "discovery".
+
+----------------------
+Push Notifications
+----------------------
+
+Calendar supports two options for push notifications: `Apple Push Notification Service <http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html>`_ (APNS), or `XMPP+pubsub <http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-pubsubdiscovery.txt>`_. Calendar will look for both of these at setup time, and will prefer APNS. Calendar Server officially supports only the XMPP+pubsub method, hosted by an external service. This configuration was commonly used in older versions of OS X Server, which includes an XMPP service.
+
+-----------------
+Troubleshooting
+-----------------
+
+Additional debug logging is available by setting some preferenecs keys in the com.apple.CalendarAgent domain. The logs are sent to the standard system logging facility, `ASL <https://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man3/asl.3.html>`_, and may be viewed with the Console utility, or the "syslog" command line tool. The "Sender" for these log messages is either CalendarAgent or Calendar.
+
+To enable complete protocol logging, open Terminal and run the following two commands:
+
+::
+
+  defaults write -g CalLogSimpleConfiguration -array com.apple.calendar.store.log.caldav.http
+  notifyutil -p com.apple.calendar.foundation.notification.logConfigUpdated
+
+The second command (notifyutil) makes CalendarAgent re-read the preferences, because normally they are only read at startup (and CalendarAgenet is a persistent process that does not exit often).
+
+The debug logging domains are specified using a reverse-dns style hierarchy, so to enable all Calendar logging (includes logging of account discovery), use the commands:
+
+::
+
+  defaults write -g CalLogSimpleConfiguration -array com.apple.calendar
+  notifyutil -p com.apple.calendar.foundation.notification.logConfigUpdated
+
+To disable Calendar debug logging, run the commands:
+
+::
+
+  defaults delete -g CalLogSimpleConfiguration
+  notifyutil -p com.apple.calendar.foundation.notification.logConfigUpdated
+
+
+To select all Calendar and CalendarAgent logs from ASL, use Console to select these two Senders from the utility box in the left of the Console window, or use the following syslog command:
+
+::
+
+  syslog -k Sender CalendarAgent -o -k Sender Calendar
+

Deleted: CalendarServer/branches/users/gaya/sharedgroups/lib-patches/setproctitle/HAVE_DECL_STRLCPY.patch
===================================================================
--- CalendarServer/trunk/lib-patches/setproctitle/HAVE_DECL_STRLCPY.patch	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/lib-patches/setproctitle/HAVE_DECL_STRLCPY.patch	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,12 +0,0 @@
-diff -ur ./src/spt_config.h ../setproctitle-1.1.6.patched/src/spt_config.h
---- ./src/spt_config.h	2012-04-27 03:00:09.000000000 -0700
-+++ ../setproctitle-1.1.6.patched/src/spt_config.h	2013-02-13 14:53:20.000000000 -0800
-@@ -15,7 +15,7 @@
- 
- /* Define to 1 if you have the declaration of `strlcpy', and to 0 if you
-    don't. */
--#define HAVE_DECL_STRLCPY 0
-+#define HAVE_DECL_STRLCPY 1
- 
- /* GCC 4.0 and later have support for specifying symbol visibility */
- #if __GNUC__ >= 4 && !defined(__MINGW32__)

Copied: CalendarServer/branches/users/gaya/sharedgroups/lib-patches/setproctitle/HAVE_DECL_STRLCPY.patch (from rev 10753, CalendarServer/trunk/lib-patches/setproctitle/HAVE_DECL_STRLCPY.patch)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/lib-patches/setproctitle/HAVE_DECL_STRLCPY.patch	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/lib-patches/setproctitle/HAVE_DECL_STRLCPY.patch	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,12 @@
+diff -ur ./src/spt_config.h ../setproctitle-1.1.6.patched/src/spt_config.h
+--- ./src/spt_config.h	2012-04-27 03:00:09.000000000 -0700
++++ ../setproctitle-1.1.6.patched/src/spt_config.h	2013-02-13 14:53:20.000000000 -0800
+@@ -15,7 +15,7 @@
+ 
+ /* Define to 1 if you have the declaration of `strlcpy', and to 0 if you
+    don't. */
+-#define HAVE_DECL_STRLCPY 0
++#define HAVE_DECL_STRLCPY 1
+ 
+ /* GCC 4.0 and later have support for specifying symbol visibility */
+ #if __GNUC__ >= 4 && !defined(__MINGW32__)

Modified: CalendarServer/branches/users/gaya/sharedgroups/pyflakes
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/pyflakes	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/pyflakes	2013-02-19 04:08:00 UTC (rev 10757)
@@ -4,7 +4,7 @@
 set -u
 
 wd="$(cd "$(dirname "$0")" && pwd)";
-flakes="$(cd "${wd}/../pyflakes-0.5.0" && pwd)";
+flakes="$(cd "${wd}/../pyflakes-0.6.1" && pwd)";
 
 export PYTHONPATH="${flakes}:${PYTHONPATH:-}";
 
@@ -14,12 +14,11 @@
 
 tmp="$(mktemp "/tmp/pyflakes.XXXXX")";
 
-cd "${wd}" && "${flakes}/bin/pyflakes" "$@" | sed                     \
-  -e "/redefinition of unused/d"                                      \
-  -e "/'from .* import \\*' used; unable to detect undefined names/d" \
-  -e "/redefinition of function/d"                                    \
-  -e "/xmlext.py:[0-9][0-9]*: /d"                                     \
-  -e "/^twext\\/backport/d"                                           \
+cd "${wd}" && "${flakes}/bin/pyflakes" "$@" | sed                         \
+  -e "/'from ctypes import \\*' used; unable to detect undefined names/d" \
+  -e "/redefinition of unused/d"                                          \
+  -e "/^twext\\/backport/d"                                               \
+  -e "/xmlext.py:[0-9][0-9]*: /d"                                         \
   | tee "${tmp}";
 
 if [ -s "${tmp}" ]; then error="true"; else error="false"; fi;

Modified: CalendarServer/branches/users/gaya/sharedgroups/run
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/run	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/run	2013-02-19 04:08:00 UTC (rev 10757)
@@ -252,3 +252,4 @@
 
 
 run_main "$@";
+

Modified: CalendarServer/branches/users/gaya/sharedgroups/setup.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/setup.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/setup.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -131,9 +131,11 @@
                              "bin/calendarserver_backup",
                              "bin/calendarserver_bootstrap_database",
                              "bin/calendarserver_command_gateway",
-                            #"bin/calendarserver_config",
+                            #"bin/calendarserver_config", # Used by run script.
                             #"bin/calendarserver_dbinspect",
+                            #"bin/calendarserver_dkimtool",
                              "bin/calendarserver_export",
+                            #"bin/calendarserver_icalendar_validate",
                             #"bin/calendarserver_load_augmentdb",
                             #"bin/calendarserver_make_partition",
                             #"bin/calendarserver_manage_postgres",
@@ -141,13 +143,14 @@
                              "bin/calendarserver_manage_push",
                             #"bin/calendarserver_manage_timezones",
                              "bin/calendarserver_migrate_resources",
+                            #"bin/calendarserver_monitor_amp_notifications",
+                            #"bin/calendarserver_monitor_notifications",
                              "bin/calendarserver_purge_attachments",
                              "bin/calendarserver_purge_events",
                              "bin/calendarserver_purge_principals",
                              "bin/calendarserver_shell",
                             #"bin/calendarserver_upgrade",
                             #"bin/calendarserver_verify_data",
-                            #"bin/calendarserver_warmup",
                            ],
         data_files       = [ ("caldavd", ["conf/caldavd.plist"]), ],
         ext_modules      = extensions,

Modified: CalendarServer/branches/users/gaya/sharedgroups/support/build.sh
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/support/build.sh	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/support/build.sh	2013-02-19 04:08:00 UTC (rev 10757)
@@ -264,7 +264,7 @@
         #
         # Try getting a copy from calendarserver.org.
         #
-        local tmp="$(mktemp "/tmp/${cache_basename}.XXXXX")";
+        local tmp="$(mktemp "/tmp/${cache_basename}.XXXXXX")";
         curl -L "http://${pkg_host}${pkg_path}/${cache_basename}" -o "${tmp}" || true;
         echo "";
         if [ ! -s "${tmp}" ] || grep '<title>404 Not Found</title>' "${tmp}" > /dev/null; then
@@ -386,8 +386,10 @@
       svn checkout -r "${revision}" "${uri}@${revision}" "${path}";
     }
 
-    if [ "${revision}" != "HEAD" ] && [ -n "${cache_deps}" ] \
-        && [ -n "${hash}" ]; then
+    if [ "${revision}" != "HEAD" ] && \
+       [ -n "${cache_deps}" ] && \
+       [ -n "${hash}" ] \
+    ; then
       local cacheid="${name}-$(echo "${uri}" | hash)";
       local cache_file="${cache_deps}/${cacheid}@r${revision}.tgz";
 
@@ -718,11 +720,10 @@
     "setuptools" "setuptools" "${st}" \
     "$pypi/s/setuptools/${st}.tar.gz";
 
-  local zv="3.3.0";
-  local zi="zope.interface-${zv}";
-  py_dependency -m "93668855e37b4691c5c956665c33392c" \
+  local zi="zope.interface-4.0.3";
+  py_dependency -v "3.6.0" -m "1ddd308f2c83703accd1696158c300eb" \
     "Zope Interface" "zope.interface" "${zi}" \
-    "http://www.zope.org/Products/ZopeInterface/${zv}/${zi}.tar.gz";
+    "http://pypi.python.org/packages/source/z/zope.interface/${zi}.tar.gz";
 
   local po="pyOpenSSL-0.10";
   py_dependency -v 0.9 -m "34db8056ec53ce80c7f5fc58bee9f093" \
@@ -753,9 +754,9 @@
 
   # Maintenance note: next time the Twisted dependency gets updated, check out
   # twext/patches.py.
-  py_dependency -v 12 -m "cf49a8676c21c50faf1b42b528049471" \
-    "Twisted" "twisted" "Twisted-12.0.0" \
-    "${pypi}/T/Twisted/Twisted-12.0.0.tar.bz2";
+  py_dependency -v 12.2 -m "6e289825f3bf5591cfd670874cc0862d" \
+    "Twisted" "twisted" "Twisted-12.3.0" \
+    "${pypi}/T/Twisted/Twisted-12.3.0.tar.bz2";
 
   local du="python-dateutil-1.5";
   py_dependency -m "35f3732db3f2cc4afdc68a8533b60a52" \
@@ -774,9 +775,9 @@
     "${pypi}/p/python-ldap/${ld}.tar.gz";
 
   # XXX actually PyCalendar should be imported in-place.
-  py_dependency -fe -i "src" -r 214 \
+  py_dependency -fe -i "src" -r 10554 \
     "pycalendar" "pycalendar" "pycalendar" \
-    "http://svn.mulberrymail.com/repos/PyCalendar/branches/server";
+    "${svn_uri_base}/PyCalendar/trunk";
 
   #
   # Tool dependencies.  The code itself doesn't depend on these, but
@@ -789,10 +790,10 @@
     "SQLParse" "sqlparse" "${sq}" \
     "http://python-sqlparse.googlecode.com/files/${sq}.tar.gz";
 
-  local v="0.5.0";
+  local v="0.6.1";
   local n="pyflakes";
   local p="${n}-${v}";
-  py_dependency -o -v "${v}" -m "568dab27c42e5822787aa8a603898672" \
+  py_dependency -o -v "${v}" -m "00debd2280b962e915dfee552a675915" \
     "Pyflakes" "${n}" "${p}" \
     "${pypi}/p/${n}/${p}.tar.gz";
  

Modified: CalendarServer/branches/users/gaya/sharedgroups/support/patchapply
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/support/patchapply	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/support/patchapply	2013-02-19 04:08:00 UTC (rev 10757)
@@ -38,8 +38,10 @@
     else:
         print "*** Failed to patch %s with %s" % (project, patch[len(cwd) + 1:],)
 
+
+
 def applypatches(project):
-    
+
     # Iterate over each patch file in the patches directory
     path = os.path.join(libpatches, project)
     if os.path.exists(path):
@@ -48,8 +50,8 @@
             if os.path.isfile(fpath) and fpath.endswith(".patch"):
                 applypatch(project, fpath)
 
+
 if __name__ == "__main__":
-
     try:
         for project in projects:
             applypatches(project)

Modified: CalendarServer/branches/users/gaya/sharedgroups/support/patchmaker
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/support/patchmaker	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/support/patchmaker	2013-02-19 04:08:00 UTC (rev 10757)
@@ -36,7 +36,7 @@
 ))
 
 def makepatches(project):
-    
+
     # First delete existing patch files.
     path = os.path.join(libpatches, project)
     print "Removing patches from directory: %s" % (path[len(cwd) + 1:],)
@@ -44,13 +44,13 @@
         fpath = os.path.join(path, file)
         if os.path.isfile(fpath):
             os.remove(fpath)
-    
+
     # Now execute svn status to find files needing to be patched.
     os.system("%s status ../%s > temppatch" % (svn, project))
-    
+
     f = open("temppatch")
     for line in f:
-        
+
         # We are only interested in new files and modified files.
         if line and line[0] == "?":
             new = True
@@ -58,7 +58,7 @@
             new = False
         else:
             continue
-        
+
         # Ignore files ending in .pyc and file names starting with ".".
         line = line[1:]
         line = line.strip()
@@ -66,20 +66,20 @@
             continue
         if os.path.basename(line)[0] == ".":
             continue
-        
+
         # Ignore build directories at the top-level
         if line[3 + len(project) + 1:].startswith("build%s" % (os.path.sep,)):
             continue
-        
+
         # Ignore modified directories - we will patch the files in them
         if not new and os.path.isdir(line):
             continue
-        
+
         # Generate the name of the patch file we want to create.
         patch = line[:line.rfind(".")]
         patch = patch[3 + len(project) + 1:]
         patch = patch.replace(os.sep, ".") + ".patch"
-        
+
         # Check whether this is a patch we want to ignore
         if patch in ignores:
             continue
@@ -87,7 +87,7 @@
         patch = os.path.join(path, patch)
 
         print ("Creating diff file %s for existing file %s", "Creating diff file %s for new file %s")[new] % (patch[len(cwd) + 1:], line)
-        
+
         # Generate an svn diff.
         # NB For new files we have to first add them then do the diff, then revert,
         # otherwise svn diff complains about an unversioned resource.
@@ -96,7 +96,7 @@
         os.system("%s diff %s > tempdiff" % (svn, line))
         if new:
             os.system("%s revert -q %s" % (svn, line))
-        
+
         # Now replace the directory prefix in the diff output and write to the actual patch file.
         repl = "../%s/" % (project,)
         fin = open("tempdiff")
@@ -106,12 +106,12 @@
         fout.close()
         fin.close()
         os.remove("tempdiff")
-        
+
     f.close()
     os.remove("temppatch")
-    
+
+
 if __name__ == "__main__":
-
     try:
         for project in projects:
             makepatches(project)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/__init__.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/__init__.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -15,8 +15,9 @@
 ##
 
 """
-Extentions to the Twisted Framework.
+Extensions to the Twisted Framework.
 """
 
 from twext import patches
-patches; del(patches)
+patches
+del(patches)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/ienterprise.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/ienterprise.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/ienterprise.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -296,3 +296,17 @@
         @rtype: L{twext.enterprise.queue.WorkItem}
         """
 
+
+    def callWithNewProposals(self, callback):
+        """
+        Tells the IQueuer to call a callback method whenever a new WorkProposal
+        is created.
+
+        @param callback: a callable which accepts a single parameter, a
+            L{WorkProposal}
+        """
+
+    def transferProposalCallbacks(self, newQueuer):
+        """
+        Transfer the registered callbacks to the new queuer.
+        """
\ No newline at end of file

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/queue.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/queue.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/queue.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -710,6 +710,7 @@
         return result
 
 
+    @PerformWork.responder
     def performWork(self, table, workID):
         """
         Dispatch work to this worker.
@@ -742,6 +743,9 @@
                                                        boxReceiver, locator)
         self.transactionFactory = transactionFactory
         self.whenConnected = whenConnected
+        # FIXME: Glyph it appears WorkProposal expects this to have reactor...
+        from twisted.internet import reactor
+        self.reactor = reactor
 
 
     def startReceivingBoxes(self, sender):
@@ -1012,9 +1016,45 @@
         """
         return _cloneDeferred(self._whenCommitted)
 
+class _BaseQueuer(object):
+    implements(IQueuer)
 
+    def __init__(self):
+        super(_BaseQueuer, self).__init__()
+        self.proposalCallbacks = set()
 
-class PeerConnectionPool(MultiService, object):
+    def callWithNewProposals(self, callback):
+        self.proposalCallbacks.add(callback);
+
+    def transferProposalCallbacks(self, newQueuer):
+        newQueuer.proposalCallbacks = self.proposalCallbacks
+        return newQueuer
+
+    def enqueueWork(self, txn, workItemType, **kw):
+        """
+        There is some work to do.  Do it, someplace else, ideally in parallel.
+        Later, let the caller know that the work has been completed by firing a
+        L{Deferred}.
+
+        @param workItemType: The type of work item to be enqueued.
+        @type workItemType: A subtype of L{WorkItem}
+
+        @param kw: The parameters to construct a work item.
+        @type kw: keyword parameters to C{workItemType.create}, i.e.
+            C{workItemType.__init__}
+
+        @return: an object that can track the enqueuing and remote execution of
+            this work.
+        @rtype: L{WorkProposal}
+        """
+        wp = WorkProposal(self, txn, workItemType, kw)
+        wp._start()
+        for callback in self.proposalCallbacks:
+            callback(wp)
+        return wp
+
+
+class PeerConnectionPool(_BaseQueuer, MultiService, object):
     """
     Each node has a L{PeerConnectionPool} connecting it to all the other nodes
     currently active on the same database.
@@ -1153,28 +1193,8 @@
         return self.choosePerformer(onlyLocally=True).performWork(table, workID)
 
 
-    def enqueueWork(self, txn, workItemType, **kw):
-        """
-        There is some work to do.  Do it, someplace else, ideally in parallel.
-        Later, let the caller know that the work has been completed by firing a
-        L{Deferred}.
 
-        @param workItemType: The type of work item to be enqueued.
-        @type workItemType: A subtype of L{WorkItem}
 
-        @param kw: The parameters to construct a work item.
-        @type kw: keyword parameters to C{workItemType.create}, i.e.
-            C{workItemType.__init__}
-
-        @return: an object that can track the enqueuing and remote execution of
-            this work.
-        @rtype: L{WorkProposal}
-        """
-        wp = WorkProposal(self, txn, workItemType, kw)
-        wp._start()
-        return wp
-
-
     def allWorkItemTypes(self):
         """
         Load all the L{WorkItem} types that this node can process and return
@@ -1397,13 +1417,16 @@
 
 
 
-class LocalQueuer(object):
+
+
+class LocalQueuer(_BaseQueuer):
     """
     When work is enqueued with this queuer, it is just executed locally.
     """
     implements(IQueuer)
 
     def __init__(self, txnFactory, reactor=None):
+        super(LocalQueuer, self).__init__()
         self.txnFactory = txnFactory
         if reactor is None:
             from twisted.internet import reactor
@@ -1414,23 +1437,4 @@
         """
         Choose to perform the work locally.
         """
-        return LocalPerformer(self.txnFactory)
-
-
-    def enqueueWork(self, txn, workItemType, **kw):
-        """
-        Do this work in the local process.
-
-        @see: L{PeerConnectionPool.enqueueWork}
-
-        @return: a pseudo work proposal, since everything completes at the same
-            time.
-        @rtype: L{WorkProposal}
-        """
-        wp = WorkProposal(self, txn, workItemType, kw)
-        wp._start()
-        return wp
-
-
-
-
+        return LocalPerformer(self.txnFactory)
\ No newline at end of file

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/test/test_queue.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/test/test_queue.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/test/test_queue.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -54,6 +54,9 @@
 from zope.interface.verify import verifyObject
 from twisted.test.proto_helpers import StringTransport
 
+from twext.enterprise.queue import _BaseQueuer
+import twext.enterprise.queue
+
 class Clock(_Clock):
     """
     More careful L{IReactorTime} fake which mimics the exception behavior of
@@ -334,7 +337,7 @@
 
 
     @inlineCallbacks
-    def test_notBeforeWhenCheckingForLostWork(self):
+    def FIXME_test_notBeforeWhenCheckingForLostWork(self):
         """
         L{PeerConnectionPool._periodicLostWorkCheck} should execute any
         outstanding work items, but only those that are expired.
@@ -627,3 +630,27 @@
         self.assertEquals(rows, [[4321, 7]])
 
 
+class DummyProposal(object):
+
+    def __init__(self, *ignored):
+        pass
+
+    def _start(self):
+        pass
+
+class BaseQueuerTests(TestCase):
+
+    def setUp(self):
+        self.proposal = None
+        self.patch(twext.enterprise.queue, "WorkProposal", DummyProposal)
+
+    def _proposalCallback(self, proposal):
+        self.proposal = proposal
+
+    def test_proposalCallbacks(self):
+        queuer = _BaseQueuer()
+        queuer.callWithNewProposals(self._proposalCallback)
+        self.assertEqual(self.proposal, None)
+        queuer.enqueueWork(None, None)
+        self.assertNotEqual(self.proposal, None)
+

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/python/test/test_timezone.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/python/test/test_timezone.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/python/test/test_timezone.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/python/timezone.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/python/timezone.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/python/timezone.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/web2/auth/digest.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/web2/auth/digest.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/web2/auth/digest.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -28,17 +28,25 @@
 
 http://www.faqs.org/rfcs/rfc2617.html
 """
-import sys
-import time
-import random
 
-from zope.interface import implements, Interface
+from zope.interface import implements
 
 from twisted.python.hashlib import md5, sha1
-from twisted.cred import credentials, error
-from twisted.internet.defer import succeed
+from twisted.cred import credentials
+
+# FIXME: Technically speaking - although you can't tell from looking at them -
+# these APIs are private, they're defined within twisted.cred._digest.  There
+# should probably be some upstream bugs agains Twisted to more aggressively hide
+# implementation details like these if they're not supposed to be used, so we
+# can see the private-ness more clearly.  The fix is really just to eliminate
+# this whole module though, and use the Twisted stuff via the public interface,
+# which should be sufficient to do digest auth.
+
+from twisted.cred.credentials import (calcHA1 as _origCalcHA1,
+                                      calcResponse as _origCalcResponse,
+                                      calcHA2 as _origCalcHA2)
+from twisted.internet.defer import maybeDeferred
 from twext.web2.auth.interfaces import ICredentialFactory
-from twext.web2.http_headers import Token, tokenize, split, parseKeyValue
 
 
 # The digest math
@@ -50,58 +58,29 @@
 }
 
 # DigestCalcHA1
-def calcHA1(
-    pszAlg,
-    pszUserName,
-    pszRealm,
-    pszPassword,
-    pszNonce,
-    pszCNonce,
-    preHA1=None
-):
+def calcHA1(pszAlg, pszUserName, pszRealm, pszPassword, pszNonce, pszCNonce,
+            preHA1=None):
     """
     @param pszAlg: The name of the algorithm to use to calculate the digest.
         Currently supported are md5 md5-sess and sha.
 
     @param pszUserName: The username
+
     @param pszRealm: The realm
+
     @param pszPassword: The password
+
     @param pszNonce: The nonce
+
     @param pszCNonce: The cnonce
 
     @param preHA1: If available this is a str containing a previously
-       calculated HA1 as a hex string. If this is given then the values for
-       pszUserName, pszRealm, and pszPassword are ignored.
+        calculated HA1 as a hex string.  If this is given then the values for
+        pszUserName, pszRealm, and pszPassword are ignored.
     """
+    return _origCalcHA1(pszAlg, pszUserName, pszRealm, pszPassword, pszNonce,
+                        pszCNonce, preHA1)
 
-    if (preHA1 and (pszUserName or pszRealm or pszPassword)):
-        raise TypeError(("preHA1 is incompatible with the pszUserName, "
-                         "pszRealm, and pszPassword arguments"))
-
-    if preHA1 is None:
-        # We need to calculate the HA1 from the username:realm:password
-        m = algorithms[pszAlg]()
-        m.update(pszUserName)
-        m.update(":")
-        m.update(pszRealm)
-        m.update(":")
-        m.update(pszPassword)
-        HA1 = m.digest()
-    else:
-        # We were given a username:realm:password
-        HA1 = preHA1.decode('hex')
-
-    if pszAlg == "md5-sess":
-        m = algorithms[pszAlg]()
-        m.update(HA1)
-        m.update(":")
-        m.update(pszNonce)
-        m.update(":")
-        m.update(pszCNonce)
-        HA1 = m.digest()
-
-    return HA1.encode('hex')
-
 # DigestCalcResponse
 def calcResponse(
     HA1,
@@ -114,285 +93,39 @@
     pszDigestUri,
     pszHEntity,
 ):
-    m = algorithms[algo]()
-    m.update(pszMethod)
-    m.update(":")
-    m.update(pszDigestUri)
-    if pszQop == "auth-int":
-        m.update(":")
-        m.update(pszHEntity)
-    HA2 = m.digest().encode('hex')
+    return _origCalcResponse(HA1, _origCalcHA2(algo, pszMethod, pszDigestUri,
+                                               pszQop, pszHEntity),
+                             algo, pszNonce, pszNonceCount, pszCNonce, pszQop)
 
-    m = algorithms[algo]()
-    m.update(HA1)
-    m.update(":")
-    m.update(pszNonce)
-    m.update(":")
-    if pszNonceCount and pszCNonce: # pszQop:
-        m.update(pszNonceCount)
-        m.update(":")
-        m.update(pszCNonce)
-        m.update(":")
-        m.update(pszQop)
-        m.update(":")
-    m.update(HA2)
-    respHash = m.digest().encode('hex')
-    return respHash
 
 
-class IUsernameDigestHash(Interface):
-    """
-    This credential is used when a CredentialChecker has access to the hash
-    of the username:realm:password as in an Apache .htdigest file.
-    """
-    def checkHash(self, digestHash):
-        """
-        @param digestHash: The hashed username:realm:password to check against.
+DigestedCredentials = credentials.DigestedCredentials
 
-        @return: a deferred which becomes, or a boolean indicating if the
-            hash matches.
-        """
-
-
-class DigestedCredentials:
-    """Yet Another Simple HTTP Digest authentication scheme"""
-
-    implements(credentials.IUsernameHashedPassword,
-               IUsernameDigestHash)
-
-    def __init__(self, username, method, realm, fields, originalMethod=None):
-        self.username = username
-        self.method = method
-        self.realm = realm
-        self.fields = fields
-        if originalMethod:
-            self.originalMethod = originalMethod
-        else:
-            self.originalMethod = method
-
-    def checkPassword(self, password):
-        response = self.fields.get('response')
-        uri = self.fields.get('uri')
-        nonce = self.fields.get('nonce')
-        cnonce = self.fields.get('cnonce')
-        nc = self.fields.get('nc')
-        algo = self.fields.get('algorithm', 'md5').lower()
-        qop = self.fields.get('qop', 'auth')
-
-        expected = calcResponse(
-            calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
-            algo, nonce, nc, cnonce, qop, self.originalMethod, uri, None
-        )
-
-        if expected == response:
-            return True
-
-        # IE7 sends cnonce and nc values, but auth fails if they are used.
-        # So try again without them...
-        # They can be omitted for backwards compatibility [RFC 2069].
-        if nc is not None or cnonce is not None:
-            expected = calcResponse(
-                calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
-                algo, nonce, None, None, qop, self.originalMethod, uri, None
-            )
-            if expected == response:
-                return True
-
-    def checkHash(self, digestHash):
-        response = self.fields.get('response')
-        uri = self.fields.get('uri')
-        nonce = self.fields.get('nonce')
-        cnonce = self.fields.get('cnonce')
-        nc = self.fields.get('nc')
-        algo = self.fields.get('algorithm', 'md5').lower()
-        qop = self.fields.get('qop', 'auth')
-
-        expected = calcResponse(
-            calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash),
-            algo, nonce, nc, cnonce, qop, self.originalMethod, uri, None
-        )
-
-        return expected == response
-
-
 class DigestCredentialFactory(object):
-    """
-    Support for RFC2617 HTTP Digest Authentication
-
-    @cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an
-        opaque should be valid.
-
-    @ivar privateKey: A random string used for generating the secure opaque.
-    """
-
     implements(ICredentialFactory)
 
-    CHALLENGE_LIFETIME_SECS = 15 * 60    # 15 minutes
+    CHALLENGE_LIFETIME_SECS = (
+        credentials.DigestCredentialFactory.CHALLENGE_LIFETIME_SECS
+    )
 
-    scheme = "digest"
-
     def __init__(self, algorithm, realm):
-        """
-        @type algorithm: C{str}
-        @param algorithm: case insensitive string that specifies
-            the hash algorithm used, should be either, md5, md5-sess
-            or sha
+        self._real = credentials.DigestCredentialFactory(algorithm, realm)
 
-        @type realm: C{str}
-        @param realm: case sensitive string that specifies the realm
-            portion of the challenge
-        """
-        self.algorithm = algorithm
-        self.realm = realm
+    scheme = 'digest'
 
-        c = tuple([random.randrange(sys.maxint) for _ in range(3)])
+    def getChallenge(self, peer):
+        return maybeDeferred(self._real.getChallenge, peer.host)
 
-        self.privateKey = '%d%d%d' % c
 
-    def generateNonce(self):
-        c = tuple([random.randrange(sys.maxint) for _ in range(3)])
-        c = '%d%d%d' % c
-        return c
+    def generateOpaque(self, *a, **k):
+        return self._real._generateOpaque(*a, **k)
 
-    def _getTime(self):
-        """
-        Parameterize the time based seed used in generateOpaque
-        so we can deterministically unittest it's behavior.
-        """
-        return time.time()
 
-    def generateOpaque(self, nonce, clientip):
-        """
-        Generate an opaque to be returned to the client.
-        This should be a unique string that can be returned to us and verified.
-        """
-
-        # Now, what we do is encode the nonce, client ip and a timestamp
-        # in the opaque value with a suitable digest
-        key = "%s,%s,%s" % (nonce, clientip, str(int(self._getTime())))
-        digest = md5(key + self.privateKey).hexdigest()
-        ekey = key.encode('base64')
-        return "%s-%s" % (digest, ekey.replace('\n', ''))
-
     def verifyOpaque(self, opaque, nonce, clientip):
-        """
-        Given the opaque and nonce from the request, as well as the clientip
-        that made the request, verify that the opaque was generated by us.
-        And that it's not too old.
+        return self._real._verifyOpaque(opaque, nonce, clientip)
 
-        @param opaque: The opaque value from the Digest response
-        @param nonce: The nonce value from the Digest response
-        @param clientip: The remote IP address of the client making the request
 
-        @return: C{True} if the opaque was successfully verified.
-
-        @raise error.LoginFailed: if C{opaque} could not be parsed or
-            contained the wrong values.
-        """
-
-        # First split the digest from the key
-        opaqueParts = opaque.split('-')
-        if len(opaqueParts) != 2:
-            raise error.LoginFailed('Invalid response, invalid opaque value')
-
-        # Verify the key
-        key = opaqueParts[1].decode('base64')
-        keyParts = key.split(',')
-
-        if len(keyParts) != 3:
-            raise error.LoginFailed('Invalid response, invalid opaque value')
-
-        if keyParts[0] != nonce:
-            raise error.LoginFailed(
-                'Invalid response, incompatible opaque/nonce values')
-
-        if keyParts[1] != clientip:
-            raise error.LoginFailed(
-                'Invalid response, incompatible opaque/client values')
-
-        if (int(self._getTime()) - int(keyParts[2]) >
-            DigestCredentialFactory.CHALLENGE_LIFETIME_SECS):
-
-            raise error.LoginFailed(
-                'Invalid response, incompatible opaque/nonce too old')
-
-        # Verify the digest
-        digest = md5(key + self.privateKey).hexdigest()
-        if digest != opaqueParts[0]:
-            raise error.LoginFailed('Invalid response, invalid opaque value')
-
-        return True
-
-    def getChallenge(self, peer):
-        """
-        Generate the challenge for use in the WWW-Authenticate header
-
-        @param peer: The L{IAddress} of the requesting client.
-
-        @return: The C{dict} that can be used to generate a WWW-Authenticate
-            header.
-        """
-
-        c = self.generateNonce()
-        o = self.generateOpaque(c, peer.host)
-
-        return succeed({'nonce': c,
-            'opaque': o,
-            'qop': 'auth',
-            'algorithm': self.algorithm,
-            'realm': self.realm,
-        })
-
     def decode(self, response, request):
-        """
-        Decode the given response and attempt to generate a
-        L{DigestedCredentials} from it.
-
-        @type response: C{str}
-        @param response: A string of comma seperated key=value pairs
-
-        @type request: L{twext.web2.server.Request}
-        @param request: the request being processed
-
-        @return: L{DigestedCredentials}
-
-        @raise: L{error.LoginFailed} if the response does not contain a
-            username, a nonce, an opaque, or if the opaque is invalid.
-        """
-        response = ' '.join(response.splitlines())
-        
-        try:
-            parts = split(tokenize((response,), foldCase=False), Token(","))
-    
-            auth = {}
-    
-            for (k, v) in [parseKeyValue(p) for p in parts]:
-                auth[k.strip()] = v.strip()
-        except ValueError:
-            raise error.LoginFailed('Invalid response.')
-            
-        username = auth.get('username')
-        if not username:
-            raise error.LoginFailed('Invalid response, no username given.')
-
-        if 'opaque' not in auth:
-            raise error.LoginFailed('Invalid response, no opaque given.')
-
-        if 'nonce' not in auth:
-            raise error.LoginFailed('Invalid response, no nonce given.')
-
-        # Now verify the nonce/opaque values for this client
-        if self.verifyOpaque(auth.get('opaque'),
-                             auth.get('nonce'),
-                             request.remoteAddr.host):
-
-            if hasattr(request, "originalMethod"):
-                originalMethod = request.originalMethod
-            else:
-                originalMethod = None
-
-            return succeed(DigestedCredentials(username,
-                                               request.method,
-                                               self.realm,
-                                               auth,
-                                               originalMethod))
+        method = getattr(request, "originalMethod", request.method)
+        host = request.remoteAddr.host
+        return self._real.decode(response, method, host)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/method/propfind.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/method/propfind.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/method/propfind.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -207,6 +207,13 @@
 
             propstats.append(xml_propstat)
 
+        # Always need to have at least one propstat present (required by Prefer header behavior)
+        if len(propstats) == 0:
+            propstats.append(davxml.PropertyStatus(
+                davxml.PropertyContainer(),
+                davxml.Status.fromResponseCode(responsecode.OK)
+            ))
+
         xml_resource = davxml.HRef(uri)
         xml_response = davxml.PropertyStatusResponse(xml_resource, *propstats)
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/test/test_auth.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/test/test_auth.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/test/test_auth.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Computer, Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Computer, Inc. All rights reserved.
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/test/util.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/test/util.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/test/util.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -29,20 +29,62 @@
 from shutil import copy
 
 from twisted.trial import unittest
+from twisted.internet import address
+
 from twisted.internet.defer import Deferred
 
 from twext.python.log import Logger
 from twext.web2.http import HTTPError, StatusResponse
-from twext.web2 import responsecode
+from twext.web2 import responsecode, server
+from twext.web2 import http_headers
+from twext.web2 import stream
+
 from twext.web2.dav.resource import TwistedACLInheritable
 from twext.web2.dav.static import DAVFile
 from twext.web2.dav.util import joinURL
 from txdav.xml import element
 from txdav.xml.base import encodeXMLName
+from twext.web2.http_headers import MimeType
+from twext.web2.dav.util import allDataFromStream
 
 log = Logger()
 
 
+
+class SimpleRequest(server.Request):
+    """
+    A L{SimpleRequest} can be used in cases where a L{server.Request} object is
+    necessary but it is beneficial to bypass the concrete transport (and
+    associated logic with the C{chanRequest} attribute).
+    """
+
+    clientproto = (1,1)
+
+    def __init__(self, site, method, uri, headers=None, content=None):
+        if not headers:
+            headers = http_headers.Headers(headers)
+
+        super(SimpleRequest, self).__init__(
+            site=site,
+            chanRequest=None,
+            command=method,
+            path=uri,
+            version=self.clientproto,
+            contentLength=len(content or ''),
+            headers=headers)
+
+        self.stream = stream.MemoryStream(content or '')
+
+        self.remoteAddr = address.IPv4Address('TCP', '127.0.0.1', 0)
+        self._parseURL()
+        self.host = 'localhost'
+        self.port = 8080
+
+    def writeResponse(self, response):
+        return response
+
+
+
 class InMemoryPropertyStore (object):
     """
     A dead property store for keeping properties in memory
@@ -202,6 +244,17 @@
 
 
     def send(self, request, callback=None):
+        """
+        Invoke the logic involved in traversing a given L{server.Request} as if
+        a client had sent it; call C{locateResource} to look up the resource to
+        be rendered, and render it by calling its C{renderHTTP} method.
+
+        @param request: A L{server.Request} (generally, to avoid real I/O, a
+            L{SimpleRequest}) already associated with a site.
+
+        @return: asynchronously return a response object or L{None}
+        @rtype: L{Deferred} firing L{Response} or L{None}
+        """
         log.msg("Sending %s request for URI %s" % (request.method, request.uri))
 
         d = request.locateResource(request.uri)
@@ -216,6 +269,57 @@
 
         return d
 
+
+    def simpleSend(self, method, path="/", body="", mimetype="text",
+                   subtype="xml", resultcode=responsecode.OK, headers=()):
+        """
+        Assemble and send a simple request using L{SimpleRequest}.  This
+        L{SimpleRequest} is associated with this L{TestCase}'s C{site}
+        attribute.
+
+        @param method: the HTTP method
+        @type method: L{bytes}
+
+        @param path: the absolute path portion of the HTTP URI
+        @type path: L{bytes}
+
+        @param body: the content body of the request
+        @type body: L{bytes}
+
+        @param mimetype: the main type of the mime type of the body of the
+            request
+        @type mimetype: L{bytes}
+
+        @param subtype: the subtype of the mimetype of the body of the request
+        @type subtype: L{bytes}
+
+        @param resultcode: The expected result code for the response to the
+            request.
+        @type resultcode: L{int}
+
+        @param headers: An iterable of 2-tuples of C{(header, value)}; headers
+            to set on the outgoing request.
+
+        @return: a L{Deferred} which fires with a L{bytes}  if the request was
+            successfully processed and fails with an L{HTTPError} if not; or,
+            if the resultcode does not match the response's code, fails with
+            L{FailTest}.
+        """
+        request = SimpleRequest(self.site, method, path, content=body)
+        if headers is not None:
+            for k, v in headers:
+                request.headers.setHeader(k, v)
+        request.headers.setHeader("content-type", MimeType(mimetype, subtype))
+        def checkResult(response):
+            self.assertEqual(response.code, resultcode)
+            if response.stream is None:
+                return None
+            return allDataFromStream(response.stream)
+        return self.send(request, None).addCallback(checkResult)
+
+
+
+
 class Site:
     # FIXME: There is no ISite interface; there should be.
     # implements(ISite)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_http_headers.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_http_headers.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_http_headers.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -799,10 +799,10 @@
 
     def testRoundtrip(self):
         for _ignore in range(2000):
-            time = random.randint(0, 2000000000)
-            timestr = http_headers.generateDateTime(time)
+            randomTime = random.randint(0, 2000000000)
+            timestr = http_headers.generateDateTime(randomTime)
             time2 = http_headers.parseDateTime(timestr)
-            self.assertEquals(time, time2)
+            self.assertEquals(randomTime, time2)
 
 
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_httpauth.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_httpauth.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_httpauth.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -16,6 +16,41 @@
 
 _trivial_GET = SimpleRequest(None, 'GET', '/')
 
+FAKE_STATIC_NONCE = '178288758716122392881254770685'
+
+
+def makeDigestDeterministic(twistedDigestFactory, key="0",
+                            nonce=FAKE_STATIC_NONCE, time=0):
+    """
+    Patch up various bits of private state to make a digest credential factory
+    (the one that comes from Twisted) behave deterministically.
+    """
+
+    def _fakeStaticNonce():
+        """
+        Generate a static nonce
+        """
+        return nonce
+
+    def _fakeStaticTime():
+        """
+        Return a stable time
+        """
+        return time
+
+    twistedDigestFactory.privateKey = key
+
+    # FIXME: These tests are somewhat redundant with the tests for Twisted's
+    # built-in digest auth; these private values need to be patched to
+    # create deterministic results, but at some future point the whole
+    # digest module should be removed from twext.web2 (as all of twext.web2
+    # should be removed) and we can just get rid of this.
+
+    twistedDigestFactory._generateNonce = _fakeStaticNonce
+    twistedDigestFactory._getTime = _fakeStaticTime
+
+
+
 class FakeDigestCredentialFactory(digest.DigestCredentialFactory):
     """
     A Fake Digest Credential Factory that generates a predictable
@@ -24,22 +59,12 @@
 
     def __init__(self, *args, **kwargs):
         super(FakeDigestCredentialFactory, self).__init__(*args, **kwargs)
+        makeDigestDeterministic(self._real, self._fakeStaticPrivateKey)
 
-        self.privateKey = "0"
+    _fakeStaticPrivateKey = "0"
 
-    def generateNonce(self):
-        """
-        Generate a static nonce
-        """
-        return '178288758716122392881254770685'
 
-    def _getTime(self):
-        """
-        Return a stable time
-        """
-        return 0
 
-
 class BasicAuthTestCase(unittest.TestCase):
     def setUp(self):
         self.credentialFactory = basic.BasicCredentialFactory('foo')
@@ -376,7 +401,8 @@
             key = '%s,%s,%s' % (challenge['nonce'],
                                 clientAddress.host,
                                 '-137876876')
-            digest = md5(key + credentialFactory.privateKey).hexdigest()
+            digest = (md5(key + credentialFactory._fakeStaticPrivateKey)
+                      .hexdigest())
             ekey = key.encode('base64')
 
             oldNonceOpaque = '%s-%s' % (digest, ekey.strip('\n'))

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_server.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_server.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/web2/test/test_server.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -10,6 +10,8 @@
 from twisted.python import components
 from twext.web2 import http, http_headers, iweb, server
 from twext.web2 import resource, stream
+from twext.web2.dav.test.util import SimpleRequest
+
 from twisted.trial import unittest
 from twisted.internet import reactor, defer, address
 
@@ -114,37 +116,6 @@
 
 
 
-class SimpleRequest(server.Request):
-    """I can be used in cases where a Request object is necessary
-    but it is beneficial to bypass the chanRequest
-    """
-
-    clientproto = (1,1)
-
-    def __init__(self, site, method, uri, headers=None, content=None):
-        if not headers:
-            headers = http_headers.Headers(headers)
-
-        super(SimpleRequest, self).__init__(
-            site=site,
-            chanRequest=None,
-            command=method,
-            path=uri,
-            version=self.clientproto,
-            contentLength=len(content or ''),
-            headers=headers)
-
-        self.stream = stream.MemoryStream(content or '')
-
-        self.remoteAddr = address.IPv4Address('TCP', '127.0.0.1', 0)
-        self._parseURL()
-        self.host = 'localhost'
-        self.port = 8080
-
-    def writeResponse(self, response):
-        return response
-
-
 class TestChanRequest:
     implements(iweb.IChanRequest)
 

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twext/who/__init__.py
===================================================================
--- CalendarServer/trunk/twext/who/__init__.py	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Directory service integration
-"""

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/who/__init__.py (from rev 10753, CalendarServer/trunk/twext/who/__init__.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/who/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Directory service integration
+"""

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twext/who/directory.py
===================================================================
--- CalendarServer/trunk/twext/who/directory.py	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/directory.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,262 +0,0 @@
-##
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Generic directory service base implementation
-"""
-
-__all__ = [
-    "DirectoryService",
-    "DirectoryRecord",
-]
-
-from uuid import UUID
-
-from zope.interface import implements
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.internet.defer import succeed, fail
-
-from twext.who.idirectory import QueryNotSupportedError, NotAllowedError
-from twext.who.idirectory import FieldName, RecordType
-from twext.who.idirectory import Operand
-from twext.who.idirectory import DirectoryQueryMatchExpression
-from twext.who.idirectory import IDirectoryService, IDirectoryRecord
-from twext.who.util import uniqueResult, describe
-
-
-
-class DirectoryService(object):
-    implements(IDirectoryService)
-
-    recordType = RecordType
-    fieldName  = FieldName
-
-    normalizedFields = {
-        FieldName.guid:           lambda g: UUID(g).hex,
-        FieldName.emailAddresses: lambda e: e.lower(),
-    }
-
-
-    def __init__(self, realmName):
-        self.realmName = realmName
-
-
-    def __repr__(self):
-        return "<%s %r>" % (
-            self.__class__.__name__,
-            self.realmName,
-        )
-
-
-    def recordTypes(self):
-        return succeed(self.recordType.iterconstants())
-
-
-    def recordsFromExpression(self, expression, records=None):
-        """
-        Finds records matching a single expression.
-        @param expression: an expression
-        @type expression: L{object}
-        @param records: a set of records to search within. C{None} if
-            the whole directory should be searched.
-        @type records: L{set} or L{frozenset}
-        """
-        return fail(QueryNotSupportedError("Unknown expression: %s" % (expression,)))
-
-
-    @inlineCallbacks
-    def recordsFromQuery(self, expressions, operand=Operand.AND):
-        expressionIterator = iter(expressions)
-
-        try:
-            expression = expressionIterator.next()
-        except StopIteration:
-            returnValue(())
-
-        results = set((yield self.recordsFromExpression(expression)))
-
-        for expression in expressions:
-            if operand == Operand.AND:
-                if not results:
-                    # No need to bother continuing here
-                    returnValue(())
-
-                records = results
-            else:
-                records = None
-
-            recordsMatchingExpression = frozenset((
-                yield self.recordsFromExpression(expression, records=records)
-            ))
-
-            if operand == Operand.AND:
-                results &= recordsMatchingExpression
-            elif operand == Operand.OR:
-                results |= recordsMatchingExpression
-            else:
-                raise QueryNotSupportedError("Unknown operand: %s" % (operand,))
-
-        returnValue(results)
-
-
-    def recordsWithFieldValue(self, fieldName, value):
-        return self.recordsFromExpression(DirectoryQueryMatchExpression(fieldName, value))
-
-    @inlineCallbacks
-    def recordWithUID(self, uid):
-        returnValue(uniqueResult((yield self.recordsWithFieldValue(FieldName.uid, uid))))
-               
-    @inlineCallbacks
-    def recordWithGUID(self, guid):
-        returnValue(uniqueResult((yield self.recordsWithFieldValue(FieldName.guid, guid))))
-
-    def recordsWithRecordType(self, recordType):
-        return self.recordsWithFieldValue(FieldName.recordType, recordType)
-
-    @inlineCallbacks
-    def recordWithShortName(self, recordType, shortName):
-        returnValue(uniqueResult((yield self.recordsFromQuery((
-            DirectoryQueryMatchExpression(FieldName.recordType, recordType),
-            DirectoryQueryMatchExpression(FieldName.shortNames, shortName ),
-        )))))
-
-    def recordsWithEmailAddress(self, emailAddress):
-        return self.recordsWithFieldValue(FieldName.emailAddresses, emailAddress)
-
-    def updateRecords(self, records, create=False):
-        for record in records:
-            raise NotAllowedError("Record updates not allowed.")
-
-    def removeRecords(self, uids):
-        for uid in uids:
-            raise NotAllowedError("Record removal not allowed.")
-
-
-
-class DirectoryRecord(object):
-    implements(IDirectoryRecord)
-
-    requiredFields = (
-        FieldName.uid,
-        FieldName.recordType,
-        FieldName.shortNames,
-    )
-
-
-    def __init__(self, service, fields):
-        for fieldName in self.requiredFields:
-            if fieldName not in fields or not fields[fieldName]:
-                raise ValueError("%s field is required." % (fieldName,))
-
-            if FieldName.isMultiValue(fieldName):
-                values = fields[fieldName]
-                if len(values) == 0:
-                    raise ValueError("%s field must have at least one value." % (fieldName,))
-                for value in values:
-                    if not value:
-                        raise ValueError("%s field must not be empty." % (fieldName,))
-
-        if fields[FieldName.recordType] not in service.recordType.iterconstants():
-            raise ValueError("Record type must be one of %r, not %r." % (
-                tuple(service.recordType.iterconstants()),
-                fields[FieldName.recordType]
-            ))
-
-        # Normalize fields
-        normalizedFields = {}
-        for name, value in fields.items():
-            normalize = service.normalizedFields.get(name, None)
-
-            if normalize is None:
-                normalizedFields[name] = value
-                continue
-
-            if FieldName.isMultiValue(name):
-                normalizedFields[name] = tuple((normalize(v) for v in value))
-            else:
-                normalizedFields[name] = normalize(value)
-        
-        self.service = service
-        self.fields  = normalizedFields
-
-
-    def __repr__(self):
-        return "<%s (%s)%s>" % (
-            self.__class__.__name__,
-            describe(self.recordType),
-            self.shortNames[0],
-        )
-
-
-    def __eq__(self, other):
-        if IDirectoryRecord.implementedBy(other.__class__):
-            return (
-                self.service == other.service and
-                self.fields == other.fields
-            )
-        return NotImplemented
-
-
-    def __ne__(self, other):
-        eq = self.__eq__(other)
-        if eq is NotImplemented:
-            return NotImplemented
-        return not eq
-
-
-    def __getattr__(self, name):
-        try:
-            fieldName = self.service.fieldName.lookupByName(name)
-        except ValueError:
-            raise AttributeError(name)
-
-        try:
-            return self.fields[fieldName]
-        except KeyError:
-            raise AttributeError(name)
-
-
-    def description(self):
-        description = [self.__class__.__name__, ":"]
-
-        for name, value in self.fields.items():
-            if hasattr(name, "description"):
-                name = name.description
-            else:
-                name = str(name)
-
-            if hasattr(value, "description"):
-                value = value.description
-            else:
-                value = str(value)
-
-            description.append("\n  ")
-            description.append(name)
-            description.append(" = ")
-            description.append(value)
-
-        return "".join(description)
-
-
-    def members(self):
-        if self.recordType == RecordType.group:
-            raise NotImplementedError()
-        return succeed(())
-
-
-    def groups(self):
-        raise NotImplementedError()

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/who/directory.py (from rev 10753, CalendarServer/trunk/twext/who/directory.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/who/directory.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/directory.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,262 @@
+##
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Generic directory service base implementation
+"""
+
+__all__ = [
+    "DirectoryService",
+    "DirectoryRecord",
+]
+
+from uuid import UUID
+
+from zope.interface import implements
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import succeed, fail
+
+from twext.who.idirectory import QueryNotSupportedError, NotAllowedError
+from twext.who.idirectory import FieldName, RecordType
+from twext.who.idirectory import Operand
+from twext.who.idirectory import DirectoryQueryMatchExpression
+from twext.who.idirectory import IDirectoryService, IDirectoryRecord
+from twext.who.util import uniqueResult, describe
+
+
+
+class DirectoryService(object):
+    implements(IDirectoryService)
+
+    recordType = RecordType
+    fieldName  = FieldName
+
+    normalizedFields = {
+        FieldName.guid:           lambda g: UUID(g).hex,
+        FieldName.emailAddresses: lambda e: e.lower(),
+    }
+
+
+    def __init__(self, realmName):
+        self.realmName = realmName
+
+
+    def __repr__(self):
+        return "<%s %r>" % (
+            self.__class__.__name__,
+            self.realmName,
+        )
+
+
+    def recordTypes(self):
+        return succeed(self.recordType.iterconstants())
+
+
+    def recordsFromExpression(self, expression, records=None):
+        """
+        Finds records matching a single expression.
+        @param expression: an expression
+        @type expression: L{object}
+        @param records: a set of records to search within. C{None} if
+            the whole directory should be searched.
+        @type records: L{set} or L{frozenset}
+        """
+        return fail(QueryNotSupportedError("Unknown expression: %s" % (expression,)))
+
+
+    @inlineCallbacks
+    def recordsFromQuery(self, expressions, operand=Operand.AND):
+        expressionIterator = iter(expressions)
+
+        try:
+            expression = expressionIterator.next()
+        except StopIteration:
+            returnValue(())
+
+        results = set((yield self.recordsFromExpression(expression)))
+
+        for expression in expressions:
+            if operand == Operand.AND:
+                if not results:
+                    # No need to bother continuing here
+                    returnValue(())
+
+                records = results
+            else:
+                records = None
+
+            recordsMatchingExpression = frozenset((
+                yield self.recordsFromExpression(expression, records=records)
+            ))
+
+            if operand == Operand.AND:
+                results &= recordsMatchingExpression
+            elif operand == Operand.OR:
+                results |= recordsMatchingExpression
+            else:
+                raise QueryNotSupportedError("Unknown operand: %s" % (operand,))
+
+        returnValue(results)
+
+
+    def recordsWithFieldValue(self, fieldName, value):
+        return self.recordsFromExpression(DirectoryQueryMatchExpression(fieldName, value))
+
+    @inlineCallbacks
+    def recordWithUID(self, uid):
+        returnValue(uniqueResult((yield self.recordsWithFieldValue(FieldName.uid, uid))))
+               
+    @inlineCallbacks
+    def recordWithGUID(self, guid):
+        returnValue(uniqueResult((yield self.recordsWithFieldValue(FieldName.guid, guid))))
+
+    def recordsWithRecordType(self, recordType):
+        return self.recordsWithFieldValue(FieldName.recordType, recordType)
+
+    @inlineCallbacks
+    def recordWithShortName(self, recordType, shortName):
+        returnValue(uniqueResult((yield self.recordsFromQuery((
+            DirectoryQueryMatchExpression(FieldName.recordType, recordType),
+            DirectoryQueryMatchExpression(FieldName.shortNames, shortName ),
+        )))))
+
+    def recordsWithEmailAddress(self, emailAddress):
+        return self.recordsWithFieldValue(FieldName.emailAddresses, emailAddress)
+
+    def updateRecords(self, records, create=False):
+        for record in records:
+            raise NotAllowedError("Record updates not allowed.")
+
+    def removeRecords(self, uids):
+        for uid in uids:
+            raise NotAllowedError("Record removal not allowed.")
+
+
+
+class DirectoryRecord(object):
+    implements(IDirectoryRecord)
+
+    requiredFields = (
+        FieldName.uid,
+        FieldName.recordType,
+        FieldName.shortNames,
+    )
+
+
+    def __init__(self, service, fields):
+        for fieldName in self.requiredFields:
+            if fieldName not in fields or not fields[fieldName]:
+                raise ValueError("%s field is required." % (fieldName,))
+
+            if FieldName.isMultiValue(fieldName):
+                values = fields[fieldName]
+                if len(values) == 0:
+                    raise ValueError("%s field must have at least one value." % (fieldName,))
+                for value in values:
+                    if not value:
+                        raise ValueError("%s field must not be empty." % (fieldName,))
+
+        if fields[FieldName.recordType] not in service.recordType.iterconstants():
+            raise ValueError("Record type must be one of %r, not %r." % (
+                tuple(service.recordType.iterconstants()),
+                fields[FieldName.recordType]
+            ))
+
+        # Normalize fields
+        normalizedFields = {}
+        for name, value in fields.items():
+            normalize = service.normalizedFields.get(name, None)
+
+            if normalize is None:
+                normalizedFields[name] = value
+                continue
+
+            if FieldName.isMultiValue(name):
+                normalizedFields[name] = tuple((normalize(v) for v in value))
+            else:
+                normalizedFields[name] = normalize(value)
+        
+        self.service = service
+        self.fields  = normalizedFields
+
+
+    def __repr__(self):
+        return "<%s (%s)%s>" % (
+            self.__class__.__name__,
+            describe(self.recordType),
+            self.shortNames[0],
+        )
+
+
+    def __eq__(self, other):
+        if IDirectoryRecord.implementedBy(other.__class__):
+            return (
+                self.service == other.service and
+                self.fields == other.fields
+            )
+        return NotImplemented
+
+
+    def __ne__(self, other):
+        eq = self.__eq__(other)
+        if eq is NotImplemented:
+            return NotImplemented
+        return not eq
+
+
+    def __getattr__(self, name):
+        try:
+            fieldName = self.service.fieldName.lookupByName(name)
+        except ValueError:
+            raise AttributeError(name)
+
+        try:
+            return self.fields[fieldName]
+        except KeyError:
+            raise AttributeError(name)
+
+
+    def description(self):
+        description = [self.__class__.__name__, ":"]
+
+        for name, value in self.fields.items():
+            if hasattr(name, "description"):
+                name = name.description
+            else:
+                name = str(name)
+
+            if hasattr(value, "description"):
+                value = value.description
+            else:
+                value = str(value)
+
+            description.append("\n  ")
+            description.append(name)
+            description.append(" = ")
+            description.append(value)
+
+        return "".join(description)
+
+
+    def members(self):
+        if self.recordType == RecordType.group:
+            raise NotImplementedError()
+        return succeed(())
+
+
+    def groups(self):
+        raise NotImplementedError()

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twext/who/idirectory.py
===================================================================
--- CalendarServer/trunk/twext/who/idirectory.py	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/idirectory.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,360 +0,0 @@
-##
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Directory service interface.
-"""
-
-__all__ = [
-    "DirectoryServiceError",
-    "DirectoryAvailabilityError",
-    "UnknownRecordTypeError",
-    "QueryNotSupportedError",
-    "NoSuchRecordError",
-    "NotAllowedError",
-
-    "RecordType",
-    "FieldName",
-    "MatchType",
-    "Operand",
-    "QueryFlags",
-    "DirectoryQueryMatchExpression",
-
-    "IDirectoryService",
-    "IDirectoryRecord",
-]
-
-from zope.interface import Attribute, Interface
-
-from twisted.python.constants import Names, NamedConstant
-from twisted.python.constants import Flags, FlagConstant
-
-
-
-##
-# Exceptions
-##
-
-class DirectoryServiceError(Exception):
-    """
-    Directory service generic error.
-    """
-
-class DirectoryAvailabilityError(DirectoryServiceError):
-    """
-    Directory not available.
-    """
-
-class UnknownRecordTypeError(DirectoryServiceError):
-    """
-    Unknown record type.
-    """
-    def __init__(self, token):
-        DirectoryServiceError.__init__(self, token)
-        self.token = token
-
-class QueryNotSupportedError(DirectoryServiceError):
-    """
-    Query not supported.
-    """
-
-class NoSuchRecordError(DirectoryServiceError):
-    """
-    Record does not exist.
-    """
-
-class NotAllowedError(DirectoryServiceError):
-    """
-    Apparently, you can't do that.
-    """
-
-
-
-##
-# Data Types
-##
-
-class RecordType(Names):
-    user  = NamedConstant()
-    group = NamedConstant()
-
-    user.description  = "user"
-    group.description = "group"
-
-
-
-class FieldName(Names):
-    """
-    Constants for common field names.
-    """
-    uid            = NamedConstant()
-    guid           = NamedConstant()
-    recordType     = NamedConstant()
-    shortNames     = NamedConstant()
-    fullNames      = NamedConstant()
-    emailAddresses = NamedConstant()
-    password       = NamedConstant()
-
-    uid.description            = "UID"
-    guid.description           = "GUID"
-    recordType.description     = "record type"
-    shortNames.description     = "short names"
-    fullNames.description      = "full names"
-    emailAddresses.description = "email addresses"
-    password.description       = "password"
-
-    shortNames.multiValue     = True
-    fullNames.multiValue      = True
-    emailAddresses.multiValue = True
-
-    @staticmethod
-    def isMultiValue(name):
-        return getattr(name, "multiValue", False)
-
-
-
-class MatchType(Names):
-    """
-    Query match types.
-    """
-    equals     = NamedConstant()
-    startsWith = NamedConstant()
-    contains   = NamedConstant()
-
-    equals.description     = "equals"
-    startsWith.description = "starts with"
-    contains.description   = "contains"
-
-
-
-class Operand(Names):
-    OR  = NamedConstant()
-    AND = NamedConstant()
-
-    OR.description  = "or"
-    AND.description = "and"
-
-
-
-class QueryFlags(Flags):
-    """
-    Query flags.
-    """
-    NOT = FlagConstant()
-    NOT.description = "not"
-
-    caseInsensitive = FlagConstant()
-    caseInsensitive.description = "case insensitive"
-
-
-
-class DirectoryQueryMatchExpression(object):
-    """
-    Query for a matching value in a given field.
-
-    @ivar fieldName: a L{NamedConstant} specifying the field
-    @ivar fieldValue: a text value to match
-    @ivar matchType: a L{NamedConstant} specifying the match algorythm
-    @ivar flags: L{NamedConstant} specifying additional options
-    """
-
-    def __init__(self, fieldName, fieldValue, matchType=MatchType.equals, flags=None):
-        self.fieldName  = fieldName
-        self.fieldValue = fieldValue
-        self.matchType  = matchType
-        self.flags      = flags
-
-    def __repr__(self):
-        def describe(constant):
-            return getattr(constant, "description", str(constant))
-
-        if self.flags is None:
-            flags = ""
-        else:
-            flags = " (%s)" % (self.flags,)
-
-        return "<%s: %r %s %r%s>" % (
-            self.__class__.__name__,
-            describe(self.fieldName),
-            describe(self.matchType),
-            describe(self.fieldValue),
-            flags
-        )
-
-
-
-##
-# Interfaces
-##
-
-class IDirectoryService(Interface):
-    """
-    Directory service.
-
-    A directory service is a service that vends information about
-    principals such as users, locations, printers, and other
-    resources.  This information is provided in the form of directory
-    records.
-
-    A directory service can be queried for the types of records it
-    supports, and for specific records matching certain criteria.
-
-    A directory service may allow support the editing, removal and
-    addition of records.
-    """
-    realmName = Attribute("The name of the authentication realm this service represents.")
-
-    def recordTypes():
-        """
-        @return: a deferred iterable of L{NamedConstant}s denoting the
-            record types that are kept in this directory.
-        """
-
-    def recordsFromQuery(expressions, operand=Operand.AND):
-        """
-        Find records matching a query consisting of an iterable of
-        expressions and an operand.
-        @param expressions: an iterable of expressions
-        @type expressions: L{object}
-        @param operand: an operand
-        @type operand: a L{NamedConstant}
-        @return: a deferred iterable of matching L{IDirectoryRecord}s.
-        @raises: L{QueryNotSupportedError} if the query is not
-            supported by this directory service.
-        """
-
-    def recordsWithFieldValue(fieldName, value):
-        """
-        Find records that have the given field name with the given
-        value.
-        @param fieldName: a field name
-        @type fieldName: L{NamedConstant}
-        @param value: a value to match
-        @type value: L{bytes}
-        @return: a deferred iterable of L{IDirectoryRecord}s.
-        """
-
-    def recordWithUID(uid):
-        """
-        Find the record that has the given UID.
-        @param uid: a UID
-        @type uid: L{bytes}
-        @return: a deferred iterable of L{IDirectoryRecord}s, or
-            C{None} if there is no such record.
-        """
-               
-    def recordWithGUID(guid):
-        """
-        Find the record that has the given GUID.
-        @param guid: a GUID
-        @type guid: L{bytes}
-        @return: a deferred iterable of L{IDirectoryRecord}s, or
-            C{None} if there is no such record.
-        """
-
-    def recordsWithRecordType(recordType):
-        """
-        Find the records that have the given record type.
-        @param recordType: a record type
-        @type recordType: L{NamedConstant}
-        @return: a deferred iterable of L{IDirectoryRecord}s.
-        """
-
-    def recordWithShortName(recordType, shortName):
-        """
-        Find the record that has the given record type and short name.
-        @param recordType: a record type
-        @type recordType: L{NamedConstant}
-        @param shortName: a short name
-        @type shortName: L{bytes}
-        @return: a deferred iterable of L{IDirectoryRecord}s, or
-            C{None} if there is no such record.
-        """
-
-    def recordsWithEmailAddress(emailAddress):
-        """
-        Find the records that have the given email address.
-        @param emailAddress: an email address
-        @type emailAddress: L{bytes}
-        @return: a deferred iterable of L{IDirectoryRecord}s, or
-            C{None} if there is no such record.
-        """
-
-    def updateRecords(records, create=False):
-        """
-        Updates existing directory records.
-        @param records: the records to update
-        @type records: iterable of L{IDirectoryRecord}s
-        @param create: if true, create records if necessary
-        @type create: boolean
-        """
-
-    def removeRecords(uids):
-        """
-        Removes the records with the given UIDs.
-        @param uids: the UIDs of the records to remove
-        @type uids: iterable of L{bytes}
-        """
-
-
-
-class IDirectoryRecord(Interface):
-    """
-    Directory record.
-
-    A directory record corresponds to a principal, and contains
-    information about the principal such as idenfiers, names and
-    passwords.
-
-    This information is stored in a set of fields (a mapping of field
-    names and values).
-
-    Some fields allow for multiple values while others allow only one
-    value.  This is discoverable by calling L{FieldName.isMultiValue}
-    on the field name.
-
-    The field L{FieldName.recordType} will be present in all directory
-    records, as all records must have a type.  Which other fields are
-    required is implementation-specific.
-
-    Principals (called group principals) may have references to other
-    principals as members.  Records representing group principals will
-    typically be records with the record type L{RecordType.group}, but
-    it is not prohibited for other record types to have members.
-
-    Fields may also be accessed as attributes.  For example:
-    C{record.recordType} is equivalent to
-    C{record.fields[FieldName.recordType]}.
-    """
-    service = Attribute("The L{IDirectoryService} this record exists in.")
-    fields  = Attribute("A mapping with L{NamedConstant} keys.")
-
-    def members():
-        """
-        Find the records that are members of this group.  Only direct
-        members are included; members of members are not expanded.
-        @return: a deferred iterable of L{IDirectoryRecord}s which are
-            direct members of this group.
-        """
-
-    def groups():
-        """
-        Find the group records that this record is a member of.  Only
-        groups for which this record is a direct member is are
-        included; membership is not expanded.
-        @return: a deferred iterable of L{IDirectoryRecord}s which are
-            groups that this record is a member of.
-        """

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/who/idirectory.py (from rev 10753, CalendarServer/trunk/twext/who/idirectory.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/who/idirectory.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/idirectory.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,360 @@
+##
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Directory service interface.
+"""
+
+__all__ = [
+    "DirectoryServiceError",
+    "DirectoryAvailabilityError",
+    "UnknownRecordTypeError",
+    "QueryNotSupportedError",
+    "NoSuchRecordError",
+    "NotAllowedError",
+
+    "RecordType",
+    "FieldName",
+    "MatchType",
+    "Operand",
+    "QueryFlags",
+    "DirectoryQueryMatchExpression",
+
+    "IDirectoryService",
+    "IDirectoryRecord",
+]
+
+from zope.interface import Attribute, Interface
+
+from twisted.python.constants import Names, NamedConstant
+from twisted.python.constants import Flags, FlagConstant
+
+
+
+##
+# Exceptions
+##
+
+class DirectoryServiceError(Exception):
+    """
+    Directory service generic error.
+    """
+
+class DirectoryAvailabilityError(DirectoryServiceError):
+    """
+    Directory not available.
+    """
+
+class UnknownRecordTypeError(DirectoryServiceError):
+    """
+    Unknown record type.
+    """
+    def __init__(self, token):
+        DirectoryServiceError.__init__(self, token)
+        self.token = token
+
+class QueryNotSupportedError(DirectoryServiceError):
+    """
+    Query not supported.
+    """
+
+class NoSuchRecordError(DirectoryServiceError):
+    """
+    Record does not exist.
+    """
+
+class NotAllowedError(DirectoryServiceError):
+    """
+    Apparently, you can't do that.
+    """
+
+
+
+##
+# Data Types
+##
+
+class RecordType(Names):
+    user  = NamedConstant()
+    group = NamedConstant()
+
+    user.description  = "user"
+    group.description = "group"
+
+
+
+class FieldName(Names):
+    """
+    Constants for common field names.
+    """
+    uid            = NamedConstant()
+    guid           = NamedConstant()
+    recordType     = NamedConstant()
+    shortNames     = NamedConstant()
+    fullNames      = NamedConstant()
+    emailAddresses = NamedConstant()
+    password       = NamedConstant()
+
+    uid.description            = "UID"
+    guid.description           = "GUID"
+    recordType.description     = "record type"
+    shortNames.description     = "short names"
+    fullNames.description      = "full names"
+    emailAddresses.description = "email addresses"
+    password.description       = "password"
+
+    shortNames.multiValue     = True
+    fullNames.multiValue      = True
+    emailAddresses.multiValue = True
+
+    @staticmethod
+    def isMultiValue(name):
+        return getattr(name, "multiValue", False)
+
+
+
+class MatchType(Names):
+    """
+    Query match types.
+    """
+    equals     = NamedConstant()
+    startsWith = NamedConstant()
+    contains   = NamedConstant()
+
+    equals.description     = "equals"
+    startsWith.description = "starts with"
+    contains.description   = "contains"
+
+
+
+class Operand(Names):
+    OR  = NamedConstant()
+    AND = NamedConstant()
+
+    OR.description  = "or"
+    AND.description = "and"
+
+
+
+class QueryFlags(Flags):
+    """
+    Query flags.
+    """
+    NOT = FlagConstant()
+    NOT.description = "not"
+
+    caseInsensitive = FlagConstant()
+    caseInsensitive.description = "case insensitive"
+
+
+
+class DirectoryQueryMatchExpression(object):
+    """
+    Query for a matching value in a given field.
+
+    @ivar fieldName: a L{NamedConstant} specifying the field
+    @ivar fieldValue: a text value to match
+    @ivar matchType: a L{NamedConstant} specifying the match algorythm
+    @ivar flags: L{NamedConstant} specifying additional options
+    """
+
+    def __init__(self, fieldName, fieldValue, matchType=MatchType.equals, flags=None):
+        self.fieldName  = fieldName
+        self.fieldValue = fieldValue
+        self.matchType  = matchType
+        self.flags      = flags
+
+    def __repr__(self):
+        def describe(constant):
+            return getattr(constant, "description", str(constant))
+
+        if self.flags is None:
+            flags = ""
+        else:
+            flags = " (%s)" % (self.flags,)
+
+        return "<%s: %r %s %r%s>" % (
+            self.__class__.__name__,
+            describe(self.fieldName),
+            describe(self.matchType),
+            describe(self.fieldValue),
+            flags
+        )
+
+
+
+##
+# Interfaces
+##
+
+class IDirectoryService(Interface):
+    """
+    Directory service.
+
+    A directory service is a service that vends information about
+    principals such as users, locations, printers, and other
+    resources.  This information is provided in the form of directory
+    records.
+
+    A directory service can be queried for the types of records it
+    supports, and for specific records matching certain criteria.
+
+    A directory service may allow support the editing, removal and
+    addition of records.
+    """
+    realmName = Attribute("The name of the authentication realm this service represents.")
+
+    def recordTypes():
+        """
+        @return: a deferred iterable of L{NamedConstant}s denoting the
+            record types that are kept in this directory.
+        """
+
+    def recordsFromQuery(expressions, operand=Operand.AND):
+        """
+        Find records matching a query consisting of an iterable of
+        expressions and an operand.
+        @param expressions: an iterable of expressions
+        @type expressions: L{object}
+        @param operand: an operand
+        @type operand: a L{NamedConstant}
+        @return: a deferred iterable of matching L{IDirectoryRecord}s.
+        @raises: L{QueryNotSupportedError} if the query is not
+            supported by this directory service.
+        """
+
+    def recordsWithFieldValue(fieldName, value):
+        """
+        Find records that have the given field name with the given
+        value.
+        @param fieldName: a field name
+        @type fieldName: L{NamedConstant}
+        @param value: a value to match
+        @type value: L{bytes}
+        @return: a deferred iterable of L{IDirectoryRecord}s.
+        """
+
+    def recordWithUID(uid):
+        """
+        Find the record that has the given UID.
+        @param uid: a UID
+        @type uid: L{bytes}
+        @return: a deferred iterable of L{IDirectoryRecord}s, or
+            C{None} if there is no such record.
+        """
+               
+    def recordWithGUID(guid):
+        """
+        Find the record that has the given GUID.
+        @param guid: a GUID
+        @type guid: L{bytes}
+        @return: a deferred iterable of L{IDirectoryRecord}s, or
+            C{None} if there is no such record.
+        """
+
+    def recordsWithRecordType(recordType):
+        """
+        Find the records that have the given record type.
+        @param recordType: a record type
+        @type recordType: L{NamedConstant}
+        @return: a deferred iterable of L{IDirectoryRecord}s.
+        """
+
+    def recordWithShortName(recordType, shortName):
+        """
+        Find the record that has the given record type and short name.
+        @param recordType: a record type
+        @type recordType: L{NamedConstant}
+        @param shortName: a short name
+        @type shortName: L{bytes}
+        @return: a deferred iterable of L{IDirectoryRecord}s, or
+            C{None} if there is no such record.
+        """
+
+    def recordsWithEmailAddress(emailAddress):
+        """
+        Find the records that have the given email address.
+        @param emailAddress: an email address
+        @type emailAddress: L{bytes}
+        @return: a deferred iterable of L{IDirectoryRecord}s, or
+            C{None} if there is no such record.
+        """
+
+    def updateRecords(records, create=False):
+        """
+        Updates existing directory records.
+        @param records: the records to update
+        @type records: iterable of L{IDirectoryRecord}s
+        @param create: if true, create records if necessary
+        @type create: boolean
+        """
+
+    def removeRecords(uids):
+        """
+        Removes the records with the given UIDs.
+        @param uids: the UIDs of the records to remove
+        @type uids: iterable of L{bytes}
+        """
+
+
+
+class IDirectoryRecord(Interface):
+    """
+    Directory record.
+
+    A directory record corresponds to a principal, and contains
+    information about the principal such as idenfiers, names and
+    passwords.
+
+    This information is stored in a set of fields (a mapping of field
+    names and values).
+
+    Some fields allow for multiple values while others allow only one
+    value.  This is discoverable by calling L{FieldName.isMultiValue}
+    on the field name.
+
+    The field L{FieldName.recordType} will be present in all directory
+    records, as all records must have a type.  Which other fields are
+    required is implementation-specific.
+
+    Principals (called group principals) may have references to other
+    principals as members.  Records representing group principals will
+    typically be records with the record type L{RecordType.group}, but
+    it is not prohibited for other record types to have members.
+
+    Fields may also be accessed as attributes.  For example:
+    C{record.recordType} is equivalent to
+    C{record.fields[FieldName.recordType]}.
+    """
+    service = Attribute("The L{IDirectoryService} this record exists in.")
+    fields  = Attribute("A mapping with L{NamedConstant} keys.")
+
+    def members():
+        """
+        Find the records that are members of this group.  Only direct
+        members are included; members of members are not expanded.
+        @return: a deferred iterable of L{IDirectoryRecord}s which are
+            direct members of this group.
+        """
+
+    def groups():
+        """
+        Find the group records that this record is a member of.  Only
+        groups for which this record is a direct member is are
+        included; membership is not expanded.
+        @return: a deferred iterable of L{IDirectoryRecord}s which are
+            groups that this record is a member of.
+        """

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twext/who/index.py
===================================================================
--- CalendarServer/trunk/twext/who/index.py	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/index.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,216 +0,0 @@
-##
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Indexed directory service implementation.
-"""
-
-__all__ = [
-    "DirectoryService",
-    "DirectoryRecord",
-]
-
-from twisted.python.constants import Names, NamedConstant
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue
-
-from twext.who.util import MergedConstants, describe, uniqueResult, iterFlags
-from twext.who.idirectory import FieldName as BaseFieldName
-from twext.who.idirectory import MatchType, QueryFlags
-from twext.who.idirectory import DirectoryQueryMatchExpression
-from twext.who.directory import DirectoryService as BaseDirectoryService
-from twext.who.directory import DirectoryRecord as BaseDirectoryRecord
-
-
-
-##
-# Data type extentions
-##
-
-class FieldName(Names):
-    memberUIDs = NamedConstant()
-    memberUIDs.description = "member UIDs"
-    memberUIDs.multiValue = True
-
-
-
-##
-# Directory Service
-##
-
-class DirectoryService(BaseDirectoryService):
-    """
-    XML directory service.
-    """
-
-    fieldName = MergedConstants(BaseDirectoryService.fieldName, FieldName)
-
-    indexedFields = (
-        BaseFieldName.recordType,
-        BaseFieldName.uid,
-        BaseFieldName.guid,
-        BaseFieldName.shortNames,
-        BaseFieldName.emailAddresses,
-        FieldName.memberUIDs,
-    )
-
-
-    def __init__(self, realmName):
-        BaseDirectoryService.__init__(self, realmName)
-
-        self.flush()
-
-
-    @property
-    def index(self):
-        self.loadRecords()
-        return self._index
-
-
-    @index.setter
-    def index(self, value):
-        self._index = value
-
-
-    def loadRecords(self):
-        """
-        Load records.
-        """
-        raise NotImplementedError("Subclasses should implement loadRecords().")
-
-
-    def flush(self):
-        """
-        Flush the index.
-        """
-        self._index = None
-
-
-    @staticmethod
-    def _queryFlags(flags):
-        predicate = lambda x: x
-        normalize = lambda x: x
-
-        if flags is not None:
-            for flag in iterFlags(flags):
-                if flag == QueryFlags.NOT:
-                    predicate = lambda x: not x
-                elif flag == QueryFlags.caseInsensitive:
-                    normalize = lambda x: x.lower()
-                else:
-                    raise NotImplementedError("Unknown query flag: %s" % (describe(flag),))
-
-        return predicate, normalize
-
-
-    def indexedRecordsFromMatchExpression(self, expression, records=None):
-        """
-        Finds records in the internal indexes matching a single
-        expression.
-        @param expression: an expression
-        @type expression: L{object}
-        """
-        predicate, normalize = self._queryFlags(expression.flags)
-
-        fieldIndex = self.index[expression.fieldName]
-        matchValue = normalize(expression.fieldValue)
-        matchType  = expression.matchType
-
-        if matchType == MatchType.startsWith:
-            indexKeys = (key for key in fieldIndex if predicate(normalize(key).startswith(matchValue)))
-        elif matchType == MatchType.contains:
-            indexKeys = (key for key in fieldIndex if predicate(matchValue in normalize(key)))
-        elif matchType == MatchType.equals:
-            if predicate(True):
-                indexKeys = (matchValue,)
-            else:
-                indexKeys = (key for key in fieldIndex if normalize(key) != matchValue)
-        else:
-            raise NotImplementedError("Unknown match type: %s" % (describe(matchType),))
-
-        matchingRecords = set()
-        for key in indexKeys:
-            matchingRecords |= fieldIndex.get(key, frozenset())
-
-        if records is not None:
-            matchingRecords &= records
-
-        return succeed(matchingRecords)
-
-
-    def unIndexedRecordsFromMatchExpression(self, expression, records=None):
-        """
-        Finds records not in the internal indexes matching a single
-        expression.
-        @param expression: an expression
-        @type expression: L{object}
-        """
-        predicate, normalize = self._queryFlags(expression.flags)
-
-        matchValue = normalize(expression.fieldValue)
-        matchType  = expression.matchType
-
-        if matchType == MatchType.startsWith:
-            match = lambda fieldValue: predicate(fieldValue.startswith(matchValue))
-        elif matchType == MatchType.contains:
-            match = lambda fieldValue: predicate(matchValue in fieldValue)
-        elif matchType == MatchType.equals:
-            match = lambda fieldValue: predicate(fieldValue == matchValue)
-        else:
-            raise NotImplementedError("Unknown match type: %s" % (describe(matchType),))
-
-        result = set()
-
-        if records is None:
-            records = (uniqueResult(values) for values in self.index[self.fieldName.uid].itervalues())
-
-        for record in records:
-            fieldValues = record.fields.get(expression.fieldName, None)
-
-            if fieldValues is None:
-                continue
-
-            for fieldValue in fieldValues:
-                if match(normalize(fieldValue)):
-                    result.add(record)
-
-        return result
-
-
-    def recordsFromExpression(self, expression, records=None):
-        if isinstance(expression, DirectoryQueryMatchExpression):
-            if expression.fieldName in self.indexedFields:
-                return self.indexedRecordsFromMatchExpression(expression, records=records)
-            else:
-                return self.unIndexedRecordsFromMatchExpression(expression, records=records)
-        else:
-            return BaseDirectoryService.recordsFromExpression(self, expression, records=records)
-
-
-
-class DirectoryRecord(BaseDirectoryRecord):
-    """
-    XML directory record
-    """
-    @inlineCallbacks
-    def members(self):
-        members = set()
-        for uid in getattr(self, "memberUIDs", ()):
-            members.add((yield self.service.recordWithUID(uid)))
-        returnValue(members)
-
-
-    def groups(self):
-        return self.service.recordsWithFieldValue(FieldName.memberUIDs, self.uid)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/who/index.py (from rev 10753, CalendarServer/trunk/twext/who/index.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/who/index.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/index.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,216 @@
+##
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Indexed directory service implementation.
+"""
+
+__all__ = [
+    "DirectoryService",
+    "DirectoryRecord",
+]
+
+from twisted.python.constants import Names, NamedConstant
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
+
+from twext.who.util import MergedConstants, describe, uniqueResult, iterFlags
+from twext.who.idirectory import FieldName as BaseFieldName
+from twext.who.idirectory import MatchType, QueryFlags
+from twext.who.idirectory import DirectoryQueryMatchExpression
+from twext.who.directory import DirectoryService as BaseDirectoryService
+from twext.who.directory import DirectoryRecord as BaseDirectoryRecord
+
+
+
+##
+# Data type extentions
+##
+
+class FieldName(Names):
+    memberUIDs = NamedConstant()
+    memberUIDs.description = "member UIDs"
+    memberUIDs.multiValue = True
+
+
+
+##
+# Directory Service
+##
+
+class DirectoryService(BaseDirectoryService):
+    """
+    XML directory service.
+    """
+
+    fieldName = MergedConstants(BaseDirectoryService.fieldName, FieldName)
+
+    indexedFields = (
+        BaseFieldName.recordType,
+        BaseFieldName.uid,
+        BaseFieldName.guid,
+        BaseFieldName.shortNames,
+        BaseFieldName.emailAddresses,
+        FieldName.memberUIDs,
+    )
+
+
+    def __init__(self, realmName):
+        BaseDirectoryService.__init__(self, realmName)
+
+        self.flush()
+
+
+    @property
+    def index(self):
+        self.loadRecords()
+        return self._index
+
+
+    @index.setter
+    def index(self, value):
+        self._index = value
+
+
+    def loadRecords(self):
+        """
+        Load records.
+        """
+        raise NotImplementedError("Subclasses should implement loadRecords().")
+
+
+    def flush(self):
+        """
+        Flush the index.
+        """
+        self._index = None
+
+
+    @staticmethod
+    def _queryFlags(flags):
+        predicate = lambda x: x
+        normalize = lambda x: x
+
+        if flags is not None:
+            for flag in iterFlags(flags):
+                if flag == QueryFlags.NOT:
+                    predicate = lambda x: not x
+                elif flag == QueryFlags.caseInsensitive:
+                    normalize = lambda x: x.lower()
+                else:
+                    raise NotImplementedError("Unknown query flag: %s" % (describe(flag),))
+
+        return predicate, normalize
+
+
+    def indexedRecordsFromMatchExpression(self, expression, records=None):
+        """
+        Finds records in the internal indexes matching a single
+        expression.
+        @param expression: an expression
+        @type expression: L{object}
+        """
+        predicate, normalize = self._queryFlags(expression.flags)
+
+        fieldIndex = self.index[expression.fieldName]
+        matchValue = normalize(expression.fieldValue)
+        matchType  = expression.matchType
+
+        if matchType == MatchType.startsWith:
+            indexKeys = (key for key in fieldIndex if predicate(normalize(key).startswith(matchValue)))
+        elif matchType == MatchType.contains:
+            indexKeys = (key for key in fieldIndex if predicate(matchValue in normalize(key)))
+        elif matchType == MatchType.equals:
+            if predicate(True):
+                indexKeys = (matchValue,)
+            else:
+                indexKeys = (key for key in fieldIndex if normalize(key) != matchValue)
+        else:
+            raise NotImplementedError("Unknown match type: %s" % (describe(matchType),))
+
+        matchingRecords = set()
+        for key in indexKeys:
+            matchingRecords |= fieldIndex.get(key, frozenset())
+
+        if records is not None:
+            matchingRecords &= records
+
+        return succeed(matchingRecords)
+
+
+    def unIndexedRecordsFromMatchExpression(self, expression, records=None):
+        """
+        Finds records not in the internal indexes matching a single
+        expression.
+        @param expression: an expression
+        @type expression: L{object}
+        """
+        predicate, normalize = self._queryFlags(expression.flags)
+
+        matchValue = normalize(expression.fieldValue)
+        matchType  = expression.matchType
+
+        if matchType == MatchType.startsWith:
+            match = lambda fieldValue: predicate(fieldValue.startswith(matchValue))
+        elif matchType == MatchType.contains:
+            match = lambda fieldValue: predicate(matchValue in fieldValue)
+        elif matchType == MatchType.equals:
+            match = lambda fieldValue: predicate(fieldValue == matchValue)
+        else:
+            raise NotImplementedError("Unknown match type: %s" % (describe(matchType),))
+
+        result = set()
+
+        if records is None:
+            records = (uniqueResult(values) for values in self.index[self.fieldName.uid].itervalues())
+
+        for record in records:
+            fieldValues = record.fields.get(expression.fieldName, None)
+
+            if fieldValues is None:
+                continue
+
+            for fieldValue in fieldValues:
+                if match(normalize(fieldValue)):
+                    result.add(record)
+
+        return result
+
+
+    def recordsFromExpression(self, expression, records=None):
+        if isinstance(expression, DirectoryQueryMatchExpression):
+            if expression.fieldName in self.indexedFields:
+                return self.indexedRecordsFromMatchExpression(expression, records=records)
+            else:
+                return self.unIndexedRecordsFromMatchExpression(expression, records=records)
+        else:
+            return BaseDirectoryService.recordsFromExpression(self, expression, records=records)
+
+
+
+class DirectoryRecord(BaseDirectoryRecord):
+    """
+    XML directory record
+    """
+    @inlineCallbacks
+    def members(self):
+        members = set()
+        for uid in getattr(self, "memberUIDs", ()):
+            members.add((yield self.service.recordWithUID(uid)))
+        returnValue(members)
+
+
+    def groups(self):
+        return self.service.recordsWithFieldValue(FieldName.memberUIDs, self.uid)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/__init__.py
===================================================================
--- CalendarServer/trunk/twext/who/test/__init__.py	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Directory service integration tests
-"""

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/__init__.py (from rev 10753, CalendarServer/trunk/twext/who/test/__init__.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Directory service integration tests
+"""

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_directory.py
===================================================================
--- CalendarServer/trunk/twext/who/test/test_directory.py	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_directory.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,242 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Generic directory service base implementation tests
-"""
-
-from zope.interface.verify import verifyObject, BrokenMethodImplementation
-
-from twisted.trial import unittest
-from twisted.trial.unittest import SkipTest
-from twisted.internet.defer import inlineCallbacks
-
-from twext.who.idirectory import QueryNotSupportedError
-from twext.who.idirectory import RecordType, FieldName
-from twext.who.idirectory import IDirectoryService, IDirectoryRecord
-from twext.who.directory import DirectoryService, DirectoryRecord
-
-
-
-class BaseTest(unittest.TestCase):
-    realmName = "xyzzy"
-
-
-    def _testService(self):
-        if not hasattr(self, "_service"):
-            self._service = DirectoryService(self.realmName)
-        return self._service
-
-
-
-class DirectoryServiceTest(BaseTest):
-    def test_interface(self):
-        service = self._testService()
-        try:
-            verifyObject(IDirectoryService, service)
-        except BrokenMethodImplementation as e:
-            self.fail(e)
-
-
-    def test_init(self):
-        service = self._testService()
-        self.assertEquals(service.realmName, self.realmName)
-
-
-    def test_repr(self):
-        service = self._testService()
-        self.assertEquals(repr(service), "<DirectoryService 'xyzzy'>")
-
-
-    @inlineCallbacks
-    def test_recordTypes(self):
-        service = self._testService()
-        self.assertEquals(
-            set((yield service.recordTypes())),
-            set(service.recordType.iterconstants())
-        )
-
-
-    @inlineCallbacks
-    def test_recordsFromQueryNone(self):
-        service = self._testService()
-        records = (yield service.recordsFromQuery(()))
-        for record in records:
-            self.failTest("No records expected")
-
-
-    def test_recordsFromQueryBogus(self):
-        service = self._testService()
-        self.assertFailure(service.recordsFromQuery((object(),)), QueryNotSupportedError)
-
-
-    def test_recordWithUID(self):
-        raise SkipTest("Subclasses should implement this test.")
-
-    def test_recordWithGUID(self):
-        raise SkipTest("Subclasses should implement this test.")
-
-    def test_recordsWithRecordType(self):
-        raise SkipTest("Subclasses should implement this test.")
-
-    def test_recordWithShortName(self):
-        raise SkipTest("Subclasses should implement this test.")
-
-    def test_recordsWithEmailAddress(self):
-        raise SkipTest("Subclasses should implement this test.")
-
-
-
-class DirectoryRecordTest(BaseTest):
-    fields_wsanchez = {
-        FieldName.uid:            "UID:wsanchez",
-        FieldName.recordType:     RecordType.user,
-        FieldName.shortNames:     ("wsanchez", "wilfredo_sanchez"),
-        FieldName.fullNames:      ("Wilfredo Sanchez", "Wilfredo Sanchez Vega"),
-        FieldName.emailAddresses: ("wsanchez at calendarserver.org", "wsanchez at example.com")
-    }
-
-    fields_glyph = {
-        FieldName.uid:            "UID:glyph",
-        FieldName.recordType:     RecordType.user,
-        FieldName.shortNames:     ("glyph",),
-        FieldName.fullNames:      ("Glyph Lefkowitz",),
-        FieldName.emailAddresses: ("glyph at calendarserver.org",)
-    }
-
-    fields_sagen = {
-        FieldName.uid:            "UID:sagen",
-        FieldName.recordType:     RecordType.user,
-        FieldName.shortNames:     ("sagen",),
-        FieldName.fullNames:      ("Morgen Sagen",),
-        FieldName.emailAddresses: ("sagen at CalendarServer.org",)
-    }
-
-
-    def _testRecord(self, fields=None, service=None):
-        if fields is None:
-            fields = self.fields_wsanchez
-        if service is None:
-            service = self._testService()
-        return DirectoryRecord(service, fields)
-
-
-    def test_interface(self):
-        record = self._testRecord()
-        try:
-            verifyObject(IDirectoryRecord, record)
-        except BrokenMethodImplementation as e:
-            self.fail(e)
-
-
-    def test_init(self):
-        service  = self._testService()
-        wsanchez = self._testRecord(self.fields_wsanchez, service=service)
-
-        self.assertEquals(wsanchez.service, service)
-        self.assertEquals(wsanchez.fields , self.fields_wsanchez)
-
-
-    def test_initWithNoUID(self):
-        fields = self.fields_wsanchez.copy()
-        del fields[FieldName.uid]
-        self.assertRaises(ValueError, self._testRecord, fields)
-
-        fields = self.fields_wsanchez.copy()
-        fields[FieldName.uid] = ""
-        self.assertRaises(ValueError, self._testRecord, fields)
-
-
-    def test_initWithNoRecordType(self):
-        fields = self.fields_wsanchez.copy()
-        del fields[FieldName.recordType]
-        self.assertRaises(ValueError, self._testRecord, fields)
-
-        fields = self.fields_wsanchez.copy()
-        fields[FieldName.recordType] = ""
-        self.assertRaises(ValueError, self._testRecord, fields)
-
-
-    def test_initWithNoShortNames(self):
-        fields = self.fields_wsanchez.copy()
-        del fields[FieldName.shortNames]
-        self.assertRaises(ValueError, self._testRecord, fields)
-
-        fields = self.fields_wsanchez.copy()
-        fields[FieldName.shortNames] = ()
-        self.assertRaises(ValueError, self._testRecord, fields)
-
-        fields = self.fields_wsanchez.copy()
-        fields[FieldName.shortNames] = ("",)
-        self.assertRaises(ValueError, self._testRecord, fields)
-
-        fields = self.fields_wsanchez.copy()
-        fields[FieldName.shortNames] = ("wsanchez", "")
-        self.assertRaises(ValueError, self._testRecord, fields)
-
-
-    def test_initWithBogusRecordType(self):
-        fields = self.fields_wsanchez.copy()
-        fields[FieldName.recordType] = object()
-        self.assertRaises(ValueError, self._testRecord, fields)
-
-
-    def test_initNormalize(self):
-        sagen = self._testRecord(self.fields_sagen)
-
-        self.assertEquals(
-            sagen.fields[FieldName.emailAddresses],
-            ("sagen at calendarserver.org",)
-        )
-
-
-    def test_compare(self):
-        fields_glyphmod = self.fields_glyph.copy()
-        del fields_glyphmod[FieldName.emailAddresses]
-
-        wsanchez    = self._testRecord(self.fields_wsanchez)
-        wsanchezmod = self._testRecord(self.fields_wsanchez, DirectoryService("plugh"))
-        glyph       = self._testRecord(self.fields_glyph)
-        glyphmod    = self._testRecord(fields_glyphmod)
-
-        self.assertEquals(wsanchez, wsanchez)
-        self.assertNotEqual(wsanchez, glyph)
-        self.assertEquals(glyph, glyphmod) # UID matches
-        self.assertNotEqual(glyphmod, wsanchez)
-        self.assertNotEqual(wsanchez, wsanchezmod) # Different service
-
-
-    def test_attributeAccess(self):
-        wsanchez = self._testRecord(self.fields_wsanchez)
-
-        self.assertEquals(wsanchez.recordType    , wsanchez.fields[FieldName.recordType    ])
-        self.assertEquals(wsanchez.uid           , wsanchez.fields[FieldName.uid           ])
-        self.assertEquals(wsanchez.shortNames    , wsanchez.fields[FieldName.shortNames    ])
-        self.assertEquals(wsanchez.emailAddresses, wsanchez.fields[FieldName.emailAddresses])
-
-    @inlineCallbacks
-    def test_members(self):
-        wsanchez = self._testRecord(self.fields_wsanchez)
-
-        self.assertEquals(
-            set((yield wsanchez.members())),
-            set()
-        )
-
-        raise SkipTest("Subclasses should implement this test.")
-
-    def test_groups(self):
-        raise SkipTest("Subclasses should implement this test.")

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_directory.py (from rev 10753, CalendarServer/trunk/twext/who/test/test_directory.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_directory.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_directory.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,242 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Generic directory service base implementation tests
+"""
+
+from zope.interface.verify import verifyObject, BrokenMethodImplementation
+
+from twisted.trial import unittest
+from twisted.trial.unittest import SkipTest
+from twisted.internet.defer import inlineCallbacks
+
+from twext.who.idirectory import QueryNotSupportedError
+from twext.who.idirectory import RecordType, FieldName
+from twext.who.idirectory import IDirectoryService, IDirectoryRecord
+from twext.who.directory import DirectoryService, DirectoryRecord
+
+
+
+class BaseTest(unittest.TestCase):
+    realmName = "xyzzy"
+
+
+    def _testService(self):
+        if not hasattr(self, "_service"):
+            self._service = DirectoryService(self.realmName)
+        return self._service
+
+
+
+class DirectoryServiceTest(BaseTest):
+    def test_interface(self):
+        service = self._testService()
+        try:
+            verifyObject(IDirectoryService, service)
+        except BrokenMethodImplementation as e:
+            self.fail(e)
+
+
+    def test_init(self):
+        service = self._testService()
+        self.assertEquals(service.realmName, self.realmName)
+
+
+    def test_repr(self):
+        service = self._testService()
+        self.assertEquals(repr(service), "<DirectoryService 'xyzzy'>")
+
+
+    @inlineCallbacks
+    def test_recordTypes(self):
+        service = self._testService()
+        self.assertEquals(
+            set((yield service.recordTypes())),
+            set(service.recordType.iterconstants())
+        )
+
+
+    @inlineCallbacks
+    def test_recordsFromQueryNone(self):
+        service = self._testService()
+        records = (yield service.recordsFromQuery(()))
+        for record in records:
+            self.failTest("No records expected")
+
+
+    def test_recordsFromQueryBogus(self):
+        service = self._testService()
+        self.assertFailure(service.recordsFromQuery((object(),)), QueryNotSupportedError)
+
+
+    def test_recordWithUID(self):
+        raise SkipTest("Subclasses should implement this test.")
+
+    def test_recordWithGUID(self):
+        raise SkipTest("Subclasses should implement this test.")
+
+    def test_recordsWithRecordType(self):
+        raise SkipTest("Subclasses should implement this test.")
+
+    def test_recordWithShortName(self):
+        raise SkipTest("Subclasses should implement this test.")
+
+    def test_recordsWithEmailAddress(self):
+        raise SkipTest("Subclasses should implement this test.")
+
+
+
+class DirectoryRecordTest(BaseTest):
+    fields_wsanchez = {
+        FieldName.uid:            "UID:wsanchez",
+        FieldName.recordType:     RecordType.user,
+        FieldName.shortNames:     ("wsanchez", "wilfredo_sanchez"),
+        FieldName.fullNames:      ("Wilfredo Sanchez", "Wilfredo Sanchez Vega"),
+        FieldName.emailAddresses: ("wsanchez at calendarserver.org", "wsanchez at example.com")
+    }
+
+    fields_glyph = {
+        FieldName.uid:            "UID:glyph",
+        FieldName.recordType:     RecordType.user,
+        FieldName.shortNames:     ("glyph",),
+        FieldName.fullNames:      ("Glyph Lefkowitz",),
+        FieldName.emailAddresses: ("glyph at calendarserver.org",)
+    }
+
+    fields_sagen = {
+        FieldName.uid:            "UID:sagen",
+        FieldName.recordType:     RecordType.user,
+        FieldName.shortNames:     ("sagen",),
+        FieldName.fullNames:      ("Morgen Sagen",),
+        FieldName.emailAddresses: ("sagen at CalendarServer.org",)
+    }
+
+
+    def _testRecord(self, fields=None, service=None):
+        if fields is None:
+            fields = self.fields_wsanchez
+        if service is None:
+            service = self._testService()
+        return DirectoryRecord(service, fields)
+
+
+    def test_interface(self):
+        record = self._testRecord()
+        try:
+            verifyObject(IDirectoryRecord, record)
+        except BrokenMethodImplementation as e:
+            self.fail(e)
+
+
+    def test_init(self):
+        service  = self._testService()
+        wsanchez = self._testRecord(self.fields_wsanchez, service=service)
+
+        self.assertEquals(wsanchez.service, service)
+        self.assertEquals(wsanchez.fields , self.fields_wsanchez)
+
+
+    def test_initWithNoUID(self):
+        fields = self.fields_wsanchez.copy()
+        del fields[FieldName.uid]
+        self.assertRaises(ValueError, self._testRecord, fields)
+
+        fields = self.fields_wsanchez.copy()
+        fields[FieldName.uid] = ""
+        self.assertRaises(ValueError, self._testRecord, fields)
+
+
+    def test_initWithNoRecordType(self):
+        fields = self.fields_wsanchez.copy()
+        del fields[FieldName.recordType]
+        self.assertRaises(ValueError, self._testRecord, fields)
+
+        fields = self.fields_wsanchez.copy()
+        fields[FieldName.recordType] = ""
+        self.assertRaises(ValueError, self._testRecord, fields)
+
+
+    def test_initWithNoShortNames(self):
+        fields = self.fields_wsanchez.copy()
+        del fields[FieldName.shortNames]
+        self.assertRaises(ValueError, self._testRecord, fields)
+
+        fields = self.fields_wsanchez.copy()
+        fields[FieldName.shortNames] = ()
+        self.assertRaises(ValueError, self._testRecord, fields)
+
+        fields = self.fields_wsanchez.copy()
+        fields[FieldName.shortNames] = ("",)
+        self.assertRaises(ValueError, self._testRecord, fields)
+
+        fields = self.fields_wsanchez.copy()
+        fields[FieldName.shortNames] = ("wsanchez", "")
+        self.assertRaises(ValueError, self._testRecord, fields)
+
+
+    def test_initWithBogusRecordType(self):
+        fields = self.fields_wsanchez.copy()
+        fields[FieldName.recordType] = object()
+        self.assertRaises(ValueError, self._testRecord, fields)
+
+
+    def test_initNormalize(self):
+        sagen = self._testRecord(self.fields_sagen)
+
+        self.assertEquals(
+            sagen.fields[FieldName.emailAddresses],
+            ("sagen at calendarserver.org",)
+        )
+
+
+    def test_compare(self):
+        fields_glyphmod = self.fields_glyph.copy()
+        del fields_glyphmod[FieldName.emailAddresses]
+
+        wsanchez    = self._testRecord(self.fields_wsanchez)
+        wsanchezmod = self._testRecord(self.fields_wsanchez, DirectoryService("plugh"))
+        glyph       = self._testRecord(self.fields_glyph)
+        glyphmod    = self._testRecord(fields_glyphmod)
+
+        self.assertEquals(wsanchez, wsanchez)
+        self.assertNotEqual(wsanchez, glyph)
+        self.assertNotEqual(glyph, glyphmod) # UID matches, other fields do not
+        self.assertNotEqual(glyphmod, wsanchez)
+        self.assertNotEqual(wsanchez, wsanchezmod) # Different service
+
+
+    def test_attributeAccess(self):
+        wsanchez = self._testRecord(self.fields_wsanchez)
+
+        self.assertEquals(wsanchez.recordType    , wsanchez.fields[FieldName.recordType    ])
+        self.assertEquals(wsanchez.uid           , wsanchez.fields[FieldName.uid           ])
+        self.assertEquals(wsanchez.shortNames    , wsanchez.fields[FieldName.shortNames    ])
+        self.assertEquals(wsanchez.emailAddresses, wsanchez.fields[FieldName.emailAddresses])
+
+    @inlineCallbacks
+    def test_members(self):
+        wsanchez = self._testRecord(self.fields_wsanchez)
+
+        self.assertEquals(
+            set((yield wsanchez.members())),
+            set()
+        )
+
+        raise SkipTest("Subclasses should implement this test.")
+
+    def test_groups(self):
+        raise SkipTest("Subclasses should implement this test.")

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_xml.py
===================================================================
--- CalendarServer/trunk/twext/who/test/test_xml.py	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_xml.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,819 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-XML directory service tests
-"""
-
-from time import sleep
-
-from twisted.python.filepath import FilePath
-from twisted.internet.defer import inlineCallbacks
-
-from twext.who.idirectory import NoSuchRecordError
-from twext.who.idirectory import DirectoryQueryMatchExpression
-from twext.who.idirectory import Operand, MatchType, QueryFlags
-from twext.who.xml import ParseError
-from twext.who.xml import DirectoryService, DirectoryRecord
-
-from twext.who.test import test_directory
-
-
-
-xmlRealmName = "Test Realm"
-
-testXMLConfig = """<?xml version="1.0" encoding="utf-8"?>
-
-<directory realm="xyzzy">
-
-  <record type="user">
-    <uid>__wsanchez__</uid>
-    <short-name>wsanchez</short-name>
-    <short-name>wilfredo_sanchez</short-name>
-    <full-name>Wilfredo Sanchez</full-name>
-    <password>zehcnasw</password>
-    <email>wsanchez at bitbucket.calendarserver.org</email>
-    <email>wsanchez at devnull.twistedmatrix.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__glyph__</uid>
-    <short-name>glyph</short-name>
-    <full-name>Glyph Lefkowitz</full-name>
-    <password>hpylg</password>
-    <email>glyph at bitbucket.calendarserver.org</email>
-    <email>glyph at devnull.twistedmatrix.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__sagen__</uid>
-    <short-name>sagen</short-name>
-    <full-name>Morgen Sagen</full-name>
-    <password>negas</password>
-    <email>sagen at bitbucket.calendarserver.org</email>
-    <email>shared at example.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__cdaboo__</uid>
-    <short-name>cdaboo</short-name>
-    <full-name>Cyrus Daboo</full-name>
-    <password>suryc</password>
-    <email>cdaboo at bitbucket.calendarserver.org</email>
-  </record>
-
-  <record type="user">
-    <uid>__dre__</uid>
-    <short-name>dre</short-name>
-    <full-name>Andre LaBranche</full-name>
-    <password>erd</password>
-    <email>dre at bitbucket.calendarserver.org</email>
-    <email>shared at example.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__exarkun__</uid>
-    <short-name>exarkun</short-name>
-    <full-name>Jean-Paul Calderone</full-name>
-    <password>nucraxe</password>
-    <email>exarkun at devnull.twistedmatrix.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__dreid__</uid>
-    <short-name>dreid</short-name>
-    <full-name>David Reid</full-name>
-    <password>dierd</password>
-    <email>dreid at devnull.twistedmatrix.com</email>
-  </record>
-
-  <record> <!-- type defaults to "user" -->
-    <uid>__joe__</uid>
-    <short-name>joe</short-name>
-    <full-name>Joe Schmoe</full-name>
-    <password>eoj</password>
-    <email>joe at example.com</email>
-  </record>
-
-  <record>
-    <uid>__alyssa__</uid>
-    <short-name>alyssa</short-name>
-    <full-name>Alyssa P. Hacker</full-name>
-    <password>assyla</password>
-    <email>alyssa at example.com</email>
-  </record>
-
-  <record type="group">
-    <uid>__calendar-dev__</uid>
-    <short-name>calendar-dev</short-name>
-    <full-name>Calendar Server developers</full-name>
-    <email>dev at bitbucket.calendarserver.org</email>
-    <member-uid>__wsanchez__</member-uid>
-    <member-uid>__glyph__</member-uid>
-    <member-uid>__sagen__</member-uid>
-    <member-uid>__cdaboo__</member-uid>
-    <member-uid>__dre__</member-uid>
-  </record>
-
-  <record type="group">
-    <uid>__twisted__</uid>
-    <short-name>twisted</short-name>
-    <full-name>Twisted Matrix Laboratories</full-name>
-    <email>hack at devnull.twistedmatrix.com</email>
-    <member-uid>__wsanchez__</member-uid>
-    <member-uid>__glyph__</member-uid>
-    <member-uid>__exarkun__</member-uid>
-    <member-uid>__dreid__</member-uid>
-    <member-uid>__dre__</member-uid>
-  </record>
-
-  <record type="group">
-    <uid>__developers__</uid>
-    <short-name>developers</short-name>
-    <full-name>All Developers</full-name>
-    <member-uid>__calendar-dev__</member-uid>
-    <member-uid>__twisted__</member-uid>
-    <member-uid>__alyssa__</member-uid>
-  </record>
-
-</directory>
-"""
-
-
-
-class BaseTest(object):
-    def _testService(self, xmlData=None):
-        if xmlData is None:
-            xmlData = testXMLConfig
-
-        filePath = FilePath(self.mktemp())
-        filePath.setContent(xmlData)
-
-        class TestService(DirectoryService):
-            def query(self, field, value, matchType=MatchType.equals, flags=None):
-                name = getattr(self.fieldName, field)
-                assert name is not None
-                return DirectoryQueryMatchExpression(
-                    name, value,
-                    matchType = matchType,
-                    flags = flags,
-                )
-
-        return TestService(filePath)
-
-
-
-class DirectoryServiceTest(BaseTest, test_directory.DirectoryServiceTest):
-    def test_repr(self):
-        service = self._testService()
-
-        self.assertEquals(repr(service), "<TestService (not loaded)>")
-        service.loadRecords()
-        self.assertEquals(repr(service), "<TestService 'xyzzy'>")
-
-
-    def assertRecords(self, records, uids):
-        self.assertEquals(
-            frozenset((record.uid for record in records)),
-            frozenset((uids)),
-        )
-
-
-    def test_realmNameImmutable(self):
-        def setRealmName():
-            service = self._testService()
-            service.realmName = "foo"
-
-        self.assertRaises(AssertionError, setRealmName)
-
-
-    def test_reloadInterval(self):
-        service = self._testService()
-
-        service.loadRecords(stat=False)
-        lastRefresh = service._lastRefresh
-        self.assertTrue(service._lastRefresh)
-
-        sleep(1)
-        service.loadRecords(stat=False)
-        self.assertEquals(lastRefresh, service._lastRefresh)
-
-
-    def test_reloadStat(self):
-        service = self._testService()
-
-        service.loadRecords(loadNow=True)
-        lastRefresh = service._lastRefresh
-        self.assertTrue(service._lastRefresh)
-
-        sleep(1)
-        service.loadRecords(loadNow=True)
-        self.assertEquals(lastRefresh, service._lastRefresh)
-
-
-    def test_badXML(self):
-        service = self._testService(xmlData="Hello")
-
-        self.assertRaises(ParseError, service.loadRecords)
-
-
-    def test_badRootElement(self):
-        service = self._testService(xmlData=
-"""<?xml version="1.0" encoding="utf-8"?>
-
-<frobnitz />
-"""
-        )
-
-        self.assertRaises(ParseError, service.loadRecords)
-        try:
-            service.loadRecords()
-        except ParseError as e:
-            self.assertTrue(str(e).startswith("Incorrect root element"), e)
-        else:
-            raise AssertionError
-
-
-    def test_noRealmName(self):
-        service = self._testService(xmlData=
-"""<?xml version="1.0" encoding="utf-8"?>
-
-<directory />
-"""
-        )
-
-        self.assertRaises(ParseError, service.loadRecords)
-        try:
-            service.loadRecords()
-        except ParseError as e:
-            self.assertTrue(str(e).startswith("No realm name"), e)
-        else:
-            raise AssertionError
-
-
-    @inlineCallbacks
-    def test_recordWithUID(self):
-        service = self._testService()
-
-        record = (yield service.recordWithUID("__null__"))
-        self.assertEquals(record, None)
-
-        record = (yield service.recordWithUID("__wsanchez__"))
-        self.assertEquals(record.uid, "__wsanchez__")
-
-
-    @inlineCallbacks
-    def test_recordWithGUID(self):
-        service = self._testService()
-        record = (yield service.recordWithGUID("6C495FCD-7E78-4D5C-AA66-BC890AD04C9D"))
-        self.assertEquals(record, None)
-
-    @inlineCallbacks
-    def test_recordsWithRecordType(self):
-        service = self._testService()
-
-        records = (yield service.recordsWithRecordType(object()))
-        self.assertEquals(set(records), set())
-
-        records = (yield service.recordsWithRecordType(service.recordType.user))
-        self.assertRecords(records,
-            (
-                "__wsanchez__",
-                "__glyph__",
-                "__sagen__",
-                "__cdaboo__",
-                "__dre__",
-                "__exarkun__",
-                "__dreid__",
-                "__alyssa__",
-                "__joe__",
-            ),
-        )
-
-        records = (yield service.recordsWithRecordType(service.recordType.group))
-        self.assertRecords(records,
-            (
-                "__calendar-dev__",
-                "__twisted__",
-                "__developers__",
-            ),
-        )
-
-
-    @inlineCallbacks
-    def test_recordWithShortName(self):
-        service = self._testService()
-
-        record = (yield service.recordWithShortName(service.recordType.user, "null"))
-        self.assertEquals(record, None)
-
-        record = (yield service.recordWithShortName(service.recordType.user, "wsanchez"))
-        self.assertEquals(record.uid, "__wsanchez__")
-
-        record = (yield service.recordWithShortName(service.recordType.user, "wilfredo_sanchez"))
-        self.assertEquals(record.uid, "__wsanchez__")
-
-
-    @inlineCallbacks
-    def test_recordsWithEmailAddress(self):
-        service = self._testService()
-
-        records = (yield service.recordsWithEmailAddress("wsanchez at bitbucket.calendarserver.org"))
-        self.assertRecords(records, ("__wsanchez__",))
-
-        records = (yield service.recordsWithEmailAddress("wsanchez at devnull.twistedmatrix.com"))
-        self.assertRecords(records, ("__wsanchez__",))
-
-        records = (yield service.recordsWithEmailAddress("shared at example.com"))
-        self.assertRecords(records, ("__sagen__", "__dre__"))
-
-
-    @inlineCallbacks
-    def test_queryAnd(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery(
-            (
-                service.query("emailAddresses", "shared at example.com"),
-                service.query("shortNames", "sagen"),
-            ),
-            operand=Operand.AND
-        )
-        self.assertRecords(records, ("__sagen__",))
-
-
-    @inlineCallbacks
-    def test_queryOr(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery(
-            (
-                service.query("emailAddresses", "shared at example.com"),
-                service.query("shortNames", "wsanchez"),
-            ),
-            operand=Operand.OR
-        )
-        self.assertRecords(records, ("__sagen__", "__dre__", "__wsanchez__"))
-
-
-    @inlineCallbacks
-    def test_queryNot(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery(
-            (
-                service.query("emailAddresses", "shared at example.com"),
-                service.query("shortNames", "sagen", flags=QueryFlags.NOT),
-            ),
-            operand=Operand.AND
-        )
-        self.assertRecords(records, ("__dre__",))
-
-
-    @inlineCallbacks
-    def test_queryNotNoIndex(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery(
-            (
-                service.query("emailAddresses", "shared at example.com"),
-                service.query("fullNames", "Andre LaBranche", flags=QueryFlags.NOT),
-            ),
-            operand=Operand.AND
-        )
-        self.assertRecords(records, ("__sagen__",))
-
-
-    @inlineCallbacks
-    def test_queryCaseInsensitive(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query("shortNames", "SagEn", flags=QueryFlags.caseInsensitive),
-        ))
-        self.assertRecords(records, ("__sagen__",))
-
-
-    @inlineCallbacks
-    def test_queryCaseInsensitiveNoIndex(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query("fullNames", "moRGen SAGen", flags=QueryFlags.caseInsensitive),
-        ))
-        self.assertRecords(records, ("__sagen__",))
-
-
-    @inlineCallbacks
-    def test_queryStartsWith(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query("shortNames", "wil", matchType=MatchType.startsWith),
-        ))
-        self.assertRecords(records, ("__wsanchez__",))
-
-
-    @inlineCallbacks
-    def test_queryStartsWithNoIndex(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query("fullNames", "Wilfredo", matchType=MatchType.startsWith),
-        ))
-        self.assertRecords(records, ("__wsanchez__",))
-
-
-    @inlineCallbacks
-    def test_queryStartsWithNot(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query(
-                "shortNames", "w",
-                matchType = MatchType.startsWith,
-                flags = QueryFlags.NOT,
-            ),
-        ))
-        self.assertRecords(
-            records,
-            (
-                '__alyssa__',
-                '__calendar-dev__',
-                '__cdaboo__',
-                '__developers__',
-                '__dre__',
-                '__dreid__',
-                '__exarkun__',
-                '__glyph__',
-                '__joe__',
-                '__sagen__',
-                '__twisted__',
-            ),
-        )
-
-
-    @inlineCallbacks
-    def test_queryStartsWithNotAny(self):
-        """
-        FIXME?: In the this case, the record __wsanchez__ has two
-        shortNames, and one doesn't match the query.  Should it be
-        included or not?  It is, because one matches the query, but
-        should NOT require that all match?
-        """
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query(
-                "shortNames", "wil",
-                matchType = MatchType.startsWith,
-                flags = QueryFlags.NOT,
-            ),
-        ))
-        self.assertRecords(
-            records,
-            (
-                '__alyssa__',
-                '__calendar-dev__',
-                '__cdaboo__',
-                '__developers__',
-                '__dre__',
-                '__dreid__',
-                '__exarkun__',
-                '__glyph__',
-                '__joe__',
-                '__sagen__',
-                '__twisted__',
-                '__wsanchez__',
-            ),
-        )
-
-
-    @inlineCallbacks
-    def test_queryStartsWithNotNoIndex(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query(
-                "fullNames", "Wilfredo",
-                matchType = MatchType.startsWith,
-                flags = QueryFlags.NOT,
-            ),
-        ))
-        self.assertRecords(
-            records,
-            (
-                '__alyssa__',
-                '__calendar-dev__',
-                '__cdaboo__',
-                '__developers__',
-                '__dre__',
-                '__dreid__',
-                '__exarkun__',
-                '__glyph__',
-                '__joe__',
-                '__sagen__',
-                '__twisted__',
-            ),
-        )
-
-
-    @inlineCallbacks
-    def test_queryStartsWithCaseInsensitive(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query(
-                "shortNames", "WIL",
-                matchType = MatchType.startsWith,
-                flags = QueryFlags.caseInsensitive,
-            ),
-        ))
-        self.assertRecords(records, ("__wsanchez__",))
-
-
-    @inlineCallbacks
-    def test_queryStartsWithCaseInsensitiveNoIndex(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query(
-                "fullNames", "wilfrEdo",
-                matchType = MatchType.startsWith,
-                flags = QueryFlags.caseInsensitive,
-            ),
-        ))
-        self.assertRecords(records, ("__wsanchez__",))
-
-
-    @inlineCallbacks
-    def test_queryContains(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query("shortNames", "sanchez", matchType=MatchType.contains),
-        ))
-        self.assertRecords(records, ("__wsanchez__",))
-
-
-    @inlineCallbacks
-    def test_queryContainsNoIndex(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query("fullNames", "fred", matchType=MatchType.contains),
-        ))
-        self.assertRecords(records, ("__wsanchez__",))
-
-
-    @inlineCallbacks
-    def test_queryContainsNot(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query(
-                "shortNames", "sanchez",
-                matchType = MatchType.contains,
-                flags = QueryFlags.NOT,
-            ),
-        ))
-        self.assertRecords(
-            records,
-            (
-                '__alyssa__',
-                '__calendar-dev__',
-                '__cdaboo__',
-                '__developers__',
-                '__dre__',
-                '__dreid__',
-                '__exarkun__',
-                '__glyph__',
-                '__joe__',
-                '__sagen__',
-                '__twisted__',
-            ),
-        )
-
-
-    @inlineCallbacks
-    def test_queryContainsNotNoIndex(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query(
-                "fullNames", "fred",
-                matchType = MatchType.contains,
-                flags = QueryFlags.NOT,
-            ),
-        ))
-        self.assertRecords(
-            records,
-            (
-                '__alyssa__',
-                '__calendar-dev__',
-                '__cdaboo__',
-                '__developers__',
-                '__dre__',
-                '__dreid__',
-                '__exarkun__',
-                '__glyph__',
-                '__joe__',
-                '__sagen__',
-                '__twisted__',
-            ),
-        )
-
-
-    @inlineCallbacks
-    def test_queryContainsCaseInsensitive(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query(
-                "shortNames", "Sanchez",
-                matchType=MatchType.contains,
-                flags=QueryFlags.caseInsensitive,
-            ),
-        ))
-        self.assertRecords(records, ("__wsanchez__",))
-
-
-    @inlineCallbacks
-    def test_queryContainsCaseInsensitiveNoIndex(self):
-        service = self._testService()
-        records = yield service.recordsFromQuery((
-            service.query(
-                "fullNames", "frEdo",
-                matchType=MatchType.contains,
-                flags=QueryFlags.caseInsensitive,
-            ),
-        ))
-        self.assertRecords(records, ("__wsanchez__",))
-
-
-    def test_unknownRecordTypesClean(self):
-        service = self._testService()
-        self.assertEquals(set(service.unknownRecordTypes), set())
-
-
-    def test_unknownRecordTypesDirty(self):
-        service = self._testService(xmlData=
-"""<?xml version="1.0" encoding="utf-8"?>
-
-<directory realm="Unknown Record Types">
-  <record type="camera">
-    <uid>__d600__</uid>
-    <short-name>d600</short-name>
-    <full-name>Nikon D600</full-name>
-  </record>
-</directory>
-"""
-        )
-        self.assertEquals(set(service.unknownRecordTypes), set(("camera",)))
-
-
-    def test_unknownFieldElementsClean(self):
-        service = self._testService()
-        self.assertEquals(set(service.unknownFieldElements), set())
-
-
-    def test_unknownFieldElementsDirty(self):
-        service = self._testService(xmlData=
-"""<?xml version="1.0" encoding="utf-8"?>
-
-<directory realm="Unknown Record Types">
-  <record type="user">
-    <uid>__wsanchez__</uid>
-    <short-name>wsanchez</short-name>
-    <political-affiliation>Community and Freedom Party</political-affiliation>
-  </record>
-</directory>
-"""
-        )
-        self.assertEquals(set(service.unknownFieldElements), set(("political-affiliation",)))
-
-
-    @inlineCallbacks
-    def test_updateRecord(self):
-        service = self._testService()
-
-        record = (yield service.recordWithUID("__wsanchez__"))
-
-        fields = record.fields.copy()
-        fields[service.fieldName.fullNames] = ["Wilfredo Sanchez Vega"]
-
-        updatedRecord = DirectoryRecord(service, fields)
-        yield service.updateRecords((updatedRecord,))
-
-        # Verify change is present immediately
-        record = (yield service.recordWithUID("__wsanchez__"))
-        self.assertEquals(set(record.fullNames), set(("Wilfredo Sanchez Vega",)))
-
-        # Verify change is persisted
-        service.flush()
-        record = (yield service.recordWithUID("__wsanchez__"))
-        self.assertEquals(set(record.fullNames), set(("Wilfredo Sanchez Vega",)))
-
-
-    @inlineCallbacks
-    def test_addRecord(self):
-        service = self._testService()
-
-        newRecord = DirectoryRecord(
-            service,
-            fields = {
-                service.fieldName.uid:        "__plugh__",
-                service.fieldName.recordType: service.recordType.user,
-                service.fieldName.shortNames: ("plugh",),
-            }
-        )
-
-        yield service.updateRecords((newRecord,), create=True)
-
-        # Verify change is present immediately
-        record = (yield service.recordWithUID("__plugh__"))
-        self.assertEquals(set(record.shortNames), set(("plugh",)))
-
-        # Verify change is persisted
-        service.flush()
-        record = (yield service.recordWithUID("__plugh__"))
-        self.assertEquals(set(record.shortNames), set(("plugh",)))
-
-
-    def test_addRecordNoCreate(self):
-        service = self._testService()
-
-        newRecord = DirectoryRecord(
-            service,
-            fields = {
-                service.fieldName.uid:        "__plugh__",
-                service.fieldName.recordType: service.recordType.user,
-                service.fieldName.shortNames: ("plugh",),
-            }
-        )
-
-        self.assertFailure(service.updateRecords((newRecord,)), NoSuchRecordError)
-
-
-    @inlineCallbacks
-    def test_removeRecord(self):
-        service = self._testService()
-
-        yield service.removeRecords(("__wsanchez__",))
-
-        # Verify change is present immediately
-        self.assertEquals((yield service.recordWithUID("__wsanchez__")), None)
-
-        # Verify change is persisted
-        service.flush()
-        self.assertEquals((yield service.recordWithUID("__wsanchez__")), None)
-
-
-    def test_removeRecordNoExist(self):
-        service = self._testService()
-
-        return service.removeRecords(("__plugh__",))
-
-
-
-class DirectoryRecordTest(BaseTest, test_directory.DirectoryRecordTest):
-    @inlineCallbacks
-    def test_members(self):
-        service = self._testService()
-
-        record = (yield service.recordWithUID("__wsanchez__"))
-        members = (yield record.members())
-        self.assertEquals(set(members), set())
-
-        record = (yield service.recordWithUID("__twisted__"))
-        members = (yield record.members())
-        self.assertEquals(
-            set((member.uid for member in members)),
-            set((
-                "__wsanchez__",
-                "__glyph__",
-                "__exarkun__",
-                "__dreid__",
-                "__dre__",
-            ))
-        )
-
-        record = (yield service.recordWithUID("__developers__"))
-        members = (yield record.members())
-        self.assertEquals(
-            set((member.uid for member in members)),
-            set((
-                "__calendar-dev__",
-                "__twisted__",
-                "__alyssa__",
-            ))
-        )
-
-    @inlineCallbacks
-    def test_groups(self):
-        service = self._testService()
-
-        record = (yield service.recordWithUID("__wsanchez__"))
-        groups = (yield record.groups())
-        self.assertEquals(
-            set(group.uid for group in groups),
-            set((
-                "__calendar-dev__",
-                "__twisted__",
-            ))
-        )

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_xml.py (from rev 10753, CalendarServer/trunk/twext/who/test/test_xml.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_xml.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/test/test_xml.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,819 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+XML directory service tests
+"""
+
+from time import sleep
+
+from twisted.python.filepath import FilePath
+from twisted.internet.defer import inlineCallbacks
+
+from twext.who.idirectory import NoSuchRecordError
+from twext.who.idirectory import DirectoryQueryMatchExpression
+from twext.who.idirectory import Operand, MatchType, QueryFlags
+from twext.who.xml import ParseError
+from twext.who.xml import DirectoryService, DirectoryRecord
+
+from twext.who.test import test_directory
+
+
+
+xmlRealmName = "Test Realm"
+
+testXMLConfig = """<?xml version="1.0" encoding="utf-8"?>
+
+<directory realm="xyzzy">
+
+  <record type="user">
+    <uid>__wsanchez__</uid>
+    <short-name>wsanchez</short-name>
+    <short-name>wilfredo_sanchez</short-name>
+    <full-name>Wilfredo Sanchez</full-name>
+    <password>zehcnasw</password>
+    <email>wsanchez at bitbucket.calendarserver.org</email>
+    <email>wsanchez at devnull.twistedmatrix.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__glyph__</uid>
+    <short-name>glyph</short-name>
+    <full-name>Glyph Lefkowitz</full-name>
+    <password>hpylg</password>
+    <email>glyph at bitbucket.calendarserver.org</email>
+    <email>glyph at devnull.twistedmatrix.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__sagen__</uid>
+    <short-name>sagen</short-name>
+    <full-name>Morgen Sagen</full-name>
+    <password>negas</password>
+    <email>sagen at bitbucket.calendarserver.org</email>
+    <email>shared at example.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__cdaboo__</uid>
+    <short-name>cdaboo</short-name>
+    <full-name>Cyrus Daboo</full-name>
+    <password>suryc</password>
+    <email>cdaboo at bitbucket.calendarserver.org</email>
+  </record>
+
+  <record type="user">
+    <uid>__dre__</uid>
+    <short-name>dre</short-name>
+    <full-name>Andre LaBranche</full-name>
+    <password>erd</password>
+    <email>dre at bitbucket.calendarserver.org</email>
+    <email>shared at example.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__exarkun__</uid>
+    <short-name>exarkun</short-name>
+    <full-name>Jean-Paul Calderone</full-name>
+    <password>nucraxe</password>
+    <email>exarkun at devnull.twistedmatrix.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__dreid__</uid>
+    <short-name>dreid</short-name>
+    <full-name>David Reid</full-name>
+    <password>dierd</password>
+    <email>dreid at devnull.twistedmatrix.com</email>
+  </record>
+
+  <record> <!-- type defaults to "user" -->
+    <uid>__joe__</uid>
+    <short-name>joe</short-name>
+    <full-name>Joe Schmoe</full-name>
+    <password>eoj</password>
+    <email>joe at example.com</email>
+  </record>
+
+  <record>
+    <uid>__alyssa__</uid>
+    <short-name>alyssa</short-name>
+    <full-name>Alyssa P. Hacker</full-name>
+    <password>assyla</password>
+    <email>alyssa at example.com</email>
+  </record>
+
+  <record type="group">
+    <uid>__calendar-dev__</uid>
+    <short-name>calendar-dev</short-name>
+    <full-name>Calendar Server developers</full-name>
+    <email>dev at bitbucket.calendarserver.org</email>
+    <member-uid>__wsanchez__</member-uid>
+    <member-uid>__glyph__</member-uid>
+    <member-uid>__sagen__</member-uid>
+    <member-uid>__cdaboo__</member-uid>
+    <member-uid>__dre__</member-uid>
+  </record>
+
+  <record type="group">
+    <uid>__twisted__</uid>
+    <short-name>twisted</short-name>
+    <full-name>Twisted Matrix Laboratories</full-name>
+    <email>hack at devnull.twistedmatrix.com</email>
+    <member-uid>__wsanchez__</member-uid>
+    <member-uid>__glyph__</member-uid>
+    <member-uid>__exarkun__</member-uid>
+    <member-uid>__dreid__</member-uid>
+    <member-uid>__dre__</member-uid>
+  </record>
+
+  <record type="group">
+    <uid>__developers__</uid>
+    <short-name>developers</short-name>
+    <full-name>All Developers</full-name>
+    <member-uid>__calendar-dev__</member-uid>
+    <member-uid>__twisted__</member-uid>
+    <member-uid>__alyssa__</member-uid>
+  </record>
+
+</directory>
+"""
+
+
+
+class BaseTest(object):
+    def _testService(self, xmlData=None):
+        if xmlData is None:
+            xmlData = testXMLConfig
+
+        filePath = FilePath(self.mktemp())
+        filePath.setContent(xmlData)
+
+        class TestService(DirectoryService):
+            def query(self, field, value, matchType=MatchType.equals, flags=None):
+                name = getattr(self.fieldName, field)
+                assert name is not None
+                return DirectoryQueryMatchExpression(
+                    name, value,
+                    matchType = matchType,
+                    flags = flags,
+                )
+
+        return TestService(filePath)
+
+
+
+class DirectoryServiceTest(BaseTest, test_directory.DirectoryServiceTest):
+    def test_repr(self):
+        service = self._testService()
+
+        self.assertEquals(repr(service), "<TestService (not loaded)>")
+        service.loadRecords()
+        self.assertEquals(repr(service), "<TestService 'xyzzy'>")
+
+
+    def assertRecords(self, records, uids):
+        self.assertEquals(
+            frozenset((record.uid for record in records)),
+            frozenset((uids)),
+        )
+
+
+    def test_realmNameImmutable(self):
+        def setRealmName():
+            service = self._testService()
+            service.realmName = "foo"
+
+        self.assertRaises(AssertionError, setRealmName)
+
+
+    def test_reloadInterval(self):
+        service = self._testService()
+
+        service.loadRecords(stat=False)
+        lastRefresh = service._lastRefresh
+        self.assertTrue(service._lastRefresh)
+
+        sleep(1)
+        service.loadRecords(stat=False)
+        self.assertEquals(lastRefresh, service._lastRefresh)
+
+
+    def test_reloadStat(self):
+        service = self._testService()
+
+        service.loadRecords(loadNow=True)
+        lastRefresh = service._lastRefresh
+        self.assertTrue(service._lastRefresh)
+
+        sleep(1)
+        service.loadRecords(loadNow=True)
+        self.assertEquals(lastRefresh, service._lastRefresh)
+
+
+    def test_badXML(self):
+        service = self._testService(xmlData="Hello")
+
+        self.assertRaises(ParseError, service.loadRecords)
+
+
+    def test_badRootElement(self):
+        service = self._testService(xmlData=
+"""<?xml version="1.0" encoding="utf-8"?>
+
+<frobnitz />
+"""
+        )
+
+        self.assertRaises(ParseError, service.loadRecords)
+        try:
+            service.loadRecords()
+        except ParseError as e:
+            self.assertTrue(str(e).startswith("Incorrect root element"), e)
+        else:
+            raise AssertionError
+
+
+    def test_noRealmName(self):
+        service = self._testService(xmlData=
+"""<?xml version="1.0" encoding="utf-8"?>
+
+<directory />
+"""
+        )
+
+        self.assertRaises(ParseError, service.loadRecords)
+        try:
+            service.loadRecords()
+        except ParseError as e:
+            self.assertTrue(str(e).startswith("No realm name"), e)
+        else:
+            raise AssertionError
+
+
+    @inlineCallbacks
+    def test_recordWithUID(self):
+        service = self._testService()
+
+        record = (yield service.recordWithUID("__null__"))
+        self.assertEquals(record, None)
+
+        record = (yield service.recordWithUID("__wsanchez__"))
+        self.assertEquals(record.uid, "__wsanchez__")
+
+
+    @inlineCallbacks
+    def test_recordWithGUID(self):
+        service = self._testService()
+        record = (yield service.recordWithGUID("6C495FCD-7E78-4D5C-AA66-BC890AD04C9D"))
+        self.assertEquals(record, None)
+
+    @inlineCallbacks
+    def test_recordsWithRecordType(self):
+        service = self._testService()
+
+        records = (yield service.recordsWithRecordType(object()))
+        self.assertEquals(set(records), set())
+
+        records = (yield service.recordsWithRecordType(service.recordType.user))
+        self.assertRecords(records,
+            (
+                "__wsanchez__",
+                "__glyph__",
+                "__sagen__",
+                "__cdaboo__",
+                "__dre__",
+                "__exarkun__",
+                "__dreid__",
+                "__alyssa__",
+                "__joe__",
+            ),
+        )
+
+        records = (yield service.recordsWithRecordType(service.recordType.group))
+        self.assertRecords(records,
+            (
+                "__calendar-dev__",
+                "__twisted__",
+                "__developers__",
+            ),
+        )
+
+
+    @inlineCallbacks
+    def test_recordWithShortName(self):
+        service = self._testService()
+
+        record = (yield service.recordWithShortName(service.recordType.user, "null"))
+        self.assertEquals(record, None)
+
+        record = (yield service.recordWithShortName(service.recordType.user, "wsanchez"))
+        self.assertEquals(record.uid, "__wsanchez__")
+
+        record = (yield service.recordWithShortName(service.recordType.user, "wilfredo_sanchez"))
+        self.assertEquals(record.uid, "__wsanchez__")
+
+
+    @inlineCallbacks
+    def test_recordsWithEmailAddress(self):
+        service = self._testService()
+
+        records = (yield service.recordsWithEmailAddress("wsanchez at bitbucket.calendarserver.org"))
+        self.assertRecords(records, ("__wsanchez__",))
+
+        records = (yield service.recordsWithEmailAddress("wsanchez at devnull.twistedmatrix.com"))
+        self.assertRecords(records, ("__wsanchez__",))
+
+        records = (yield service.recordsWithEmailAddress("shared at example.com"))
+        self.assertRecords(records, ("__sagen__", "__dre__"))
+
+
+    @inlineCallbacks
+    def test_queryAnd(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery(
+            (
+                service.query("emailAddresses", "shared at example.com"),
+                service.query("shortNames", "sagen"),
+            ),
+            operand=Operand.AND
+        )
+        self.assertRecords(records, ("__sagen__",))
+
+
+    @inlineCallbacks
+    def test_queryOr(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery(
+            (
+                service.query("emailAddresses", "shared at example.com"),
+                service.query("shortNames", "wsanchez"),
+            ),
+            operand=Operand.OR
+        )
+        self.assertRecords(records, ("__sagen__", "__dre__", "__wsanchez__"))
+
+
+    @inlineCallbacks
+    def test_queryNot(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery(
+            (
+                service.query("emailAddresses", "shared at example.com"),
+                service.query("shortNames", "sagen", flags=QueryFlags.NOT),
+            ),
+            operand=Operand.AND
+        )
+        self.assertRecords(records, ("__dre__",))
+
+
+    @inlineCallbacks
+    def test_queryNotNoIndex(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery(
+            (
+                service.query("emailAddresses", "shared at example.com"),
+                service.query("fullNames", "Andre LaBranche", flags=QueryFlags.NOT),
+            ),
+            operand=Operand.AND
+        )
+        self.assertRecords(records, ("__sagen__",))
+
+
+    @inlineCallbacks
+    def test_queryCaseInsensitive(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query("shortNames", "SagEn", flags=QueryFlags.caseInsensitive),
+        ))
+        self.assertRecords(records, ("__sagen__",))
+
+
+    @inlineCallbacks
+    def test_queryCaseInsensitiveNoIndex(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query("fullNames", "moRGen SAGen", flags=QueryFlags.caseInsensitive),
+        ))
+        self.assertRecords(records, ("__sagen__",))
+
+
+    @inlineCallbacks
+    def test_queryStartsWith(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query("shortNames", "wil", matchType=MatchType.startsWith),
+        ))
+        self.assertRecords(records, ("__wsanchez__",))
+
+
+    @inlineCallbacks
+    def test_queryStartsWithNoIndex(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query("fullNames", "Wilfredo", matchType=MatchType.startsWith),
+        ))
+        self.assertRecords(records, ("__wsanchez__",))
+
+
+    @inlineCallbacks
+    def test_queryStartsWithNot(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query(
+                "shortNames", "w",
+                matchType = MatchType.startsWith,
+                flags = QueryFlags.NOT,
+            ),
+        ))
+        self.assertRecords(
+            records,
+            (
+                '__alyssa__',
+                '__calendar-dev__',
+                '__cdaboo__',
+                '__developers__',
+                '__dre__',
+                '__dreid__',
+                '__exarkun__',
+                '__glyph__',
+                '__joe__',
+                '__sagen__',
+                '__twisted__',
+            ),
+        )
+
+
+    @inlineCallbacks
+    def test_queryStartsWithNotAny(self):
+        """
+        FIXME?: In the this case, the record __wsanchez__ has two
+        shortNames, and one doesn't match the query.  Should it be
+        included or not?  It is, because one matches the query, but
+        should NOT require that all match?
+        """
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query(
+                "shortNames", "wil",
+                matchType = MatchType.startsWith,
+                flags = QueryFlags.NOT,
+            ),
+        ))
+        self.assertRecords(
+            records,
+            (
+                '__alyssa__',
+                '__calendar-dev__',
+                '__cdaboo__',
+                '__developers__',
+                '__dre__',
+                '__dreid__',
+                '__exarkun__',
+                '__glyph__',
+                '__joe__',
+                '__sagen__',
+                '__twisted__',
+                '__wsanchez__',
+            ),
+        )
+
+
+    @inlineCallbacks
+    def test_queryStartsWithNotNoIndex(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query(
+                "fullNames", "Wilfredo",
+                matchType = MatchType.startsWith,
+                flags = QueryFlags.NOT,
+            ),
+        ))
+        self.assertRecords(
+            records,
+            (
+                '__alyssa__',
+                '__calendar-dev__',
+                '__cdaboo__',
+                '__developers__',
+                '__dre__',
+                '__dreid__',
+                '__exarkun__',
+                '__glyph__',
+                '__joe__',
+                '__sagen__',
+                '__twisted__',
+            ),
+        )
+
+
+    @inlineCallbacks
+    def test_queryStartsWithCaseInsensitive(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query(
+                "shortNames", "WIL",
+                matchType = MatchType.startsWith,
+                flags = QueryFlags.caseInsensitive,
+            ),
+        ))
+        self.assertRecords(records, ("__wsanchez__",))
+
+
+    @inlineCallbacks
+    def test_queryStartsWithCaseInsensitiveNoIndex(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query(
+                "fullNames", "wilfrEdo",
+                matchType = MatchType.startsWith,
+                flags = QueryFlags.caseInsensitive,
+            ),
+        ))
+        self.assertRecords(records, ("__wsanchez__",))
+
+
+    @inlineCallbacks
+    def test_queryContains(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query("shortNames", "sanchez", matchType=MatchType.contains),
+        ))
+        self.assertRecords(records, ("__wsanchez__",))
+
+
+    @inlineCallbacks
+    def test_queryContainsNoIndex(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query("fullNames", "fred", matchType=MatchType.contains),
+        ))
+        self.assertRecords(records, ("__wsanchez__",))
+
+
+    @inlineCallbacks
+    def test_queryContainsNot(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query(
+                "shortNames", "sanchez",
+                matchType = MatchType.contains,
+                flags = QueryFlags.NOT,
+            ),
+        ))
+        self.assertRecords(
+            records,
+            (
+                '__alyssa__',
+                '__calendar-dev__',
+                '__cdaboo__',
+                '__developers__',
+                '__dre__',
+                '__dreid__',
+                '__exarkun__',
+                '__glyph__',
+                '__joe__',
+                '__sagen__',
+                '__twisted__',
+            ),
+        )
+
+
+    @inlineCallbacks
+    def test_queryContainsNotNoIndex(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query(
+                "fullNames", "fred",
+                matchType = MatchType.contains,
+                flags = QueryFlags.NOT,
+            ),
+        ))
+        self.assertRecords(
+            records,
+            (
+                '__alyssa__',
+                '__calendar-dev__',
+                '__cdaboo__',
+                '__developers__',
+                '__dre__',
+                '__dreid__',
+                '__exarkun__',
+                '__glyph__',
+                '__joe__',
+                '__sagen__',
+                '__twisted__',
+            ),
+        )
+
+
+    @inlineCallbacks
+    def test_queryContainsCaseInsensitive(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query(
+                "shortNames", "Sanchez",
+                matchType=MatchType.contains,
+                flags=QueryFlags.caseInsensitive,
+            ),
+        ))
+        self.assertRecords(records, ("__wsanchez__",))
+
+
+    @inlineCallbacks
+    def test_queryContainsCaseInsensitiveNoIndex(self):
+        service = self._testService()
+        records = yield service.recordsFromQuery((
+            service.query(
+                "fullNames", "frEdo",
+                matchType=MatchType.contains,
+                flags=QueryFlags.caseInsensitive,
+            ),
+        ))
+        self.assertRecords(records, ("__wsanchez__",))
+
+
+    def test_unknownRecordTypesClean(self):
+        service = self._testService()
+        self.assertEquals(set(service.unknownRecordTypes), set())
+
+
+    def test_unknownRecordTypesDirty(self):
+        service = self._testService(xmlData=
+"""<?xml version="1.0" encoding="utf-8"?>
+
+<directory realm="Unknown Record Types">
+  <record type="camera">
+    <uid>__d600__</uid>
+    <short-name>d600</short-name>
+    <full-name>Nikon D600</full-name>
+  </record>
+</directory>
+"""
+        )
+        self.assertEquals(set(service.unknownRecordTypes), set(("camera",)))
+
+
+    def test_unknownFieldElementsClean(self):
+        service = self._testService()
+        self.assertEquals(set(service.unknownFieldElements), set())
+
+
+    def test_unknownFieldElementsDirty(self):
+        service = self._testService(xmlData=
+"""<?xml version="1.0" encoding="utf-8"?>
+
+<directory realm="Unknown Record Types">
+  <record type="user">
+    <uid>__wsanchez__</uid>
+    <short-name>wsanchez</short-name>
+    <political-affiliation>Community and Freedom Party</political-affiliation>
+  </record>
+</directory>
+"""
+        )
+        self.assertEquals(set(service.unknownFieldElements), set(("political-affiliation",)))
+
+
+    @inlineCallbacks
+    def test_updateRecord(self):
+        service = self._testService()
+
+        record = (yield service.recordWithUID("__wsanchez__"))
+
+        fields = record.fields.copy()
+        fields[service.fieldName.fullNames] = ["Wilfredo Sanchez Vega"]
+
+        updatedRecord = DirectoryRecord(service, fields)
+        yield service.updateRecords((updatedRecord,))
+
+        # Verify change is present immediately
+        record = (yield service.recordWithUID("__wsanchez__"))
+        self.assertEquals(set(record.fullNames), set(("Wilfredo Sanchez Vega",)))
+
+        # Verify change is persisted
+        service.flush()
+        record = (yield service.recordWithUID("__wsanchez__"))
+        self.assertEquals(set(record.fullNames), set(("Wilfredo Sanchez Vega",)))
+
+
+    @inlineCallbacks
+    def test_addRecord(self):
+        service = self._testService()
+
+        newRecord = DirectoryRecord(
+            service,
+            fields = {
+                service.fieldName.uid:        "__plugh__",
+                service.fieldName.recordType: service.recordType.user,
+                service.fieldName.shortNames: ("plugh",),
+            }
+        )
+
+        yield service.updateRecords((newRecord,), create=True)
+
+        # Verify change is present immediately
+        record = (yield service.recordWithUID("__plugh__"))
+        self.assertEquals(set(record.shortNames), set(("plugh",)))
+
+        # Verify change is persisted
+        service.flush()
+        record = (yield service.recordWithUID("__plugh__"))
+        self.assertEquals(set(record.shortNames), set(("plugh",)))
+
+
+    def test_addRecordNoCreate(self):
+        service = self._testService()
+
+        newRecord = DirectoryRecord(
+            service,
+            fields = {
+                service.fieldName.uid:        "__plugh__",
+                service.fieldName.recordType: service.recordType.user,
+                service.fieldName.shortNames: ("plugh",),
+            }
+        )
+
+        self.assertFailure(service.updateRecords((newRecord,)), NoSuchRecordError)
+
+
+    @inlineCallbacks
+    def test_removeRecord(self):
+        service = self._testService()
+
+        yield service.removeRecords(("__wsanchez__",))
+
+        # Verify change is present immediately
+        self.assertEquals((yield service.recordWithUID("__wsanchez__")), None)
+
+        # Verify change is persisted
+        service.flush()
+        self.assertEquals((yield service.recordWithUID("__wsanchez__")), None)
+
+
+    def test_removeRecordNoExist(self):
+        service = self._testService()
+
+        return service.removeRecords(("__plugh__",))
+
+
+
+class DirectoryRecordTest(BaseTest, test_directory.DirectoryRecordTest):
+    @inlineCallbacks
+    def test_members(self):
+        service = self._testService()
+
+        record = (yield service.recordWithUID("__wsanchez__"))
+        members = (yield record.members())
+        self.assertEquals(set(members), set())
+
+        record = (yield service.recordWithUID("__twisted__"))
+        members = (yield record.members())
+        self.assertEquals(
+            set((member.uid for member in members)),
+            set((
+                "__wsanchez__",
+                "__glyph__",
+                "__exarkun__",
+                "__dreid__",
+                "__dre__",
+            ))
+        )
+
+        record = (yield service.recordWithUID("__developers__"))
+        members = (yield record.members())
+        self.assertEquals(
+            set((member.uid for member in members)),
+            set((
+                "__calendar-dev__",
+                "__twisted__",
+                "__alyssa__",
+            ))
+        )
+
+    @inlineCallbacks
+    def test_groups(self):
+        service = self._testService()
+
+        record = (yield service.recordWithUID("__wsanchez__"))
+        groups = (yield record.groups())
+        self.assertEquals(
+            set(group.uid for group in groups),
+            set((
+                "__calendar-dev__",
+                "__twisted__",
+            ))
+        )

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twext/who/util.py
===================================================================
--- CalendarServer/trunk/twext/who/util.py	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/util.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,107 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Directory service module utilities.
-"""
-
-__all__ = [
-    "MergedConstants",
-    "uniqueResult",
-    "describe",
-    "iterFlags",
-]
-
-from types import FunctionType
-
-from twisted.python.constants import NamedConstant
-
-from twext.who.idirectory import DirectoryServiceError
-
-
-
-class MergedConstants(object):
-    """
-    Work-around for the fact that Names is apparently not subclassable
-    and doesn't provide a way to merge multiple Names classes.
-    """
-    def __init__(self, *containers):
-        seenNames = set()
-        myContainers = set()
-        for container in containers:
-            for constant in container.iterconstants():
-                if constant.name in seenNames:
-                    raise ValueError(
-                        "Multiple constants with the same name may not be merged: %s"
-                        % (constant.name,)
-                    )
-                seenNames.add(constant.name)
-
-            if isinstance(container, MergedConstants):
-                # Avoid nesting
-                myContainers |= container._containers
-            else:
-                myContainers.add(container)
-
-        self._containers = myContainers
-
-    def __getattr__(self, name):
-        for container in self._containers:
-            attr = getattr(container, name, None)
-            if attr is not None:
-                # Named constant or static method
-                if isinstance(attr, (NamedConstant, FunctionType)):
-                    return attr
-
-        raise AttributeError(name)
-
-    def iterconstants(self):
-        for container in self._containers:
-            for constant in container.iterconstants():
-                yield constant
-
-    def lookupByName(self, name):
-        for container in self._containers:
-            try:
-                return container.lookupByName(name)
-            except ValueError:
-                pass
-
-        raise ValueError(name)
-
-
-
-def uniqueResult(values):
-    result = None
-    for value in values:
-        if result is None:
-            result = value
-        else:
-            raise DirectoryServiceError("Multiple values found where one expected.")
-    return result
-
-
-def describe(constant):
-    return getattr(constant, "description", str(constant))
-
-
-def iterFlags(flags):
-    if hasattr(flags, "__iter__"):
-        return flags
-    else:
-        # Work around http://twistedmatrix.com/trac/ticket/6302
-        # FIXME: This depends on a private attribute (flags._container)
-        return (flags._container.lookupByName(name) for name in flags.names)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/who/util.py (from rev 10753, CalendarServer/trunk/twext/who/util.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/who/util.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/util.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,107 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Directory service module utilities.
+"""
+
+__all__ = [
+    "MergedConstants",
+    "uniqueResult",
+    "describe",
+    "iterFlags",
+]
+
+from types import FunctionType
+
+from twisted.python.constants import NamedConstant
+
+from twext.who.idirectory import DirectoryServiceError
+
+
+
+class MergedConstants(object):
+    """
+    Work-around for the fact that Names is apparently not subclassable
+    and doesn't provide a way to merge multiple Names classes.
+    """
+    def __init__(self, *containers):
+        seenNames = set()
+        myContainers = set()
+        for container in containers:
+            for constant in container.iterconstants():
+                if constant.name in seenNames:
+                    raise ValueError(
+                        "Multiple constants with the same name may not be merged: %s"
+                        % (constant.name,)
+                    )
+                seenNames.add(constant.name)
+
+            if isinstance(container, MergedConstants):
+                # Avoid nesting
+                myContainers |= container._containers
+            else:
+                myContainers.add(container)
+
+        self._containers = myContainers
+
+    def __getattr__(self, name):
+        for container in self._containers:
+            attr = getattr(container, name, None)
+            if attr is not None:
+                # Named constant or static method
+                if isinstance(attr, (NamedConstant, FunctionType)):
+                    return attr
+
+        raise AttributeError(name)
+
+    def iterconstants(self):
+        for container in self._containers:
+            for constant in container.iterconstants():
+                yield constant
+
+    def lookupByName(self, name):
+        for container in self._containers:
+            try:
+                return container.lookupByName(name)
+            except ValueError:
+                pass
+
+        raise ValueError(name)
+
+
+
+def uniqueResult(values):
+    result = None
+    for value in values:
+        if result is None:
+            result = value
+        else:
+            raise DirectoryServiceError("Multiple values found where one expected.")
+    return result
+
+
+def describe(constant):
+    return getattr(constant, "description", str(constant))
+
+
+def iterFlags(flags):
+    if hasattr(flags, "__iter__"):
+        return flags
+    else:
+        # Work around http://twistedmatrix.com/trac/ticket/6302
+        # FIXME: This depends on a private attribute (flags._container)
+        return (flags._container.lookupByName(name) for name in flags.names)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twext/who/xml.py
===================================================================
--- CalendarServer/trunk/twext/who/xml.py	2013-02-18 18:39:49 UTC (rev 10753)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/xml.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,434 +0,0 @@
-##
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from __future__ import absolute_import
-
-"""
-XML directory service implementation.
-"""
-
-__all__ = [
-    "ParseError",
-    "DirectoryService",
-    "DirectoryRecord",
-]
-
-from time import time
-
-from xml.etree.ElementTree import parse as parseXML
-from xml.etree.ElementTree import ParseError as XMLParseError
-from xml.etree.ElementTree import tostring as etreeToString
-from xml.etree.ElementTree import Element as XMLElement
-
-from twisted.python.constants import Values, ValueConstant
-from twisted.internet.defer import fail
-
-from twext.who.idirectory import DirectoryServiceError
-from twext.who.idirectory import NoSuchRecordError, UnknownRecordTypeError
-from twext.who.idirectory import RecordType, FieldName as BaseFieldName
-from twext.who.index import DirectoryService as BaseDirectoryService
-from twext.who.index import DirectoryRecord
-from twext.who.index import FieldName as IndexFieldName
-
-
-
-##
-# Exceptions
-##
-
-class ParseError(DirectoryServiceError):
-    """
-    Parse error.
-    """
-
-
-
-##
-# XML constants
-##
-
-class Element(Values):
-    directory = ValueConstant("directory")
-    record    = ValueConstant("record")
-
-    #
-    # Field names
-    #
-    uid = ValueConstant("uid")
-    uid.fieldName = BaseFieldName.uid
-
-    guid = ValueConstant("guid")
-    guid.fieldName = BaseFieldName.guid
-
-    shortName = ValueConstant("short-name")
-    shortName.fieldName = BaseFieldName.shortNames
-
-    fullName = ValueConstant("full-name")
-    fullName.fieldName = BaseFieldName.fullNames
-
-    emailAddress = ValueConstant("email")
-    emailAddress.fieldName = BaseFieldName.emailAddresses
-
-    password = ValueConstant("password")
-    password.fieldName = BaseFieldName.password
-
-    memberUID = ValueConstant("member-uid")
-    memberUID.fieldName = IndexFieldName.memberUIDs
-
-
-
-class Attribute(Values):
-    realm      = ValueConstant("realm")
-    recordType = ValueConstant("type")
-
-
-
-class Value(Values):
-    #
-    # Booleans
-    #
-    true  = ValueConstant("true")
-    false = ValueConstant("false")
-
-    #
-    # Record types
-    #
-    user = ValueConstant("user")
-    user.recordType = RecordType.user
-
-    group = ValueConstant("group")
-    group.recordType = RecordType.group
-
-
-
-##
-# Directory Service
-##
-
-class DirectoryService(BaseDirectoryService):
-    """
-    XML directory service.
-    """
-
-    element   = Element
-    attribute = Attribute
-    value     = Value
-
-
-    def __init__(self, filePath, refreshInterval=4):
-        BaseDirectoryService.__init__(self, realmName=noRealmName)
-
-        self.filePath = filePath
-        self.refreshInterval = refreshInterval
-
-
-    def __repr__(self):
-        realmName = self._realmName
-        if realmName is None:
-            realmName = "(not loaded)"
-        else:
-            realmName = repr(realmName)
-
-        return "<%s %s>" % (
-            self.__class__.__name__,
-            realmName,
-        )
-
-
-    @property
-    def realmName(self):
-        self.loadRecords()
-        return self._realmName
-
-
-    @realmName.setter
-    def realmName(self, value):
-        if value is not noRealmName:
-            raise AssertionError("realmName may not be set directly")
-
-
-    @property
-    def unknownRecordTypes(self):
-        self.loadRecords()
-        return self._unknownRecordTypes
-
-
-    @property
-    def unknownFieldElements(self):
-        self.loadRecords()
-        return self._unknownFieldElements
-
-
-    def loadRecords(self, loadNow=False, stat=True):
-        """
-        Load records from L{self.filePath}.
-
-        Does nothing if a successful refresh has happened within the
-        last L{self.refreshInterval} seconds.
-
-        @param loadNow: If true, load now (ignoring
-            L{self.refreshInterval})
-        @type loadNow: L{type}
-
-        @param stat: If true, check file metadata and don't reload if
-            unchanged.
-        @type loadNow: L{type}
-        """
-        #
-        # Punt if we've read the file recently
-        #
-        now = time()
-        if not loadNow and now - self._lastRefresh <= self.refreshInterval:
-            return
-
-        #
-        # Punt if we've read the file and it's still the same.
-        #
-        if stat:
-            self.filePath.restat()
-            cacheTag = (self.filePath.getModificationTime(), self.filePath.getsize())
-            if cacheTag == self._cacheTag:
-                return
-        else:
-            cacheTag = None
-
-        #
-        # Open and parse the file
-        #
-        try:
-            fh = self.filePath.open()
-
-            try:
-                etree = parseXML(fh)
-            except XMLParseError as e:
-                raise ParseError(e)
-        finally:
-            fh.close()
-
-        #
-        # Pull data from DOM
-        #
-        directoryNode = etree.getroot()
-        if directoryNode.tag != self.element.directory.value:
-            raise ParseError("Incorrect root element: %s" % (directoryNode.tag,))
-
-        realmName = directoryNode.get(self.attribute.realm.value, "").encode("utf-8")
-
-        if not realmName:
-            raise ParseError("No realm name.")
-
-        unknownRecordTypes   = set()
-        unknownFieldElements = set()
-
-        records = set()
-
-        for recordNode in directoryNode:
-            try:
-                records.add(self.parseRecordNode(recordNode, unknownFieldElements))
-            except UnknownRecordTypeError as e:
-                unknownRecordTypes.add(e.token)
-
-        #
-        # Store results
-        #
-
-        index = {}
-
-        for fieldName in self.indexedFields:
-            index[fieldName] = {}
-
-        for record in records:
-            for fieldName in self.indexedFields:
-                values = record.fields.get(fieldName, None)
-
-                if values is not None:
-                    if not self.fieldName.isMultiValue(fieldName):
-                        values = (values,)
-
-                    for value in values:
-                        index[fieldName].setdefault(value, set()).add(record)
-
-        self._realmName = realmName
-
-        self._unknownRecordTypes   = unknownRecordTypes
-        self._unknownFieldElements = unknownFieldElements
-
-        self._cacheTag = cacheTag
-        self._lastRefresh = now
-
-        self.index = index
-
-        return etree
-
-
-    def parseRecordNode(self, recordNode, unknownFieldElements=None):
-        recordTypeAttribute = recordNode.get(self.attribute.recordType.value, "").encode("utf-8")
-        if recordTypeAttribute:
-            try:
-                recordType = self.value.lookupByValue(recordTypeAttribute).recordType
-            except (ValueError, AttributeError):
-                raise UnknownRecordTypeError(recordTypeAttribute)
-        else:
-            recordType = self.recordType.user
-
-        fields = {}
-        fields[self.fieldName.recordType] = recordType
-
-        for fieldNode in recordNode:
-            try:
-                fieldElement = self.element.lookupByValue(fieldNode.tag)
-            except ValueError:
-                if unknownFieldElements is not None:
-                    unknownFieldElements.add(fieldNode.tag)
-
-            try:
-                fieldName = fieldElement.fieldName
-            except AttributeError:
-                if unknownFieldElements is not None:
-                    unknownFieldElements.add(fieldNode.tag)
-
-            value = fieldNode.text.encode("utf-8")
-
-            if self.fieldName.isMultiValue(fieldName):
-                values = fields.setdefault(fieldName, [])
-                values.append(value)
-            else:
-                fields[fieldName] = value
-
-        return DirectoryRecord(self, fields)
-
-
-    def _uidForRecordNode(self, recordNode):
-        uidNode = recordNode.find(self.element.uid.value)
-        if uidNode is None:
-            raise NotImplementedError("No UID node")
-
-        return uidNode.text
-
-
-    def flush(self):
-        BaseDirectoryService.flush(self)
-
-        self._realmName            = None
-        self._unknownRecordTypes   = None
-        self._unknownFieldElements = None
-        self._cacheTag             = None
-        self._lastRefresh          = 0
-
-
-    def updateRecords(self, records, create=False):
-        # Index the records to update by UID
-        recordsByUID = dict(((record.uid, record) for record in records))
-
-        # Index the record type -> attribute mappings.
-        recordTypes = {}
-        for valueName in self.value.iterconstants():
-            recordType = getattr(valueName, "recordType", None)
-            if recordType is not None:
-                recordTypes[recordType] = valueName.value
-        del valueName
-
-        # Index the field name -> element mappings.
-        fieldNames = {}
-        for elementName in self.element.iterconstants():
-            fieldName = getattr(elementName, "fieldName", None)
-            if fieldName is not None:
-                fieldNames[fieldName] = elementName.value
-        del elementName
-
-        directoryNode = self._directoryNodeForEditing()
-
-        def fillRecordNode(recordNode, record):
-            for (name, value) in record.fields.items():
-                if name == self.fieldName.recordType:
-                    if value in recordTypes:
-                        recordNode.set(self.attribute.recordType.value, recordTypes[value])
-                    else:
-                        raise AssertionError("Unknown record type: %r" % (value,))
-
-                else:
-                    if name in fieldNames:
-                        tag = fieldNames[name]
-
-                        if self.fieldName.isMultiValue(name):
-                            values = value
-                        else:
-                            values = (value,)
-
-                        for value in values:
-                            subNode = XMLElement(tag)
-                            subNode.text = value
-                            recordNode.append(subNode)
-
-                    else:
-                        raise AssertionError("Unknown field name: %r" % (name,))
-
-        # Walk through the record nodes in the XML tree and apply
-        # updates.
-        for recordNode in directoryNode:
-            uid = self._uidForRecordNode(recordNode)
-
-            record = recordsByUID.get(uid, None)
-
-            if record:
-                recordNode.clear()
-                fillRecordNode(recordNode, record)
-                del recordsByUID[uid]
-
-        if recordsByUID:
-            if not create:
-                return fail(NoSuchRecordError(recordsByUID.keys()))
-
-            for uid, record in recordsByUID.items():
-                recordNode = XMLElement(self.element.record.value)
-                fillRecordNode(recordNode, record)
-                directoryNode.append(recordNode)
-
-        self._writeDirectoryNode(directoryNode)
-
-
-    def removeRecords(self, uids):
-        directoryNode = self._directoryNodeForEditing()
-
-        #
-        # Walk through the record nodes in the XML tree and start
-        # zapping.
-        #
-        for recordNode in directoryNode:
-            uid = self._uidForRecordNode(recordNode)
-
-            if uid in uids:
-                directoryNode.remove(recordNode)
-
-        self._writeDirectoryNode(directoryNode)
-
-
-    def _directoryNodeForEditing(self):
-        """
-        Drop cached data and load the XML DOM.
-        """
-        self.flush()
-        etree = self.loadRecords(loadNow=True)
-        return etree.getroot()
-
-
-    def _writeDirectoryNode(self, directoryNode):
-        self.filePath.setContent(etreeToString(directoryNode))
-        self.flush()
-
-
-
-noRealmName = object()

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/who/xml.py (from rev 10753, CalendarServer/trunk/twext/who/xml.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/who/xml.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/who/xml.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,434 @@
+##
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from __future__ import absolute_import
+
+"""
+XML directory service implementation.
+"""
+
+__all__ = [
+    "ParseError",
+    "DirectoryService",
+    "DirectoryRecord",
+]
+
+from time import time
+
+from xml.etree.ElementTree import parse as parseXML
+from xml.etree.ElementTree import ParseError as XMLParseError
+from xml.etree.ElementTree import tostring as etreeToString
+from xml.etree.ElementTree import Element as XMLElement
+
+from twisted.python.constants import Values, ValueConstant
+from twisted.internet.defer import fail
+
+from twext.who.idirectory import DirectoryServiceError
+from twext.who.idirectory import NoSuchRecordError, UnknownRecordTypeError
+from twext.who.idirectory import RecordType, FieldName as BaseFieldName
+from twext.who.index import DirectoryService as BaseDirectoryService
+from twext.who.index import DirectoryRecord
+from twext.who.index import FieldName as IndexFieldName
+
+
+
+##
+# Exceptions
+##
+
+class ParseError(DirectoryServiceError):
+    """
+    Parse error.
+    """
+
+
+
+##
+# XML constants
+##
+
+class Element(Values):
+    directory = ValueConstant("directory")
+    record    = ValueConstant("record")
+
+    #
+    # Field names
+    #
+    uid = ValueConstant("uid")
+    uid.fieldName = BaseFieldName.uid
+
+    guid = ValueConstant("guid")
+    guid.fieldName = BaseFieldName.guid
+
+    shortName = ValueConstant("short-name")
+    shortName.fieldName = BaseFieldName.shortNames
+
+    fullName = ValueConstant("full-name")
+    fullName.fieldName = BaseFieldName.fullNames
+
+    emailAddress = ValueConstant("email")
+    emailAddress.fieldName = BaseFieldName.emailAddresses
+
+    password = ValueConstant("password")
+    password.fieldName = BaseFieldName.password
+
+    memberUID = ValueConstant("member-uid")
+    memberUID.fieldName = IndexFieldName.memberUIDs
+
+
+
+class Attribute(Values):
+    realm      = ValueConstant("realm")
+    recordType = ValueConstant("type")
+
+
+
+class Value(Values):
+    #
+    # Booleans
+    #
+    true  = ValueConstant("true")
+    false = ValueConstant("false")
+
+    #
+    # Record types
+    #
+    user = ValueConstant("user")
+    user.recordType = RecordType.user
+
+    group = ValueConstant("group")
+    group.recordType = RecordType.group
+
+
+
+##
+# Directory Service
+##
+
+class DirectoryService(BaseDirectoryService):
+    """
+    XML directory service.
+    """
+
+    element   = Element
+    attribute = Attribute
+    value     = Value
+
+
+    def __init__(self, filePath, refreshInterval=4):
+        BaseDirectoryService.__init__(self, realmName=noRealmName)
+
+        self.filePath = filePath
+        self.refreshInterval = refreshInterval
+
+
+    def __repr__(self):
+        realmName = self._realmName
+        if realmName is None:
+            realmName = "(not loaded)"
+        else:
+            realmName = repr(realmName)
+
+        return "<%s %s>" % (
+            self.__class__.__name__,
+            realmName,
+        )
+
+
+    @property
+    def realmName(self):
+        self.loadRecords()
+        return self._realmName
+
+
+    @realmName.setter
+    def realmName(self, value):
+        if value is not noRealmName:
+            raise AssertionError("realmName may not be set directly")
+
+
+    @property
+    def unknownRecordTypes(self):
+        self.loadRecords()
+        return self._unknownRecordTypes
+
+
+    @property
+    def unknownFieldElements(self):
+        self.loadRecords()
+        return self._unknownFieldElements
+
+
+    def loadRecords(self, loadNow=False, stat=True):
+        """
+        Load records from L{self.filePath}.
+
+        Does nothing if a successful refresh has happened within the
+        last L{self.refreshInterval} seconds.
+
+        @param loadNow: If true, load now (ignoring
+            L{self.refreshInterval})
+        @type loadNow: L{type}
+
+        @param stat: If true, check file metadata and don't reload if
+            unchanged.
+        @type loadNow: L{type}
+        """
+        #
+        # Punt if we've read the file recently
+        #
+        now = time()
+        if not loadNow and now - self._lastRefresh <= self.refreshInterval:
+            return
+
+        #
+        # Punt if we've read the file and it's still the same.
+        #
+        if stat:
+            self.filePath.restat()
+            cacheTag = (self.filePath.getModificationTime(), self.filePath.getsize())
+            if cacheTag == self._cacheTag:
+                return
+        else:
+            cacheTag = None
+
+        #
+        # Open and parse the file
+        #
+        try:
+            fh = self.filePath.open()
+
+            try:
+                etree = parseXML(fh)
+            except XMLParseError as e:
+                raise ParseError(e)
+        finally:
+            fh.close()
+
+        #
+        # Pull data from DOM
+        #
+        directoryNode = etree.getroot()
+        if directoryNode.tag != self.element.directory.value:
+            raise ParseError("Incorrect root element: %s" % (directoryNode.tag,))
+
+        realmName = directoryNode.get(self.attribute.realm.value, "").encode("utf-8")
+
+        if not realmName:
+            raise ParseError("No realm name.")
+
+        unknownRecordTypes   = set()
+        unknownFieldElements = set()
+
+        records = set()
+
+        for recordNode in directoryNode:
+            try:
+                records.add(self.parseRecordNode(recordNode, unknownFieldElements))
+            except UnknownRecordTypeError as e:
+                unknownRecordTypes.add(e.token)
+
+        #
+        # Store results
+        #
+
+        index = {}
+
+        for fieldName in self.indexedFields:
+            index[fieldName] = {}
+
+        for record in records:
+            for fieldName in self.indexedFields:
+                values = record.fields.get(fieldName, None)
+
+                if values is not None:
+                    if not self.fieldName.isMultiValue(fieldName):
+                        values = (values,)
+
+                    for value in values:
+                        index[fieldName].setdefault(value, set()).add(record)
+
+        self._realmName = realmName
+
+        self._unknownRecordTypes   = unknownRecordTypes
+        self._unknownFieldElements = unknownFieldElements
+
+        self._cacheTag = cacheTag
+        self._lastRefresh = now
+
+        self.index = index
+
+        return etree
+
+
+    def parseRecordNode(self, recordNode, unknownFieldElements=None):
+        recordTypeAttribute = recordNode.get(self.attribute.recordType.value, "").encode("utf-8")
+        if recordTypeAttribute:
+            try:
+                recordType = self.value.lookupByValue(recordTypeAttribute).recordType
+            except (ValueError, AttributeError):
+                raise UnknownRecordTypeError(recordTypeAttribute)
+        else:
+            recordType = self.recordType.user
+
+        fields = {}
+        fields[self.fieldName.recordType] = recordType
+
+        for fieldNode in recordNode:
+            try:
+                fieldElement = self.element.lookupByValue(fieldNode.tag)
+            except ValueError:
+                if unknownFieldElements is not None:
+                    unknownFieldElements.add(fieldNode.tag)
+
+            try:
+                fieldName = fieldElement.fieldName
+            except AttributeError:
+                if unknownFieldElements is not None:
+                    unknownFieldElements.add(fieldNode.tag)
+
+            value = fieldNode.text.encode("utf-8")
+
+            if self.fieldName.isMultiValue(fieldName):
+                values = fields.setdefault(fieldName, [])
+                values.append(value)
+            else:
+                fields[fieldName] = value
+
+        return DirectoryRecord(self, fields)
+
+
+    def _uidForRecordNode(self, recordNode):
+        uidNode = recordNode.find(self.element.uid.value)
+        if uidNode is None:
+            raise NotImplementedError("No UID node")
+
+        return uidNode.text
+
+
+    def flush(self):
+        BaseDirectoryService.flush(self)
+
+        self._realmName            = None
+        self._unknownRecordTypes   = None
+        self._unknownFieldElements = None
+        self._cacheTag             = None
+        self._lastRefresh          = 0
+
+
+    def updateRecords(self, records, create=False):
+        # Index the records to update by UID
+        recordsByUID = dict(((record.uid, record) for record in records))
+
+        # Index the record type -> attribute mappings.
+        recordTypes = {}
+        for valueName in self.value.iterconstants():
+            recordType = getattr(valueName, "recordType", None)
+            if recordType is not None:
+                recordTypes[recordType] = valueName.value
+        del valueName
+
+        # Index the field name -> element mappings.
+        fieldNames = {}
+        for elementName in self.element.iterconstants():
+            fieldName = getattr(elementName, "fieldName", None)
+            if fieldName is not None:
+                fieldNames[fieldName] = elementName.value
+        del elementName
+
+        directoryNode = self._directoryNodeForEditing()
+
+        def fillRecordNode(recordNode, record):
+            for (name, value) in record.fields.items():
+                if name == self.fieldName.recordType:
+                    if value in recordTypes:
+                        recordNode.set(self.attribute.recordType.value, recordTypes[value])
+                    else:
+                        raise AssertionError("Unknown record type: %r" % (value,))
+
+                else:
+                    if name in fieldNames:
+                        tag = fieldNames[name]
+
+                        if self.fieldName.isMultiValue(name):
+                            values = value
+                        else:
+                            values = (value,)
+
+                        for value in values:
+                            subNode = XMLElement(tag)
+                            subNode.text = value
+                            recordNode.append(subNode)
+
+                    else:
+                        raise AssertionError("Unknown field name: %r" % (name,))
+
+        # Walk through the record nodes in the XML tree and apply
+        # updates.
+        for recordNode in directoryNode:
+            uid = self._uidForRecordNode(recordNode)
+
+            record = recordsByUID.get(uid, None)
+
+            if record:
+                recordNode.clear()
+                fillRecordNode(recordNode, record)
+                del recordsByUID[uid]
+
+        if recordsByUID:
+            if not create:
+                return fail(NoSuchRecordError(recordsByUID.keys()))
+
+            for uid, record in recordsByUID.items():
+                recordNode = XMLElement(self.element.record.value)
+                fillRecordNode(recordNode, record)
+                directoryNode.append(recordNode)
+
+        self._writeDirectoryNode(directoryNode)
+
+
+    def removeRecords(self, uids):
+        directoryNode = self._directoryNodeForEditing()
+
+        #
+        # Walk through the record nodes in the XML tree and start
+        # zapping.
+        #
+        for recordNode in directoryNode:
+            uid = self._uidForRecordNode(recordNode)
+
+            if uid in uids:
+                directoryNode.remove(recordNode)
+
+        self._writeDirectoryNode(directoryNode)
+
+
+    def _directoryNodeForEditing(self):
+        """
+        Drop cached data and load the XML DOM.
+        """
+        self.flush()
+        etree = self.loadRecords(loadNow=True)
+        return etree.getroot()
+
+
+    def _writeDirectoryNode(self, directoryNode):
+        self.filePath.setContent(etreeToString(directoryNode))
+        self.flush()
+
+
+
+noRealmName = object()

Modified: CalendarServer/branches/users/gaya/sharedgroups/twisted/plugins/caldav.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twisted/plugins/caldav.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twisted/plugins/caldav.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -53,6 +53,4 @@
 
 
 TwistedCalDAV = TAP("calendarserver.tap.caldav.CalDAVServiceMaker")
-CalDAVNotifier = TAP("twistedcaldav.notify.NotificationServiceMaker")
-CalDAVMailGateway = TAP("twistedcaldav.scheduling.imip.mailgateway.MailGatewayServiceMaker")
 CalDAVGroupCacher = TAP("twistedcaldav.directory.directory.GroupMembershipCacherServiceMaker")

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/client/geturl.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/client/geturl.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/client/geturl.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -62,9 +62,11 @@
             self.factory.protocolConnectionMade = None
             d.callback(self)
 
+
     def dataReceived(self, data):
         self.data += data
 
+
     def connectionLost(self, reason):
         self.closed = 1
         self.closedReason = reason
@@ -72,12 +74,16 @@
             d, self.closedDeferred = self.closedDeferred, None
             d.callback(None)
 
+
+
 @inlineCallbacks
 def getURL(url, method="GET", redirect=0):
 
+    if isinstance(url, unicode):
+        url = url.encode("utf-8")
     agent = Agent(reactor)
     headers = http_headers.Headers({})
-    
+
     try:
         response = (yield agent.request(method, url, headers, None))
     except Exception, e:
@@ -90,14 +96,14 @@
             else:
                 location = response.headers.getRawHeaders("location")
                 if location:
-                    newresponse = (yield getURL(location[0], method=method, redirect=redirect+1))
+                    newresponse = (yield getURL(location[0], method=method, redirect=redirect + 1))
                     if response.code == MOVED_PERMANENTLY:
                         scheme, netloc, url, _ignore_params, _ignore_query, _ignore_fragment = urlparse(location[0])
                         newresponse.location = urlunparse((scheme, netloc, url, None, None, None,))
                     returnValue(newresponse)
                 else:
                     log.err("Redirect without a Location header")
-    
+
     if response is not None and response.code / 100 == 2:
         protocol = AccumulatingProtocol()
         response.deliverBody(protocol)
@@ -106,6 +112,5 @@
         response.data = protocol.data
     else:
         log.error("Failed getURL: %s" % (url,))
-        
+
     returnValue(response)
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/customxml.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/customxml.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -418,8 +418,6 @@
         (calendarserver_namespace, "subscription-url") : (1, 1),
         (calendarserver_namespace, "apsbundleid") : (1, 1),
         (calendarserver_namespace, "env") : (1, 1),
-        (calendarserver_namespace, "xmpp-server") : (1, 1),
-        (calendarserver_namespace, "xmpp-uri") : (1, 1),
     }
 
 
@@ -470,66 +468,8 @@
 
 
 
- at registerElement
-class PubSubXMPPURIProperty (WebDAVTextElement):
-    """
-    A calendar home property to indicate the pubsub XMPP URI to subscribe to
-    for notifications.
-    """
-    namespace = calendarserver_namespace
-    name = "xmpp-uri"
-    protected = True
-    hidden = True
 
 
-
- at registerElement
-class PubSubHeartbeatProperty (WebDAVElement):
-    """
-    A calendar home property to indicate the pubsub XMPP URI to subscribe to
-    for server heartbeats.
-    """
-    namespace = calendarserver_namespace
-    name = "xmpp-heartbeat"
-    protected = True
-    hidden = True
-    allowed_children = {
-        (calendarserver_namespace, "xmpp-heartbeat-uri")     : (1, 1),
-        (calendarserver_namespace, "xmpp-heartbeat-minutes") : (1, 1),
-    }
-
-
-
- at registerElement
-class PubSubHeartbeatURIProperty (WebDAVTextElement):
-    namespace = calendarserver_namespace
-    name = "xmpp-heartbeat-uri"
-    protected = True
-    hidden = True
-
-
-
- at registerElement
-class PubSubHeartbeatMinutesProperty (WebDAVTextElement):
-    namespace = calendarserver_namespace
-    name = "xmpp-heartbeat-minutes"
-    protected = True
-    hidden = True
-
-
-
- at registerElement
-class PubSubXMPPServerProperty (WebDAVTextElement):
-    """
-    A calendar home property to indicate the pubsub XMPP hostname to
-    contact for notifications.
-    """
-    namespace = calendarserver_namespace
-    name = "xmpp-server"
-    protected = True
-    hidden = True
-
-
 PrincipalPropertySearch.allowed_children[(calendarserver_namespace, "limit")] = (0, 1)
 PrincipalPropertySearch.allowed_attributes["type"] = False
 Match.allowed_attributes = {

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/aggregate.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/aggregate.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/aggregate.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -34,7 +34,12 @@
 
 class AggregateDirectoryService(DirectoryService):
     """
-    L{IDirectoryService} implementation which aggregates multiple directory services.
+    L{IDirectoryService} implementation which aggregates multiple directory
+    services.
+
+    @ivar _recordTypes: A map of record types to L{IDirectoryService}s.
+    @type _recordTypes: L{dict} mapping L{bytes} to L{IDirectoryService}
+        provider.
     """
     baseGUID = "06FB225F-39E7-4D34-B1D1-29925F5E619B"
 
@@ -182,24 +187,25 @@
     @inlineCallbacks
     def recordsMatchingTokens(self, tokens, context=None):
         """
+        Combine the results from the sub-services.
+
+        Each token is searched for within each record's full name and email
+        address; if each token is found within a record that record is returned
+        in the results.
+
+        If context is None, all record types are considered.  If context is
+        "location", only locations are considered.  If context is "attendee",
+        only users, groups, and resources are considered.
+
         @param tokens: The tokens to search on
         @type tokens: C{list} of C{str} (utf-8 bytes)
-        @param context: An indication of what the end user is searching
-            for; "attendee", "location", or None
+
+        @param context: An indication of what the end user is searching for;
+            "attendee", "location", or None
         @type context: C{str}
-        @return: a deferred sequence of L{IDirectoryRecord}s which
-            match the given tokens and optional context.
 
-        Each token is searched for within each record's full name and
-        email address; if each token is found within a record that
-        record is returned in the results.
-
-        If context is None, all record types are considered.  If
-        context is "location", only locations are considered.  If
-        context is "attendee", only users, groups, and resources
-        are considered.
-
-        Combine the results from the sub-services.
+        @return: a deferred sequence of L{IDirectoryRecord}s which match the
+            given tokens and optional context.
         """
 
         services = set(self._recordTypes.values())

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/digest.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/digest.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/digest.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,3 +1,4 @@
+# -*- test-case-name: twistedcaldav.directory.test.test_digest -*-
 ##
 # Copyright (c) 2006-2013 Apple Inc. All rights reserved.
 #
@@ -44,14 +45,14 @@
     An interface to a digest credentials database that is used to hold per-client digest credentials so that fast
     re-authentication can be done with replay attacks etc prevented.
     """
-    
+
     def has_key(self, key):
         """
         See whether the matching key exists.
 
         @param key:    the key to check.
         @type key:     C{str}.
-        
+
         @return:       C{True} if the key exists, C{False} otherwise.
         """
         pass
@@ -66,7 +67,7 @@
         @type value:       any.
         """
         pass
-    
+
     def get(self, key):
         """
         Validate client supplied credentials by comparing with the cached values. If valid, store the new
@@ -78,7 +79,7 @@
         @return:       the value for the corresponding key, or C{None} if the key is not found.
         """
         pass
-    
+
     def delete(self, key):
         """
         Remove the record associated with the supplied key.
@@ -99,7 +100,7 @@
             namespace=namespace,
             pickle=True,
         )
-    
+
     def has_key(self, key):
         """
         See IDigestCredentialsDatabase.
@@ -143,11 +144,12 @@
         self.qop = qop
         self.db = DigestCredentialsMemcache(namespace)
 
+
     @inlineCallbacks
     def getChallenge(self, peer):
         """
-        Generate the challenge for use in the WWW-Authenticate header
-        Do the default behavior but then strip out any 'qop' from the challenge fields
+        Generate the challenge for use in the WWW-Authenticate header Do the
+        default behavior but then strip out any 'qop' from the challenge fields
         if no qop was specified.
 
         @param peer: The L{IAddress} of the requesting client.
@@ -156,8 +158,10 @@
             header.
         """
 
-        c = self.generateNonce()
-        
+        challenge = yield (super(QopDigestCredentialFactory, self)
+                           .getChallenge(peer))
+        c = challenge['nonce']
+
         # Make sure it is not a duplicate
         result = (yield self.db.has_key(c))
         if result:
@@ -166,24 +170,18 @@
         # The database record is a tuple of (nonce-count, timestamp)
         yield self.db.set(c, (0, time.time()))
 
-        challenge = {
-            'nonce': c,
-            'qop': 'auth',
-            'algorithm': self.algorithm,
-            'realm': self.realm,
-        }
-
         if self.qop:
             challenge['qop'] = self.qop
         else:
             del challenge['qop']
-        
+
         # If stale was marked when decoding this request's Authorization header, add that to the challenge
         if hasattr(peer, 'stale') and peer.stale:
             challenge['stale'] = 'true'
 
         returnValue(challenge)
 
+
     @inlineCallbacks
     def decode(self, response, request):
         """
@@ -208,17 +206,17 @@
         """
 
         response = ' '.join(response.splitlines())
-        
+
         try:
             parts = split(tokenize((response,), foldCase=False), Token(","))
-    
+
             auth = {}
-    
+
             for (k, v) in [parseKeyValue(p) for p in parts]:
                 auth[k.strip()] = v.strip()
         except ValueError:
             raise error.LoginFailed('Invalid response.')
-            
+
         username = auth.get('username')
         if not username:
             raise error.LoginFailed('Invalid response, no username given.')
@@ -235,10 +233,9 @@
                 originalMethod = None
 
             credentials = DigestedCredentials(username,
-                                              request.method,
-                                              self.realm,
-                                              auth,
-                                              originalMethod)
+                                              originalMethod or request.method,
+                                              self._real.authenticationRealm,
+                                              auth)
 
             if not self.qop and credentials.fields.has_key('qop'):
                 del credentials.fields['qop']
@@ -257,7 +254,7 @@
         @type auth:         C{dict}
         @param request:     the request being processed.
         @type request:      L{twext.web2.server.Request}
-        
+
         @return:            C{True} if validated.
         @raise LoginFailed: if validation fails.
         """
@@ -279,13 +276,13 @@
             if nonce_count is None:
                 yield self._invalidate(nonce)
                 raise error.LoginFailed('nonce-count is required when qop is specified')
-                
+
             # Next check the nonce-count is one greater than the previous one and update it in the DB
             try:
                 nonce_count = int(nonce_count, 16)
             except ValueError:
                 yield self._invalidate(nonce)
-                raise error.LoginFailed('nonce-count is not a valid hex string: %s' % (auth.get('nonce-count'),))            
+                raise error.LoginFailed('nonce-count is not a valid hex string: %s' % (auth.get('nonce-count'),))
             if nonce_count != db_nonce_count + 1:
                 yield self._invalidate(nonce)
                 raise error.LoginFailed('nonce-count value out of sequence: %s should be one more than %s' % (nonce_count, db_nonce_count,))
@@ -295,8 +292,8 @@
             # i.e. we can't allow a qop auth then a non-qop auth with the same nonce
             if db_nonce_count != 0:
                 yield self._invalidate(nonce)
-                raise error.LoginFailed('nonce-count was sent with this nonce: %s' % (nonce,))                
-        
+                raise error.LoginFailed('nonce-count was sent with this nonce: %s' % (nonce,))
+
         # Now check timestamp
         if db_timestamp + DigestCredentialFactory.CHALLENGE_LIFETIME_SECS <= time.time():
             yield self._invalidate(nonce)
@@ -305,7 +302,7 @@
             raise error.LoginFailed('Digest credentials expired')
 
         returnValue(True)
-    
+
     def _invalidate(self, nonce):
         """
         Invalidate cached credentials for the specified nonce value.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_digest.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_digest.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_digest.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -15,7 +15,6 @@
 ##
 
 import sys
-import time
 from hashlib import md5
 
 from twisted.cred import error
@@ -30,6 +29,8 @@
 from twistedcaldav.test.util import TestCase
 from twistedcaldav.config import config
 from twext.web2.auth.digest import DigestCredentialFactory
+from twext.web2.test.test_httpauth import makeDigestDeterministic
+from twext.web2.test.test_httpauth import FAKE_STATIC_NONCE
 
 class FakeDigestCredentialFactory(QopDigestCredentialFactory):
     """
@@ -39,12 +40,8 @@
 
     def __init__(self, *args, **kwargs):
         super(FakeDigestCredentialFactory, self).__init__(*args, **kwargs)
+        makeDigestDeterministic(self._real)
 
-    def generateNonce(self):
-        """
-        Generate a static nonce
-        """
-        return '178288758716122392881254770685'
 
 
 clientAddress = address.IPv4Address('TCP', '127.0.0.1', 80)
@@ -432,7 +429,7 @@
                 request
             )
 
-            factory._invalidate(factory.generateNonce())
+            factory._invalidate(FAKE_STATIC_NONCE)
             response = (yield UnauthorizedResponse.makeResponse(
                 {"Digest":factory},
                 request.remoteAddr
@@ -525,10 +522,13 @@
         """
         Test that we can decode a valid response to our challenge
         """
+        theTime = 0
+        class newtime(object):
+            def time(self):
+                return theTime
+        from twistedcaldav.directory import digest
+        self.patch(digest, "time", newtime())
 
-        oldTime = DigestCredentialFactory.CHALLENGE_LIFETIME_SECS
-        DigestCredentialFactory.CHALLENGE_LIFETIME_SECS = 2
-
         for ctr, factory in enumerate(self.credentialFactories):
             challenge = (yield factory.getChallenge(clientAddress))
     
@@ -540,7 +540,7 @@
             creds = (yield factory.decode(clientResponse, _trivial_GET()))
             self.failUnless(creds.checkPassword('password'))
             
-            time.sleep(3)
+            theTime += DigestCredentialFactory.CHALLENGE_LIFETIME_SECS + 1
             request = _trivial_GET()
             try:
                 clientResponse = authRequest2[ctr] % (
@@ -556,7 +556,6 @@
             challenge = (yield factory.getChallenge(request.remoteAddr))
             self.assertTrue(challenge.get("stale") == "true")
             
-        DigestCredentialFactory.CHALLENGE_LIFETIME_SECS = oldTime
 
 def _trivial_GET():
     return SimpleRequest(None, 'GET', '/')

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_directory.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_directory.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_directory.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -40,6 +40,8 @@
         return 0
     return 1
 
+
+
 class SACLTests(TestCase):
 
     def setUp(self):
@@ -59,10 +61,10 @@
         """
 
         data = [
-            ("amanda",  True,  True,),
-            ("betty",   True,  False,),
+            ("amanda", True, True,),
+            ("betty", True, False,),
             ("carlene", False, True,),
-            ("daniel",  False, False,),
+            ("daniel", False, False,),
         ]
         for username, cal, ab in data:
             record = DirectoryRecord(self.service, "users", None, (username,),
@@ -71,19 +73,21 @@
             self.assertEquals(record.enabledForCalendaring, cal)
             self.assertEquals(record.enabledForAddressBooks, ab)
 
+
+
 class GroupMembershipTests (TestCase):
 
     @inlineCallbacks
     def setUp(self):
         super(GroupMembershipTests, self).setUp()
 
-        self.directoryService = XMLDirectoryService(
+        self.directoryFixture.addDirectoryService(XMLDirectoryService(
             {
                 'xmlFile' : xmlFile,
                 'augmentService' :
                     augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
             }
-        )
+        ))
         calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
 
         # Set up a principals hierarchy for each service we're testing with
@@ -99,20 +103,24 @@
 
         yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
 
+
     def tearDown(self):
         """ Empty the proxy db between tests """
-        return calendaruserproxy.ProxyDBService.clean()
+        return calendaruserproxy.ProxyDBService.clean() #@UndefinedVariable
 
+
     def _getPrincipalByShortName(self, type, name):
         provisioningResource = self.principalRootResources[self.directoryService.__class__.__name__]
         return provisioningResource.principalForShortName(type, name)
 
+
     def _updateMethod(self):
         """
         Update a counter in the following test
         """
         self.count += 1
 
+
     @inlineCallbacks
     def test_groupMembershipCacherService(self):
         """
@@ -153,7 +161,6 @@
         self.assertFalse(service.updateAwaiting)
 
 
-
     def test_expandedMembers(self):
         """
         Make sure expandedMembers( ) returns a complete, flattened set of
@@ -193,8 +200,6 @@
         self.assertEquals(members, set())
 
 
-
-
     @inlineCallbacks
     def test_groupMembershipCacheUpdater(self):
         """
@@ -569,6 +574,7 @@
                 groups,
             )
 
+
     def test_diffAssignments(self):
         """
         Ensure external proxy assignment diffing works
@@ -577,80 +583,79 @@
         self.assertEquals(
             (
                 # changed
-                [ ],
+                [],
                 # removed
-                [ ],
+                [],
             ),
             diffAssignments(
                 # old
-                [ ],
+                [],
                 # new
-                [ ],
+                [],
             )
         )
 
         self.assertEquals(
             (
                 # changed
-                [ ],
+                [],
                 # removed
-                [ ],
+                [],
             ),
             diffAssignments(
                 # old
-                [ ("B", set(["3"])), ("A", set(["1", "2"])), ],
+                [("B", set(["3"])), ("A", set(["1", "2"])), ],
                 # new
-                [ ("A", set(["1", "2"])), ("B", set(["3"])), ],
+                [("A", set(["1", "2"])), ("B", set(["3"])), ],
             )
         )
 
         self.assertEquals(
             (
                 # changed
-                [ ("A", set(["1", "2"])), ("B", set(["3"])), ],
+                [("A", set(["1", "2"])), ("B", set(["3"])), ],
                 # removed
-                [ ],
+                [],
             ),
             diffAssignments(
                 # old
-                [ ],
+                [],
                 # new
-                [ ("A", set(["1", "2"])), ("B", set(["3"])), ],
+                [("A", set(["1", "2"])), ("B", set(["3"])), ],
             )
         )
 
         self.assertEquals(
             (
                 # changed
-                [ ],
+                [],
                 # removed
-                [ "A", "B" ],
+                ["A", "B"],
             ),
             diffAssignments(
                 # old
-                [ ("A", set(["1", "2"])), ("B", set(["3"])), ],
+                [("A", set(["1", "2"])), ("B", set(["3"])), ],
                 # new
-                [ ],
+                [],
             )
         )
 
         self.assertEquals(
             (
                 # changed
-                [ ("A", set(["2"])), ("C", set(["4", "5"])), ("D", set(["6"])), ],
+                [("A", set(["2"])), ("C", set(["4", "5"])), ("D", set(["6"])), ],
                 # removed
-                [ "B" ],
+                ["B"],
             ),
             diffAssignments(
                 # old
-                [ ("A", set(["1", "2"])), ("B", set(["3"])), ("C", set(["4"])), ],
+                [("A", set(["1", "2"])), ("B", set(["3"])), ("C", set(["4"])), ],
                 # new
-                [ ("D", set(["6"])), ("C", set(["4", "5"])), ("A", set(["2"])), ],
+                [("D", set(["6"])), ("C", set(["4", "5"])), ("A", set(["2"])), ],
             )
         )
 
 
-
     @inlineCallbacks
     def test_groupMembershipCacheSnapshot(self):
         """
@@ -757,6 +762,7 @@
             }
         )
 
+
     def test_autoAcceptMembers(self):
         """
         autoAcceptMembers( ) returns an empty list if no autoAcceptGroup is
@@ -765,7 +771,7 @@
 
         # No auto-accept-group for "orion" in augments.xml
         orion = self.directoryService.recordWithGUID("orion")
-        self.assertEquals( orion.autoAcceptMembers(), [])
+        self.assertEquals(orion.autoAcceptMembers(), [])
 
         # "both_coasts" group assigned to "apollo" in augments.xml
         apollo = self.directoryService.recordWithGUID("apollo")
@@ -781,19 +787,21 @@
             ])
         )
 
+
+
 class RecordsMatchingTokensTests(TestCase):
 
     @inlineCallbacks
     def setUp(self):
         super(RecordsMatchingTokensTests, self).setUp()
 
-        self.directoryService = XMLDirectoryService(
+        self.directoryFixture.addDirectoryService(XMLDirectoryService(
             {
                 'xmlFile' : xmlFile,
                 'augmentService' :
                     augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
             }
-        )
+        ))
         calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
 
         # Set up a principals hierarchy for each service we're testing with
@@ -809,10 +817,12 @@
 
         yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
 
+
     def tearDown(self):
         """ Empty the proxy db between tests """
-        return calendaruserproxy.ProxyDBService.clean()
+        return calendaruserproxy.ProxyDBService.clean() #@UndefinedVariable
 
+
     @inlineCallbacks
     def test_recordsMatchingTokens(self):
         """
@@ -836,7 +846,7 @@
         self.assertEquals(records[0].shortNames[0], "apollo")
 
 
- 
+
 class GUIDTests(TestCase):
 
     def setUp(self):
@@ -844,6 +854,7 @@
         self.service.setRealm("test")
         self.service.baseGUID = "0E8E6EC2-8E52-4FF3-8F62-6F398B08A498"
 
+
     def test_normalizeUUID(self):
 
         # Ensure that record.guid automatically gets normalized to
@@ -882,6 +893,8 @@
                 shortNames=("testing",))
             self.assertEquals(expected, record.guid)
 
+
+
 class DirectoryRecordTests(TestCase):
     """
     Test L{DirectoryRecord} apis.
@@ -892,6 +905,7 @@
         self.service.setRealm("test")
         self.service.baseGUID = "0E8E6EC2-8E52-4FF3-8F62-6F398B08A498"
 
+
     def test_cacheToken(self):
         """
         Test that DirectoryRecord.cacheToken is different for different records, and its value changes

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -19,7 +19,7 @@
 
 from twistedcaldav.directory.directory import DirectoryService
 from twistedcaldav.test.util import xmlFile, augmentsFile, proxiesFile
-from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource,\
+from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource, \
     DirectoryPrincipalResource
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 
@@ -37,13 +37,13 @@
     def setUp(self):
         super(ProxyPrincipals, self).setUp()
 
-        self.directoryService = XMLDirectoryService(
+        self.directoryFixture.addDirectoryService(XMLDirectoryService(
             {
                 'xmlFile' : xmlFile,
                 'augmentService' :
                     augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
             }
-        )
+        ))
         calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
 
         # Set up a principals hierarchy for each service we're testing with
@@ -59,14 +59,17 @@
 
         yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
 
+
     def tearDown(self):
         """ Empty the proxy db between tests """
-        return calendaruserproxy.ProxyDBService.clean()
+        return calendaruserproxy.ProxyDBService.clean() #@UndefinedVariable
 
+
     def _getPrincipalByShortName(self, type, name):
         provisioningResource = self.principalRootResources[self.directoryService.__class__.__name__]
         return provisioningResource.principalForShortName(type, name)
 
+
     def _groupMembersTest(self, recordType, recordName, subPrincipalName, expectedMembers):
         def gotMembers(members):
             memberNames = set([p.displayName() for p in members])
@@ -80,6 +83,7 @@
         d.addCallback(gotMembers)
         return d
 
+
     def _groupMembershipsTest(self, recordType, recordName, subPrincipalName, expectedMemberships):
         def gotMemberships(memberships):
             uids = set([p.principalUID() for p in memberships])
@@ -92,7 +96,8 @@
         d = principal.groupMemberships()
         d.addCallback(gotMemberships)
         return d
-    
+
+
     @inlineCallbacks
     def _addProxy(self, principal, subPrincipalName, proxyPrincipal):
 
@@ -104,9 +109,10 @@
         if isinstance(proxyPrincipal, tuple):
             proxyPrincipal = self._getPrincipalByShortName(proxyPrincipal[0], proxyPrincipal[1])
         members.add(proxyPrincipal)
-        
+
         yield principal.setGroupMemberSetPrincipals(members)
 
+
     @inlineCallbacks
     def _removeProxy(self, recordType, recordName, subPrincipalName, proxyRecordType, proxyRecordName):
 
@@ -119,9 +125,10 @@
             if p.principalUID() == proxyPrincipal.principalUID():
                 members.remove(p)
                 break
-        
+
         yield principal.setGroupMemberSetPrincipals(members)
 
+
     @inlineCallbacks
     def _clearProxy(self, principal, subPrincipalName):
 
@@ -130,13 +137,15 @@
         principal = principal.getChild(subPrincipalName)
         yield principal.setGroupMemberSetPrincipals(set())
 
+
     @inlineCallbacks
     def _proxyForTest(self, recordType, recordName, expectedProxies, read_write):
         principal = self._getPrincipalByShortName(recordType, recordName)
         proxies = (yield principal.proxyFor(read_write))
-        proxies = sorted([principal.displayName() for principal in proxies])
+        proxies = sorted([_principal.displayName() for _principal in proxies])
         self.assertEquals(proxies, sorted(expectedProxies))
 
+
     @inlineCallbacks
     def test_multipleProxyAssignmentsAtOnce(self):
         yield self._proxyForTest(
@@ -150,6 +159,7 @@
             True
         )
 
+
     def test_groupMembersRegular(self):
         """
         DirectoryPrincipalResource.expandedGroupMembers()
@@ -159,6 +169,7 @@
             ("Chris Lecroy", "David Reid", "Wilfredo Sanchez", "West Coast", "East Coast", "Cyrus Daboo",),
         )
 
+
     def test_groupMembersRecursive(self):
         """
         DirectoryPrincipalResource.expandedGroupMembers()
@@ -168,6 +179,7 @@
             ("Wilfredo Sanchez", "Recursive2 Coasts", "Cyrus Daboo",),
         )
 
+
     def test_groupMembersProxySingleUser(self):
         """
         DirectoryPrincipalResource.expandedGroupMembers()
@@ -177,6 +189,7 @@
             ("Wilfredo Sanchez",),
         )
 
+
     def test_groupMembersProxySingleGroup(self):
         """
         DirectoryPrincipalResource.expandedGroupMembers()
@@ -186,6 +199,7 @@
             ("Chris Lecroy", "David Reid", "Wilfredo Sanchez", "West Coast",),
         )
 
+
     def test_groupMembersProxySingleGroupWithNestedGroups(self):
         """
         DirectoryPrincipalResource.expandedGroupMembers()
@@ -195,6 +209,7 @@
             ("Chris Lecroy", "David Reid", "Wilfredo Sanchez", "West Coast", "East Coast", "Cyrus Daboo", "Both Coasts",),
         )
 
+
     def test_groupMembersProxySingleGroupWithNestedRecursiveGroups(self):
         """
         DirectoryPrincipalResource.expandedGroupMembers()
@@ -204,6 +219,7 @@
             ("Wilfredo Sanchez", "Cyrus Daboo", "Recursive1 Coasts", "Recursive2 Coasts",),
         )
 
+
     def test_groupMembersProxySingleGroupWithNonCalendarGroup(self):
         """
         DirectoryPrincipalResource.expandedGroupMembers()
@@ -222,6 +238,7 @@
 
         return DeferredList(ds)
 
+
     def test_groupMembersProxyMissingUser(self):
         """
         DirectoryPrincipalResource.expandedGroupMembers()
@@ -245,6 +262,7 @@
         d.addCallback(check)
         return d
 
+
     def test_groupMembershipsMissingUser(self):
         """
         DirectoryPrincipalResource.expandedGroupMembers()
@@ -269,6 +287,7 @@
         d.addCallback(check)
         return d
 
+
     @inlineCallbacks
     def test_setGroupMemberSet(self):
         class StubMemberDB(object):
@@ -282,7 +301,6 @@
             def getMembers(self, uid):
                 return succeed(self.members)
 
-
         user = self._getPrincipalByShortName(self.directoryService.recordType_users,
                                            "cdaboo")
 
@@ -305,6 +323,7 @@
             set(["5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1",
                  "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0"]))
 
+
     @inlineCallbacks
     def test_setGroupMemberSetNotifiesPrincipalCaches(self):
         class StubCacheNotifier(object):
@@ -336,14 +355,16 @@
         finally:
             DirectoryPrincipalResource.cacheNotifierFactory = oldCacheNotifier
 
+
     def test_proxyFor(self):
 
         return self._proxyForTest(
-            DirectoryService.recordType_users, "wsanchez", 
-            ("Mercury Seven", "Gemini Twelve", "Apollo Eleven", "Orion", ),
+            DirectoryService.recordType_users, "wsanchez",
+            ("Mercury Seven", "Gemini Twelve", "Apollo Eleven", "Orion",),
             True
         )
 
+
     @inlineCallbacks
     def test_proxyForDuplicates(self):
 
@@ -354,22 +375,24 @@
         )
 
         yield self._proxyForTest(
-            DirectoryService.recordType_users, "wsanchez", 
-            ("Mercury Seven", "Gemini Twelve", "Apollo Eleven", "Orion", ),
+            DirectoryService.recordType_users, "wsanchez",
+            ("Mercury Seven", "Gemini Twelve", "Apollo Eleven", "Orion",),
             True
         )
 
+
     def test_readOnlyProxyFor(self):
 
         return self._proxyForTest(
-            DirectoryService.recordType_users, "wsanchez", 
-            ("Non-calendar proxy", ),
+            DirectoryService.recordType_users, "wsanchez",
+            ("Non-calendar proxy",),
             False
         )
 
+
     @inlineCallbacks
     def test_UserProxy(self):
-        
+
         for proxyType in ("calendar-proxy-read", "calendar-proxy-write"):
 
             yield self._addProxy(
@@ -377,31 +400,31 @@
                 proxyType,
                 (DirectoryService.recordType_users, "cdaboo",),
             )
-    
+
             yield self._groupMembersTest(
                 DirectoryService.recordType_users, "wsanchez",
                 proxyType,
                 ("Cyrus Daboo",),
             )
-            
+
             yield self._addProxy(
                 (DirectoryService.recordType_users, "wsanchez",),
                 proxyType,
                 (DirectoryService.recordType_users, "lecroy",),
             )
-    
+
             yield self._groupMembersTest(
                 DirectoryService.recordType_users, "wsanchez",
                 proxyType,
                 ("Cyrus Daboo", "Chris Lecroy",),
             )
-    
+
             yield self._removeProxy(
                 DirectoryService.recordType_users, "wsanchez",
                 proxyType,
                 DirectoryService.recordType_users, "cdaboo",
             )
-    
+
             yield self._groupMembersTest(
                 DirectoryService.recordType_users, "wsanchez",
                 proxyType,
@@ -421,7 +444,7 @@
 
         record = self.directoryService.recordWithGUID("320B73A1-46E2-4180-9563-782DFDBE1F63")
         provisioningResource = self.principalRootResources[self.directoryService.__class__.__name__]
-        principal =  provisioningResource.principalForRecord(record)
+        principal = provisioningResource.principalForRecord(record)
         proxyPrincipal = provisioningResource.principalForShortName(recordType,
             "wsanchez")
 
@@ -438,7 +461,7 @@
         delegated-to directly
         """
         self.assertEquals(
-            set((yield calendaruserproxy.ProxyDBService.getAllMembers())),
+            set((yield calendaruserproxy.ProxyDBService.getAllMembers())), #@UndefinedVariable
             set([u'6423F94A-6B76-4A3A-815B-D52CFD77935D', u'8A985493-EE2C-4665-94CF-4DFEA3A89500', u'9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD2', u'both_coasts', u'left_coast', u'non_calendar_group', u'recursive1_coasts', u'recursive2_coasts', u'EC465590-E9E9-4746-ACE8-6C756A49FE4D'])
         )
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/util.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/util.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/test/util.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -342,7 +342,6 @@
                         "cnonce": "phlegm",
                         "nc": None,
                     },
-                    None,
                 )
 
                 if good:

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/wiki.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/wiki.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/wiki.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -131,6 +131,42 @@
     Ask the wiki server we're paired with what level of access the userID has
     for the given wikiID.  Possible values are "read", "write", and "admin"
     (which we treat as "write").
+
+    @param userID: the GUID (UUID) of the user's directory record.
+    @type userID: L{bytes} (UTF-8)
+
+    @param wikiID: the short name of the wiki principal's synthetic directory
+        record.  (See L{WikiDirectoryService}).
+    @type wikiID: L{bytes} (UTF-8)
+
+    @return: A string indicating the level of access that the given user has to
+        the given wiki.  Possible values are:
+
+        1. C{b"no-access"} for read-only access
+
+        2. C{b"no-access"} for read/write access
+
+        3. C{b"no-access"} for administrative access (which, for calendaring
+           purposes, should be equialent to read/write)
+
+        4. C{b"no-access"} for a user who is not allowed to see the wiki at
+           all.
+
+    @rtype: L{bytes}
+
+    @raise: L{HTTPError} indicating that there is a problem requesting
+        permission information.  This may be raised with a few different status
+        codes, each indicating a different problem:
+
+        1. L{responsecode.FORBIDDEN}: The user represented by C{userID} did not
+           exist.
+
+        2. L{responsecode.NOT_FOUND}: The wiki represented by C{wikiID} did not
+           exist.
+
+        3. L{responsecode.SERVICE_UNAVAILABLE}: The service that we are
+           checking permissions with is currently offline or responding with an
+           unknown fault.
     """
     wikiConfig = config.Authentication.Wiki
     if method is None:

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/instance.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/instance.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/instance.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -27,41 +27,50 @@
 from pycalendar.timezone import PyCalendarTimezone
 
 class TooManyInstancesError(Exception):
+
     def __init__(self):
         Exception.__init__(self)
         self.max_allowed = config.MaxAllowedInstances
 
+
     def __repr__(self):
         return "<%s max:%s>" % (self.__class__.__name__, self.max_allowed)
 
+
+
 class InvalidOverriddenInstanceError(Exception):
+
     def __init__(self, rid):
         Exception.__init__(self)
         self.rid = rid
 
+
     def __repr__(self):
         return "<%s invalid:%s>" % (self.__class__.__name__, self.rid)
 
+
+
 class Instance(object):
-    
-    def __init__(self, component, start = None, end = None, rid = None, overridden = False, future = False):
+
+    def __init__(self, component, start=None, end=None, rid=None, overridden=False, future=False):
         self.component = component
         self.start = component.getStartDateUTC() if start is None else start
         self.end = component.getEndDateUTC() if end is None else end
         self.rid = self.start if rid is None else rid
         self.overridden = overridden
         self.future = future
-        
+
+
     def getAlarmTriggers(self):
         """
         Get the set of alarm triggers for this instance.
         @return: a set containing the UTC datetime's of each trigger in each alarm
         """
         triggers = set()
-        
+
         for alarm in [x for x in self.component.subcomponents() if x.name() == "VALARM"]:
-            (trigger, related, repeat, duration)  = alarm.getTriggerDetails()
-            
+            (trigger, related, repeat, duration) = alarm.getTriggerDetails()
+
             # Handle relative vs absolute triggers
             if isinstance(trigger, PyCalendarDateTime):
                 # Absolute trigger
@@ -70,36 +79,45 @@
                 # Relative trigger
                 start = (self.start if related else self.end) + trigger
             triggers.add(start)
-            
+
             # Handle repeats
             if repeat > 0:
                 tstart = start.duplicate()
-                for _ignore in xrange(1, repeat+1):
+                for _ignore in xrange(1, repeat + 1):
                     tstart += duration
                     triggers.add(tstart)
-        
+
         return triggers
-    
+
+
     def isMasterInstance(self):
         return not self.overridden and self.start == self.component.getStartDateUTC()
 
+
+
 class InstanceList(object):
-    
+
     def __init__(self, ignoreInvalidInstances=False, normalizeFunction=normalizeForIndex):
         self.instances = {}
         self.limit = None
         self.lowerLimit = None
         self.ignoreInvalidInstances = ignoreInvalidInstances
         self.normalizeFunction = normalizeFunction
-        
+
+        self.adjustedLowerLimit = None
+        self.adjustedUpperLimit = None
+
+
     def __iter__(self):
         # Return keys in sorted order via iterator
         for i in sorted(self.instances.keys()):
             yield i
 
+
     def __getitem__(self, key):
         return self.instances[key]
 
+
     def expandTimeRanges(self, componentSet, limit, lowerLimit=None):
         """
         Expand the set of recurrence instances up to the specified date limit.
@@ -111,7 +129,7 @@
                 and type, forming a proper recurring set.
         @param limit: L{PyCalendarDateTime} value representing the end of the expansion.
         """
-        
+
         # Look at each component type
         got_master = False
         overrides = []
@@ -142,7 +160,7 @@
                     # AVAILABLE components are just like VEVENT components
                     self._addMasterEventComponent(component, lowerLimit, limit)
                     got_master = True
-            
+
         for component in overrides:
             if component.name() == "VEVENT":
                 self._addOverrideEventComponent(component, lowerLimit, limit, got_master)
@@ -155,6 +173,7 @@
                 # AVAILABLE components are just like VEVENT components
                 self._addOverrideEventComponent(component, lowerLimit, limit, got_master)
 
+
     def addInstance(self, instance):
         """
         Add the supplied instance to the map.
@@ -162,11 +181,34 @@
         """
 
         self.instances[str(instance.rid)] = instance
-        
+
         # Check for too many instances
         if config.MaxAllowedInstances and len(self.instances) > config.MaxAllowedInstances:
             raise TooManyInstancesError()
 
+
+    def _setupLimits(self, dt, lowerLimit, upperLimit):
+        """
+        Change the limits to account for testing against DATE only values. The lower limit
+        is simply truncated to its date value. The upper limit is truncated to one day past
+        the date value.
+        """
+
+        if self.adjustedUpperLimit is None:
+            if dt.isDateOnly():
+                if lowerLimit:
+                    self.adjustedLowerLimit = lowerLimit.duplicate()
+                    self.adjustedLowerLimit.setDateOnly(True)
+                self.adjustedUpperLimit = upperLimit.duplicate()
+                self.adjustedUpperLimit.setDateOnly(True)
+                self.adjustedUpperLimit.offsetDay(1)
+            else:
+                self.adjustedLowerLimit = lowerLimit
+                self.adjustedUpperLimit = upperLimit
+
+        return (self.adjustedLowerLimit, self.adjustedUpperLimit,)
+
+
     def _getMasterEventDetails(self, component):
         """
         Logic here comes from RFC4791 Section 9.9
@@ -189,41 +231,46 @@
             end = start + duration
         else:
             duration = differenceDateTime(start, end)
-        
+
         return (rulestart, start, end, duration,)
 
-    def _addMasterEventComponent(self, component, lowerLimit, upperlimit):
+
+    def _addMasterEventComponent(self, component, lowerLimit, upperLimit):
         """
         Add the specified master VEVENT Component to the instance list, expanding it
         within the supplied time range.
         @param component: the Component to expand
         @param limit: the end L{PyCalendarDateTime} for expansion
         """
-        
+
         details = self._getMasterEventDetails(component)
         if details is None:
             return
         rulestart, start, end, duration = details
 
-        self._addMasterComponent(component, lowerLimit, upperlimit, rulestart, start, end, duration)
+        lowerLimit, upperLimit = self._setupLimits(start, lowerLimit, upperLimit)
+        self._addMasterComponent(component, lowerLimit, upperLimit, rulestart, start, end, duration)
 
-    def _addOverrideEventComponent(self, component, lowerLimit, upperlimit, got_master):
+
+    def _addOverrideEventComponent(self, component, lowerLimit, upperLimit, got_master):
         """
-        Add the specified overridden VEVENT Component to the instance list, replacing 
+        Add the specified overridden VEVENT Component to the instance list, replacing
         the one generated by the master component.
         @param component: the overridden Component.
         @param got_master: whether a master component has already been expanded.
         """
-        
+
         #TODO: This does not take into account THISANDPRIOR - only THISANDFUTURE
-        
+
         details = self._getMasterEventDetails(component)
         if details is None:
             return
         _ignore_rulestart, start, end, _ignore_duration = details
 
-        self._addOverrideComponent(component, lowerLimit, upperlimit, start, end, got_master)
+        lowerLimit, upperLimit = self._setupLimits(start, lowerLimit, upperLimit)
+        self._addOverrideComponent(component, lowerLimit, upperLimit, start, end, got_master)
 
+
     def _getMasterToDoDetails(self, component):
         """
         Logic here comes from RFC4791 Section 9.9
@@ -243,12 +290,12 @@
                 end = dtdue
             else:
                 end = dtstart
-        
+
         # DUE case
         elif dtdue is not None:
             rulestart = component.propertyValue("DUE")
             start = end = dtdue
-        
+
         # Fall back to COMPLETED or CREATED - cannot be recurring
         else:
             rulestart = None
@@ -269,7 +316,8 @@
 
         return (rulestart, start, end, duration,)
 
-    def _addMasterToDoComponent(self, component, lowerLimit, upperlimit):
+
+    def _addMasterToDoComponent(self, component, lowerLimit, upperLimit):
         """
         Add the specified master VTODO Component to the instance list, expanding it
         within the supplied time range.
@@ -281,27 +329,31 @@
             return
         rulestart, start, end, duration = details
 
-        self._addMasterComponent(component, lowerLimit, upperlimit, rulestart, start, end, duration)
+        lowerLimit, upperLimit = self._setupLimits(start, lowerLimit, upperLimit)
+        self._addMasterComponent(component, lowerLimit, upperLimit, rulestart, start, end, duration)
 
-    def _addOverrideToDoComponent(self, component, lowerLimit, upperlimit, got_master):
+
+    def _addOverrideToDoComponent(self, component, lowerLimit, upperLimit, got_master):
         """
-        Add the specified overridden VTODO Component to the instance list, replacing 
+        Add the specified overridden VTODO Component to the instance list, replacing
         the one generated by the master component.
         @param component: the overridden Component.
         @param got_master: whether a master component has already been expanded.
         """
-        
+
         #TODO: This does not take into account THISANDPRIOR - only THISANDFUTURE
-        
+
         details = self._getMasterToDoDetails(component)
         if details is None:
             return
         _ignore_rulestart, start, end, _ignore_duration = details
 
-        self._addOverrideComponent(component, lowerLimit, upperlimit, start, end, got_master)
+        lowerLimit, upperLimit = self._setupLimits(start, lowerLimit, upperLimit)
+        self._addOverrideComponent(component, lowerLimit, upperLimit, start, end, got_master)
 
+
     def _addMasterComponent(self, component, lowerLimit, upperlimit, rulestart, start, end, duration):
-        
+
         rrules = component.getRecurrenceSet()
         if rrules is not None and rulestart is not None:
             # Do recurrence set expansion
@@ -310,7 +362,7 @@
             # than the master DTSTART, and if we exclude those, the associated
             # overridden instances will cause an InvalidOverriddenInstance.
             limited = rrules.expand(rulestart,
-                PyCalendarPeriod(PyCalendarDateTime(1900,1,1), upperlimit), expanded)
+                PyCalendarPeriod(PyCalendarDateTime(1900, 1, 1), upperlimit), expanded)
             for startDate in expanded:
                 startDate = self.normalizeFunction(startDate)
                 endDate = startDate + duration
@@ -331,13 +383,16 @@
                     self.lowerLimit = lowerLimit
             else:
                 self.limit = upperlimit
-    
+
+        self.master_cancelled = component.propertyValue("STATUS") == "CANCELLED"
+
+
     def _addOverrideComponent(self, component, lowerLimit, upperlimit, start, end, got_master):
 
         # Get the recurrence override info
         rid = component.getRecurrenceIDUTC()
         range = component.getRange()
-        
+
         # Now add this instance, effectively overriding the one with the matching R-ID
         start = self.normalizeFunction(start)
         end = self.normalizeFunction(end)
@@ -350,48 +405,53 @@
             return
 
         # Make sure override RECURRENCE-ID is a valid instance of the master
+        cancelled = component.propertyValue("STATUS") == "CANCELLED"
         if got_master:
             if str(rid) not in self.instances and rid < upperlimit and (lowerLimit is None or rid >= lowerLimit):
-                if self.ignoreInvalidInstances:
+                if self.master_cancelled or cancelled:
+                    # Ignore invalid overrides when either the master or override is cancelled
+                    pass
+                elif self.ignoreInvalidInstances:
                     return
                 else:
                     raise InvalidOverriddenInstanceError(str(rid))
-        
+
         self.addInstance(Instance(component, start, end, rid, True, range))
-        
+
         # Handle THISANDFUTURE if present
         if range:
             # Iterate over all the instances after this one, replacing those
             # with a version based on this override component
-            
+
             # We need to account for a time shift in the overridden component by
             # applying that shift to the future instances as well
             timeShift = (start != rid)
             if timeShift:
                 offsetTime = start - rid
                 newDuration = end - start
-        
+
             # First get sorted instance keys greater than the current components R-ID
             for key in sorted(x for x in self.instances.keys() if x > str(rid)):
                 oldinstance = self.instances[key]
-                
+
                 # Do not override instance that is already overridden
                 if oldinstance.overridden:
                     continue
-                
+
                 # Determine the start/end of the new instance
                 originalStart = oldinstance.rid
                 start = oldinstance.start
                 end = oldinstance.end
-                
+
                 if timeShift:
                     start += offsetTime
                     end = start + newDuration
-                
+
                 # Now replacing existing entry with the new one
                 self.addInstance(Instance(component, start, end, originalStart, False, False))
 
-    def _addFreeBusyComponent(self, component, lowerLimit, upperlimit):
+
+    def _addFreeBusyComponent(self, component, lowerLimit, upperLimit):
         """
         Add the specified master VFREEBUSY Component to the instance list, expanding it
         within the supplied time range.
@@ -402,15 +462,18 @@
         start = component.getStartDateUTC()
         end = component.getEndDateUTC()
         if end is None and start is not None:
-            raise ValueError("VFREEBUSY component must have both DTSTART and DTEND: %r" % (component, ))
+            raise ValueError("VFREEBUSY component must have both DTSTART and DTEND: %r" % (component,))
 
+        if start:
+            lowerLimit, upperLimit = self._setupLimits(start, lowerLimit, upperLimit)
+
         # If the free busy is beyond the end of the range we want, ignore it
-        if start is not None and start >= upperlimit:
+        if start is not None and start >= upperLimit:
             return
 
         # If the free busy is before the start of the range we want, ignore it
         if lowerLimit is not None and end is not None and end < lowerLimit:
-            return            
+            return
 
         # Now look at each FREEBUSY property
         for fb in component.properties("FREEBUSY"):
@@ -419,13 +482,14 @@
             for period in fb.value():
                 # Ignore if period starts after limit
                 period = period.getValue()
-                if period.getStart() >= upperlimit:
+                if period.getStart() >= upperLimit:
                     continue
                 start = self.normalizeFunction(period.getStart())
                 end = self.normalizeFunction(period.getEnd())
                 self.addInstance(Instance(component, start, end))
 
-    def _addAvailabilityComponent(self, component, lowerLimit, upperlimit):
+
+    def _addAvailabilityComponent(self, component, lowerLimit, upperLimit):
         """
         Add the specified master VAVAILABILITY Component to the instance list, expanding it
         within the supplied time range. VAVAILABILITY components are not recurring, they have an
@@ -437,7 +501,11 @@
         """
 
         start = component.getStartDateUTC()
-        if start is not None and start >= upperlimit:
+
+        if start:
+            lowerLimit, upperLimit = self._setupLimits(start, lowerLimit, upperLimit)
+
+        if start is not None and start >= upperLimit:
             # If the availability is beyond the end of the range we want, ignore it
             return
         if start is None:
@@ -447,7 +515,7 @@
         end = component.getEndDateUTC()
         if lowerLimit is not None and end is not None and end < lowerLimit:
             # If the availability is before the start of the range we want, ignore it
-            return            
+            return
         if end is None:
             end = PyCalendarDateTime(2100, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
         end = self.normalizeFunction(end)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/memcacheprops.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/memcacheprops.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/memcacheprops.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -7,10 +7,10 @@
 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 # copies of the Software, and to permit persons to whom the Software is
 # furnished to do so, subject to the following conditions:
-# 
+#
 # The above copyright notice and this permission notice shall be included in all
 # copies or substantial portions of the Software.
-# 
+#
 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -55,6 +55,7 @@
         self.collection = collection
         self.cacheTimeout = cacheTimeout
 
+
     @classmethod
     def memcacheClient(cls, refresh=False):
         if not hasattr(MemcachePropertyCollection, "_memcacheClient"):
@@ -71,6 +72,7 @@
 
         return MemcachePropertyCollection._memcacheClient
 
+
     def propertyCache(self):
         # The property cache has this format:
         #  {
@@ -88,6 +90,7 @@
             self._propertyCache = self._loadCache()
         return self._propertyCache
 
+
     def childCache(self, child):
         path = child.fp.path
         key = self._keyForPath(path)
@@ -105,6 +108,7 @@
 
         return propertyCache, key, childCache, token
 
+
     def _keyForPath(self, path):
         key = "|".join((
             self.__class__.__name__,
@@ -112,6 +116,7 @@
         ))
         return md5(key).hexdigest()
 
+
     def _loadCache(self, childNames=None):
         if childNames is None:
             abortIfMissing = False
@@ -134,7 +139,7 @@
             for childName in childNames
         ))
 
-        result = self._split_gets_multi((key for key, name in keys),
+        result = self._split_gets_multi((key for key, _ignore_name in keys),
             client.gets_multi)
 
         if self.logger.willLogAtLevel("debug"):
@@ -145,7 +150,7 @@
             self.log_debug("Loaded keys for %schildren of %s: %s" % (
                 missing,
                 self.collection,
-                [name for key, name in keys],
+                [name for _ignore_key, name in keys],
             ))
 
         missing = tuple((
@@ -187,6 +192,7 @@
             results.update(func(subset))
         return results
 
+
     def _split_set_multi(self, values, func, time=0, chunksize=250):
         """
         Splits set_multi into chunks to avoid a memcacheclient timeout due
@@ -221,6 +227,7 @@
             self._split_set_multi(values, client.set_multi,
                 time=self.cacheTimeout)
 
+
     def _buildCache(self, childNames=None):
         if childNames is None:
             childNames = self.collection.listChildren()
@@ -247,13 +254,14 @@
 
         return cache
 
+
     def setProperty(self, child, property, uid, delete=False):
         propertyCache, key, childCache, token = self.childCache(child)
 
         if delete:
             qname = property
             qnameuid = qname + (uid,)
-            if childCache.has_key(qnameuid):
+            if qnameuid in childCache:
                 del childCache[qnameuid]
         else:
             qname = property.qname()
@@ -286,7 +294,7 @@
                 propertyCache, key, childCache, token = self.childCache(child)
 
                 if delete:
-                    if childCache.has_key(qnameuid):
+                    if qnameuid in childCache:
                         del childCache[qnameuid]
                 else:
                     childCache[qnameuid] = property
@@ -301,9 +309,11 @@
                     child
                 ))
 
+
     def deleteProperty(self, child, qname, uid):
         return self.setProperty(child, qname, uid, delete=True)
 
+
     def flushCache(self, child):
         path = child.fp.path
         key = self._keyForPath(path)
@@ -318,9 +328,11 @@
             if not result:
                 raise MemcacheError("Unable to flush cache on %s" % (child,))
 
+
     def propertyStoreForChild(self, child, childPropertyStore):
         return self.ChildPropertyStore(self, child, childPropertyStore)
 
+
     class ChildPropertyStore (LoggingMixIn):
         def __init__(self, parentPropertyCollection, child, childPropertyStore):
             self.parentPropertyCollection = parentPropertyCollection
@@ -393,14 +405,13 @@
                 propertyCache = self.propertyCache()
                 results = propertyCache.keys()
                 if filterByUID:
-                    return [ 
+                    return [
                         (namespace, name)
                         for namespace, name, propuid in results
                         if propuid == uid
                     ]
                 else:
                     return results
-                
 
             self.log_debug("List for %s"
                            % (self.childPropertyStore.resource.fp.path,))

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/propfind.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/propfind.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/propfind.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -208,6 +208,13 @@
 
                 propstats.append(xml_propstat)
 
+            # Always need to have at least one propstat present (required by Prefer header behavior)
+            if len(propstats) == 0:
+                propstats.append(davxml.PropertyStatus(
+                    davxml.PropertyContainer(),
+                    davxml.Status.fromResponseCode(responsecode.OK)
+                ))
+
             xml_response = davxml.PropertyStatusResponse(davxml.HRef(uri), *propstats)
 
             # This needed for propfind cache tracking of children changes

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/put_common.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/put_common.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -770,7 +770,7 @@
 
         # Only relevant if calendar is sharee collection
         changed = False
-        if self.destinationparent.isShareeCollection():
+        if self.destinationparent.isShareeResource():
 
             # Get all X-APPLE-DROPBOX's and ATTACH's that are http URIs
             xdropboxes = self.calendar.getAllPropertiesInAnyComponent(
@@ -845,7 +845,7 @@
             return changed
 
         # Never add default alarms to calendar data in shared calendars
-        if self.destinationparent.isShareeCollection():
+        if self.destinationparent.isShareeResource():
             return changed
 
         # Add default alarm for VEVENT and VTODO only
@@ -912,6 +912,39 @@
 
 
     @inlineCallbacks
+    def hasCalendarResourceUIDSomewhereElse(self, uid):
+        """
+        See if a calendar component with a matching UID exists anywhere in the calendar home of the
+        current recipient owner and is not the resource being targeted.
+        """
+
+        # Ignore for an overwrite or a MOVE
+        if self.destination.exists() or self.sourceparent and self.deletesource:
+            returnValue(None)
+
+        failed = False
+
+        # Always fail a copy
+        if self.sourceparent and self.sourcecal and not self.deletesource and self.destinationcal:
+            failed = True
+        else:
+            # Get owner's calendar-home
+            calendar_owner_principal = (yield self.destination.resourceOwnerPrincipal(self.request))
+            calendar_home = yield calendar_owner_principal.calendarHome(self.request)
+
+            # Check for matching resource somewhere else in the home use the "schedule" mode to prevent any kind of match
+            failed = (yield calendar_home.hasCalendarResourceUIDSomewhereElse(uid, self.destination, "schedule"))
+
+        if failed:
+            log.debug("Implicit - found component with same UID in a different collection: %s" % (self.destination_uri,))
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (caldav_namespace, "unique-scheduling-object-resource"),
+                "Cannot duplicate scheduling object resource",
+            ))
+
+
+    @inlineCallbacks
     def doImplicitScheduling(self):
 
         data_changed = False
@@ -961,8 +994,8 @@
             if do_implicit_action and self.allowImplicitSchedule:
 
                 # Cannot do implicit in sharee's shared calendar
-                isShareeCollection = self.destinationparent.isShareeCollection()
-                if isShareeCollection:
+                isShareeResource = self.destinationparent.isShareeResource()
+                if isShareeResource:
                     raise HTTPError(ErrorResponse(
                         responsecode.FORBIDDEN,
                         (calendarserver_namespace, "sharee-privilege-needed",),
@@ -1179,6 +1212,7 @@
                             ),
                             "UID already exists",
                         ))
+                    yield self.hasCalendarResourceUIDSomewhereElse(self.uid)
 
             # Preserve private comments
             yield self.preservePrivateComments()

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_common.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_common.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -194,6 +194,13 @@
 
                 propstats.append(xml_propstat)
 
+        # Always need to have at least one propstat present (required by Prefer header behavior)
+        if len(propstats) == 0:
+            propstats.append(element.PropertyStatus(
+                element.PropertyContainer(),
+                element.Status.fromResponseCode(responsecode.OK)
+            ))
+
         if propstats:
             responses.append(element.PropertyStatusResponse(href, *propstats))
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/resource.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/resource.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -75,10 +75,7 @@
 
 from twistedcaldav.icaldav import ICalDAVResource, ICalendarPrincipalResource
 from twistedcaldav.linkresource import LinkResource
-from twistedcaldav.notify import (
-    getPubSubConfiguration, getPubSubXMPPURI, getPubSubHeartbeatURI,
-    getPubSubAPSConfiguration,
-)
+from calendarserver.push.notifier import getPubSubAPSConfiguration
 from twistedcaldav.sharing import SharedResourceMixin, SharedHomeMixin
 from twistedcaldav.util import normalizationLookup
 from twistedcaldav.vcard import Component as vComponent
@@ -586,7 +583,7 @@
                 if hasattr(self, "_newStoreObject"):
                     dataObject = getattr(self, "_newStoreObject")
                 if dataObject:
-                    label = "collection" if self.isShareeCollection() else "default"
+                    label = "collection" if self.isShareeResource() else "default"
                     nodeName = (yield dataObject.nodeName(label=label))
                     if nodeName:
                         propVal = customxml.PubSubXMPPPushKeyProperty(nodeName)
@@ -716,9 +713,9 @@
                 returnValue(customxml.AllowedSharingModes(customxml.CanBeShared()))
 
         elif qname == customxml.SharedURL.qname():
-            isShareeCollection = self.isShareeCollection()
+            isShareeResource = self.isShareeResource()
 
-            if isShareeCollection:
+            if isShareeResource:
                 returnValue(customxml.SharedURL(element.HRef.fromString(self._share.url())))
             else:
                 returnValue(None)
@@ -837,8 +834,8 @@
     def accessControlList(self, request, *args, **kwargs):
 
         acls = None
-        isShareeCollection = self.isShareeCollection()
-        if isShareeCollection:
+        isShareeResource = self.isShareeResource()
+        if isShareeResource:
             acls = (yield self.shareeAccessControlList(request, *args, **kwargs))
 
         if acls is None:
@@ -894,8 +891,8 @@
         Return the DAV:owner property value (MUST be a DAV:href or None).
         """
 
-        isShareeCollection = self.isShareeCollection()
-        if isShareeCollection:
+        isShareeResource = self.isShareeResource()
+        if isShareeResource:
             parent = (yield self.locateParent(request, self._share.url()))
         else:
             parent = (yield self.locateParent(request, request.urlForResource(self)))
@@ -911,8 +908,8 @@
         """
         Return the DAV:owner property value (MUST be a DAV:href or None).
         """
-        isShareeCollection = self.isShareeCollection()
-        if isShareeCollection:
+        isShareeResource = self.isShareeResource()
+        if isShareeResource:
             parent = (yield self.locateParent(request, self._share.url()))
         else:
             parent = (yield self.locateParent(request, request.urlForResource(self)))
@@ -1346,15 +1343,15 @@
         """
 
         sharedParent = None
-        isShareeCollection = self.isShareeCollection()
-        if isShareeCollection:
+        isShareeResource = self.isShareeResource()
+        if isShareeResource:
             # A sharee collection's quota root is the resource owner's root
             sharedParent = (yield request.locateResource(parentForURL(self._share.url())))
         else:
             parent = (yield self.locateParent(request, request.urlForResource(self)))
             if isCalendarCollectionResource(parent) or isAddressBookCollectionResource(parent):
-                isShareeCollection = parent.isShareeCollection()
-                if isShareeCollection:
+                isShareeResource = parent.isShareeResource()
+                if isShareeResource:
                     # A sharee collection's quota root is the resource owner's root
                     sharedParent = (yield request.locateResource(parentForURL(parent._share.url())))
 
@@ -1450,7 +1447,7 @@
 
                         # Always test against the current etag first just in case schedule-etags is out of sync
                         etag = (yield self.etag())
-                        etags = (etag,) + tuple([http_headers.ETag(etag) for etag in etags])
+                        etags = (etag,) + tuple([http_headers.ETag(schedule_etag) for schedule_etag in etags])
 
                         # Loop over each tag and succeed if any one matches, else re-raise last exception
                         exists = self.exists()
@@ -1545,11 +1542,13 @@
         return fail(NotImplementedError())
 
 
-    def iCalendarRolledup(self):
+    def iCalendarRolledup(self, request):
         """
         Only implemented by calendar collections; see storebridge.
         """
+        raise HTTPError(responsecode.NOT_ALLOWED)
 
+
     @inlineCallbacks
     def iCalendarFiltered(self, isowner, accessUID=None):
 
@@ -1974,7 +1973,7 @@
         elif namespace == carddav_namespace and self.addressBooksEnabled():
             if name == "addressbook-home-set":
                 returnValue(carddavxml.AddressBookHomeSet(
-                    *[element.HRef(url) for url in self.addressBookHomeURLs()]
+                    *[element.HRef(abhome_url) for abhome_url in self.addressBookHomeURLs()]
                  ))
             elif name == "directory-gateway" and self.directoryAddressBookEnabled():
                 returnValue(carddavxml.DirectoryGateway(
@@ -2074,6 +2073,15 @@
 class CommonHomeResource(PropfindCacheMixin, SharedHomeMixin, CalDAVResource):
     """
     Logic common to Calendar and Addressbook home resources.
+
+    @ivar _provisionedChildren: A map of resource names to built-in children
+        with protocol-level meanings, like C{"attachments"}, C{"inbox"},
+        C{"outbox"}, and so on.
+    @type _provisionedChildren: L{dict} mapping L{bytes} to L{Resource}
+
+    @ivar _provisionedLinks: A map of resource names to built-in links that the
+        server has inserted into this L{CommonHomeResource}.
+    @type _provisionedLinks: L{dict} mapping L{bytes} to L{Resource}
     """
     cacheNotifierFactory = DisabledCacheNotifier
 
@@ -2252,10 +2260,11 @@
             returnValue(child)
 
         # get regular or shared child
-        child = (yield self.makeRegularChild(name))
+        child = yield self.makeRegularChild(name)
 
-        # add _share attribute if child is shared
-        yield self.provisionShare(child)
+        # add _share attribute if child is shared; verify that child should
+        # still be accessible and convert it to None if it's not.
+        child = yield self.provisionShare(child)
 
         returnValue(child)
 
@@ -2311,8 +2320,7 @@
 
         elif qname == (customxml.calendarserver_namespace, "push-transports"):
 
-            if (config.Notifications.Services.XMPPNotifier.Enabled or
-                config.Notifications.Services.ApplePushNotifier.Enabled):
+            if config.Notifications.Services.ApplePushNotifier.Enabled:
 
                 nodeName = (yield self._newStoreHome.nodeName())
                 if nodeName:
@@ -2342,78 +2350,18 @@
                                 )
                             )
 
-                        pubSubConfiguration = getPubSubConfiguration(config)
-                        if (pubSubConfiguration['enabled'] and
-                            pubSubConfiguration['xmpp-server']):
-                            children.append(
-                                customxml.PubSubTransportProperty(
-                                    customxml.PubSubXMPPServerProperty(
-                                        pubSubConfiguration['xmpp-server']
-                                    ),
-                                    customxml.PubSubXMPPURIProperty(
-                                        getPubSubXMPPURI(notifierID, pubSubConfiguration)
-                                    ),
-                                    type="XMPP",
-                                )
-                            )
 
                         returnValue(customxml.PubSubPushTransportsProperty(*children))
             returnValue(None)
 
         elif qname == (customxml.calendarserver_namespace, "pushkey"):
-            if (config.Notifications.Services.XMPPNotifier.Enabled or
-                config.Notifications.Services.AMPNotifier.Enabled or
+            if (config.Notifications.Services.AMPNotifier.Enabled or
                 config.Notifications.Services.ApplePushNotifier.Enabled):
                 nodeName = (yield self._newStoreHome.nodeName())
                 if nodeName:
                     returnValue(customxml.PubSubXMPPPushKeyProperty(nodeName))
             returnValue(None)
 
-        elif qname == (customxml.calendarserver_namespace, "xmpp-uri"):
-            if config.Notifications.Services.XMPPNotifier.Enabled:
-                nodeName = (yield self._newStoreHome.nodeName())
-                if nodeName:
-                    notifierID = self._newStoreHome.notifierID()
-                    if notifierID:
-                        pubSubConfiguration = getPubSubConfiguration(config)
-                        returnValue(customxml.PubSubXMPPURIProperty(
-                            getPubSubXMPPURI(notifierID, pubSubConfiguration)))
-
-            returnValue(None)
-
-        elif qname == (customxml.calendarserver_namespace, "xmpp-heartbeat-uri"):
-            if config.Notifications.Services.XMPPNotifier.Enabled:
-                # Look up node name not because we want to return it, but
-                # to see if XMPP server is actually responding.  If it comes
-                # back with an empty nodeName, don't advertise
-                # xmpp-heartbeat-uri
-                nodeName = (yield self._newStoreHome.nodeName())
-                if nodeName:
-                    pubSubConfiguration = getPubSubConfiguration(config)
-                    returnValue(
-                        customxml.PubSubHeartbeatProperty(
-                            customxml.PubSubHeartbeatURIProperty(
-                                getPubSubHeartbeatURI(pubSubConfiguration)
-                            ),
-                            customxml.PubSubHeartbeatMinutesProperty(
-                                str(pubSubConfiguration['heartrate'])
-                            )
-                        )
-                    )
-            returnValue(None)
-
-        elif qname == (customxml.calendarserver_namespace, "xmpp-server"):
-            if config.Notifications.Services.XMPPNotifier.Enabled:
-                # Look up node name not because we want to return it, but
-                # to see if XMPP server is actually responding.  If it comes
-                # back with an empty nodeName, don't advertise xmpp-server
-                nodeName = (yield self._newStoreHome.nodeName())
-                if nodeName:
-                    pubSubConfiguration = getPubSubConfiguration(config)
-                    returnValue(customxml.PubSubXMPPServerProperty(
-                        pubSubConfiguration['xmpp-server']))
-            returnValue(None)
-
         returnValue((yield super(CommonHomeResource, self).readProperty(property, request)))
 
 
@@ -2546,11 +2494,6 @@
             #caldavxml.DefaultAlarmVToDoDate.qname(),
 
         )
-        existing += (
-            (customxml.calendarserver_namespace, "xmpp-uri"),
-            (customxml.calendarserver_namespace, "xmpp-heartbeat-uri"),
-            (customxml.calendarserver_namespace, "xmpp-server"),
-        )
 
         if config.EnableManagedAttachments:
             existing += (
@@ -2652,13 +2595,13 @@
         returnValue(similar)
 
 
-    def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, type):
+    def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, mode):
         """
         Test if there are other child object resources with the specified UID.
 
         Pass through direct to store.
         """
-        return self._newStoreHome.hasCalendarResourceUIDSomewhereElse(uid, ok_object._newStoreObject, type)
+        return self._newStoreHome.hasCalendarResourceUIDSomewhereElse(uid, ok_object._newStoreObject, mode)
 
 
     def getCalendarResourcesForUID(self, uid, allow_shared=False):
@@ -2829,7 +2772,7 @@
             if defaultAddressBookProperty and len(defaultAddressBookProperty.children) == 1:
                 defaultAddressBook = str(defaultAddressBookProperty.children[0])
                 adbk = (yield request.locateResource(str(defaultAddressBook)))
-                if adbk is not None and isAddressBookCollectionResource(adbk) and adbk.exists() and not adbk.isShareeCollection():
+                if adbk is not None and isAddressBookCollectionResource(adbk) and adbk.exists() and not adbk.isShareeResource():
                     returnValue(defaultAddressBookProperty)
 
             # Default is not valid - we have to try to pick one
@@ -2852,7 +2795,7 @@
             if len(new_adbk) == 1:
                 adbkURI = str(new_adbk[0])
                 adbk = (yield request.locateResource(str(new_adbk[0])))
-            if adbk is None or not adbk.exists() or not isAddressBookCollectionResource(adbk) or adbk.isShareeCollection():
+            if adbk is None or not adbk.exists() or not isAddressBookCollectionResource(adbk) or adbk.isShareeResource():
                 # Validate that href's point to a valid addressbook.
                 raise HTTPError(ErrorResponse(
                     responsecode.CONFLICT,

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/__init__.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/__init__.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/delivery.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/delivery.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/delivery.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/resource.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/resource.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.directory.test.test_calendar -*-
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -252,7 +252,7 @@
         if defaultCalendarProperty and len(defaultCalendarProperty.children) == 1:
             defaultCalendar = str(defaultCalendarProperty.children[0])
             cal = (yield request.locateResource(str(defaultCalendar)))
-            if cal is not None and isCalendarCollectionResource(cal) and cal.exists() and not cal.isShareeCollection():
+            if cal is not None and isCalendarCollectionResource(cal) and cal.exists() and not cal.isShareeResource():
                 returnValue(defaultCalendarProperty)
 
         # Default is not valid - we have to try to pick one
@@ -280,7 +280,7 @@
 
         # TODO: check that owner of the new calendar is the same as owner of this inbox
         if cal is None or not cal.exists() or not isCalendarCollectionResource(cal) or \
-            cal.isShareeCollection() or not cal.isSupportedComponent(componentType):
+            cal.isShareeResource() or not cal.isSupportedComponent(componentType):
             # Validate that href's point to a valid calendar.
             raise HTTPError(ErrorResponse(
                 responsecode.CONFLICT,

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/scheduler.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/scheduler.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/scheduler.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/__init__.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/__init__.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_delivery.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_delivery.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_delivery.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_resource.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_resource.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -375,7 +375,7 @@
             self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
 
         # Force the new calendar to think it is a virtual share
-        newcalendar._isShareeCollection = True
+        newcalendar._isShareeResource = True
 
         try:
             default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/icaldiff.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/icaldiff.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -506,11 +506,13 @@
         self._transferProperty("TRANSP", serverComponent, clientComponent)
         self._transferProperty("DTSTAMP", serverComponent, clientComponent)
         self._transferProperty("LAST-MODIFIED", serverComponent, clientComponent)
-        self._transferProperty("X-APPLE-NEEDS-REPLY", serverComponent, clientComponent)
         self._transferProperty("COMPLETED", serverComponent, clientComponent)
+        for pname in config.Scheduling.CalDAV.PerAttendeeProperties:
+            self._transferProperty(pname, serverComponent, clientComponent)
 
         # Dropbox - this now never returns false
-        self._transferDropBoxData(serverComponent, clientComponent)
+        if config.EnableDropBox:
+            self._transferDropBoxData(serverComponent, clientComponent)
 
         # Handle VALARMs
         serverComponent.removeAlarms()

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/__init__.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/__init__.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/delivery.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/delivery.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/delivery.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.scheduling.test.test_imip -*-
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -19,25 +19,20 @@
 Handles the sending of scheduling messages via iMIP (mail gateway).
 """
 
-from twisted.python.failure import Failure
-from twisted.internet.defer import inlineCallbacks, returnValue
-
 from twext.python.log import Logger
-from twext.web2.dav.http import ErrorResponse
-
 from twext.web2 import responsecode
+from twext.web2.dav.http import ErrorResponse
 from twext.web2.http import HTTPError
-from twisted.web import client
-
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.failure import Failure
 from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.util import AuthorizedHTTPGetter
+from twistedcaldav.directory.util import transactionFromRequest
 from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.imip.outbound import IMIPInvitationWork
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.internet.adaptendpoint import connect
 
 
+
 __all__ = [
     "ScheduleViaIMip",
 ]
@@ -105,9 +100,11 @@
 
                     fromAddr = str(self.scheduler.originator.cuaddr)
 
-                    log.debug("POSTing iMIP message to gateway...  To: '%s', From :'%s'\n%s" % (toAddr, fromAddr, caldata,))
-                    yield self.postToGateway(fromAddr, toAddr, caldata)
+                    txn = transactionFromRequest(self.scheduler.request, self.scheduler.request._newStoreTransaction.store)
+                    log.debug("Submitting iMIP message...  To: '%s', From :'%s'\n%s" % (toAddr, fromAddr, caldata,))
+                    yield txn.enqueue(IMIPInvitationWork, fromAddr=fromAddr, toAddr=toAddr, icalendarText=caldata)
 
+
                 except Exception, e:
                     # Generated failed response for this recipient
                     log.debug("iMIP request %s failed for recipient %s: %s" % (self, recipient, e))
@@ -125,26 +122,3 @@
             log.debug("iMIP request %s failed: %s" % (self, e))
             for recipient in self.recipients:
                 failForRecipient(recipient)
-
-
-    def postToGateway(self, fromAddr, toAddr, caldata, reactor=None):
-        if reactor is None:
-            from twisted.internet import reactor
-
-        mailGatewayServer = config.Scheduling['iMIP']['MailGatewayServer']
-        mailGatewayPort = config.Scheduling['iMIP']['MailGatewayPort']
-        url = "http://%s:%d/inbox" % (mailGatewayServer, mailGatewayPort)
-        headers = {
-            'Content-Type' : 'text/calendar',
-            'Originator' : fromAddr,
-            'Recipient' : toAddr,
-            config.Scheduling.iMIP.Header : config.Scheduling.iMIP.Password,
-        }
-        factory = client.HTTPClientFactory(url, method='POST', headers=headers,
-            postdata=caldata, agent="CalDAV server")
-
-        factory.noisy = False
-        factory.protocol = AuthorizedHTTPGetter
-        connect(GAIEndpoint(reactor, mailGatewayServer, mailGatewayPort),
-                factory)
-        return factory.deferred

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/mailgateway.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/mailgateway.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/mailgateway.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,6 +1,5 @@
-# -*- test-case-name: twistedcaldav.test.test_mail -*-
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,480 +15,21 @@
 ##
 
 """
-Mail Gateway for Calendar Server
+SQLite implementation of mail token database (deprecated).  This only exists
+now in order to migrate tokens from sqlite to the new store.
 """
 
-from __future__ import with_statement
-
-from cStringIO import StringIO
-
-from calendarserver.tap.util import getRootResource, directoryFromConfig
-
-from email.mime.image import MIMEImage
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-
-from pycalendar.datetime import PyCalendarDateTime
-from pycalendar.duration import PyCalendarDuration
-
-from twext.internet.adaptendpoint import connect
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.python.log import Logger, LoggingMixIn
-from twext.web2 import server
-from twext.web2.channel.http import HTTPFactory
-
-from twisted.application import internet, service
-from twisted.internet import protocol, defer, ssl, reactor as _reactor
-from twisted.internet.defer import succeed
-from twisted.mail import pop3client, imap4
-from twisted.mail.smtp import messageid, rfc822date, ESMTPSenderFactory
-from twisted.plugin import IPlugin
-from twisted.python.usage import Options, UsageError
-from twisted.web import client
-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
-
-from twistedcaldav import memcachepool
-from twistedcaldav.config import config
-from twistedcaldav.ical import Property, Component
-from twistedcaldav.localization import translationTo, _
-from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
-from twistedcaldav.scheduling.imip.resource import IMIPInvitationInboxResource
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.sql import AbstractSQLDatabase
-from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
-from twistedcaldav.util import AuthorizedHTTPGetter
-
-from zope.interface import implements
-
 import datetime
-import email.utils
 import os
-import urlparse
 import uuid
 
+from twext.python.log import Logger, LoggingMixIn
+from twistedcaldav.sql import AbstractSQLDatabase
+from twisted.internet.defer import inlineCallbacks
 
-__all__ = [
-    "MailGatewayServiceMaker",
-    "MailGatewayTokensDatabase",
-    "MailHandler",
-]
-
-
 log = Logger()
 
-#
-# Monkey patch imap4.log so it doesn't emit useless logging,
-# specifically, "Unhandled unsolicited response" nonsense.
-#
-class IMAPLogger(Logger):
-    def emit(self, level, message, *args, **kwargs):
-        if message.startswith("Unhandled unsolicited response:"):
-            return
 
-        Logger.emit(self, level, message, *args, **kwargs)
-
-imap4.log = IMAPLogger()
-
-#
-# Templates
-#
-
-plainCancelTemplate = u"""%(subject)s
-
-%(orgLabel)s: %(plainOrganizer)s
-%(dateLabel)s: %(dateInfo)s %(recurrenceInfo)s
-%(timeLabel)s: %(timeInfo)s %(durationInfo)s
-"""
-
-plainInviteTemplate = u"""%(subject)s
-
-%(orgLabel)s: %(plainOrganizer)s
-%(locLabel)s: %(location)s
-%(dateLabel)s: %(dateInfo)s %(recurrenceInfo)s
-%(timeLabel)s: %(timeInfo)s %(durationInfo)s
-%(descLabel)s: %(description)s
-%(urlLabel)s: %(url)s
-%(attLabel)s: %(plainAttendees)s
-"""
-
-
-htmlCancelTemplate = u"""<html>
-    <body><div>
-
-    <h1>%(subject)s</h1>
-    <p>
-    <h3>%(orgLabel)s:</h3> %(htmlOrganizer)s
-    </p>
-    <p>
-    <h3>%(dateLabel)s:</h3> %(dateInfo)s %(recurrenceInfo)s
-    </p>
-    <p>
-    <h3>%(timeLabel)s:</h3> %(timeInfo)s %(durationInfo)s
-    </p>
-    """.encode("utf-8")
-
-
-htmlInviteTemplate = u"""<html>
-    <body><div>
-    <p>%(inviteLabel)s</p>
-
-    <h1>%(summary)s</h1>
-    <p>
-    <h3>%(orgLabel)s:</h3> %(htmlOrganizer)s
-    </p>
-    <p>
-    <h3>%(locLabel)s:</h3> %(location)s
-    </p>
-    <p>
-    <h3>%(dateLabel)s:</h3> %(dateInfo)s %(recurrenceInfo)s
-    </p>
-    <p>
-    <h3>%(timeLabel)s:</h3> %(timeInfo)s %(durationInfo)s
-    </p>
-    <p>
-    <h3>%(descLabel)s:</h3> %(description)s
-    </p>
-    <p>
-    <h3>%(urlLabel)s:</h3> <a href="%(url)s">%(url)s</a>
-    </p>
-    <p>
-    <h3>%(attLabel)s:</h3> %(htmlAttendees)s
-    </p>
-    """.encode("utf-8")
-
-def _visit(document, node):
-    if isinstance(node, DOMText):
-        idx = node.parentNode.childNodes.index(node)
-        splitted = node.data.split("%(")
-        firstTextNode = document.createTextNode(splitted[0])
-        firstTextNode.parentNode = node.parentNode
-        replacements = [firstTextNode]
-        for moreText in splitted[1:]:
-            slotName, extra = moreText.split(')', 1)
-            extra = extra[1:]
-            slotElement = document.createElement('t:slot')
-            slotElement.setAttribute("name", slotName)
-            slotElement.parentNode = node.parentNode
-            textNode = document.createTextNode(extra)
-            textNode.parentNode = node.parentNode
-            replacements.append(slotElement)
-            replacements.append(textNode)
-        node.parentNode.childNodes[idx:idx + 1] = replacements
-
-    elif isinstance(node, DOMElement):
-        for attrName, attrVal in node.attributes.items():
-            if '%(' in attrVal:
-                del node.attributes[attrName]
-                elem = document.createElement('t:attr')
-                elem.setAttribute('name', attrName)
-                textNode = document.createTextNode(attrVal)
-                elem.appendChild(textNode)
-                node.appendChild(elem)
-
-
-
-def _walk(document, n):
-    _visit(document, n)
-    for subn in n.childNodes:
-        _walk(document, subn)
-
-
-
-def _fixup(data, rendererName):
-    document = parseString(data, beExtremelyLenient=True)
-    document.documentElement.setAttribute(
-        "xmlns:t", TEMPLATE_NAMESPACE
-    )
-    document.doctype = (
-        'html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" '
-        '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"'
-    )
-    document.documentElement.setAttribute(
-        "t:render", rendererName
-    )
-    _walk(document, document)
-    result = document.toxml()
-    return result
-
-
-
-class StringFormatTemplateLoader(object):
-    """
-    Loader for twisted.web.template that converts a template with %()s slots.
-    """
-    def __init__(self, fileFactory, rendererName):
-        """
-        @param fileFactory: a 1-argument callable which returns a file-like
-            object that contains the %()s-format template.
-
-        @param rendererName: the name of the renderer.
-
-        @type rendererName: C{str}
-        """
-        self.fileFactory = fileFactory
-        self.rendererName = rendererName
-
-
-    def load(self):
-        html = _fixup(self.fileFactory().read(), self.rendererName)
-        return XMLString(html).load()
-
-
-
-def localizedLabels(language, canceled, inviteState):
-    """
-    Generate localized labels for an email in the given language.
-
-    @param language: a 2-letter language code
-
-    @type language: C{str}
-
-    @return: a 2-tuple of (subjectFormatString, labelDict), where the first is a
-        format string for use in the subject, and the latter is a dictionary
-        with labels suitable for filling out HTML and plain-text templates.  All
-        values are C{str}s.
-    """
-    with translationTo(language):
-        if canceled:
-            subjectFormatString = _("Event canceled: %(summary)s")
-        elif inviteState == "new":
-            subjectFormatString = _("Event invitation: %(summary)s")
-        elif inviteState == "update":
-            subjectFormatString = _("Event update: %(summary)s")
-        else:
-            subjectFormatString = _("Event reply: %(summary)s")
-
-        if canceled:
-            inviteLabel = _("Event Canceled")
-        else:
-            if inviteState == "new":
-                inviteLabel = _("Event Invitation")
-            elif inviteState == "update":
-                inviteLabel = _("Event Update")
-            else:
-                inviteLabel = _("Event Reply")
-
-        labels = dict(
-            dateLabel=_("Date"),
-            timeLabel=_("Time"),
-            durationLabel=_("Duration"),
-            recurrenceLabel=_("Occurs"),
-            descLabel=_("Description"),
-            urlLabel=_("URL"),
-            orgLabel=_("Organizer"),
-            attLabel=_("Attendees"),
-            locLabel=_("Location"),
-            inviteLabel=inviteLabel,
-        )
-
-        # The translations we get back from gettext are utf-8 encoded
-        # strings, so convert to unicode
-        for key in labels.keys():
-            if isinstance(labels[key], str):
-                labels[key] = labels[key].decode("utf-8")
-
-    return subjectFormatString.decode("utf-8"), labels
-
-
-
-class MailGatewayOptions(Options):
-    """
-    Mail gateway service config
-    """
-    optParameters = [[
-        "config", "f", DEFAULT_CONFIG_FILE, "Path to configuration file."
-    ]]
-
-    def __init__(self, *args, **kwargs):
-        super(MailGatewayOptions, self).__init__(*args, **kwargs)
-
-        self.overrides = {}
-
-
-    def _coerceOption(self, configDict, key, value):
-        """
-        Coerce the given C{val} to type of C{configDict[key]}
-        """
-        if key in configDict:
-            if isinstance(configDict[key], bool):
-                value = value == "True"
-
-            elif isinstance(configDict[key], (int, float, long)):
-                value = type(configDict[key])(value)
-
-            elif isinstance(configDict[key], (list, tuple)):
-                value = value.split(',')
-
-            elif isinstance(configDict[key], dict):
-                raise UsageError(
-                    "Dict options not supported on the command line"
-                )
-
-            elif value == 'None':
-                value = None
-
-        return value
-
-
-    def _setOverride(self, configDict, path, value, overrideDict):
-        """
-        Set the value at path in configDict
-        """
-        key = path[0]
-
-        if len(path) == 1:
-            overrideDict[key] = self._coerceOption(configDict, key, value)
-            return
-
-        if key in configDict:
-            if not isinstance(configDict[key], dict):
-                raise UsageError(
-                    "Found intermediate path element that is not a dictionary"
-                )
-
-            if key not in overrideDict:
-                overrideDict[key] = {}
-
-            self._setOverride(
-                configDict[key], path[1:],
-                value, overrideDict[key]
-            )
-
-
-    def opt_option(self, option):
-        """
-        Set an option to override a value in the config file. True, False, int,
-        and float options are supported, as well as comma separated lists. Only
-        one option may be given for each --option flag, however multiple
-        --option flags may be specified.
-        """
-
-        if "=" in option:
-            path, value = option.split('=')
-            self._setOverride(
-                DEFAULT_CONFIG,
-                path.split('/'),
-                value,
-                self.overrides
-            )
-        else:
-            self.opt_option('%s=True' % (option,))
-
-    opt_o = opt_option
-
-    def postOptions(self):
-        config.load(self['config'])
-        config.updateDefaults(self.overrides)
-        self.parent['pidfile'] = None
-
-
-
-def injectionSettingsFromURL(url, config):
-    """
-    Given a url returned from server podding info (or None if not podding),
-    generate the url that should be used to inject an iMIP reply.  If the
-    url is None, then compute the url from config.
-    """
-    path = "inbox"
-    if url is None:
-        # Didn't get url from server podding configuration, so use caldavd.plist
-        if config.Scheduling.iMIP.MailGatewayServer == "localhost":
-            hostname = "localhost"
-        else:
-            hostname = config.ServerHostName
-        if config.EnableSSL:
-            useSSL = True
-            port = config.SSLPort
-        else:
-            useSSL = False
-            port = config.HTTPPort
-        scheme = "https:" if useSSL else "http:"
-        url = "%s//%s:%d/%s/" % (scheme, hostname, port, path)
-    else:
-        url = "%s/%s/" % (url.rstrip("/"), path)
-    return url
-
-
-
-def injectMessage(url, organizer, attendee, calendar, msgId, reactor=None):
-
-    if reactor is None:
-        reactor = _reactor
-
-    headers = {
-        'Content-Type' : 'text/calendar',
-        'Originator' : attendee,
-        'Recipient' : organizer,
-        config.Scheduling.iMIP.Header : config.Scheduling.iMIP.Password,
-    }
-
-    data = str(calendar)
-    url = injectionSettingsFromURL(url, config)
-    parsed = urlparse.urlparse(url)
-
-    log.debug("Injecting to %s: %s %s" % (url, str(headers), data))
-
-    factory = client.HTTPClientFactory(url, method='POST', headers=headers,
-        postdata=data, agent="iMIP gateway")
-
-    factory.noisy = False
-    factory.protocol = AuthorizedHTTPGetter
-
-    if parsed.scheme == "https":
-        connect(GAIEndpoint(reactor, parsed.hostname, parsed.port,
-                            ssl.ClientContextFactory()),
-                factory)
-    else:
-        connect(GAIEndpoint(reactor, parsed.hostname, parsed.port), factory)
-
-
-    def _success(result, msgId):
-        log.info("Mail gateway successfully injected message %s" % (msgId,))
-
-
-    def _failure(failure, msgId):
-        log.err("Mail gateway failed to inject message %s (Reason: %s)" %
-            (msgId, failure.getErrorMessage()))
-        log.debug("Failed calendar body: %s" % (str(calendar),))
-
-    factory.deferred.addCallback(_success, msgId).addErrback(_failure, msgId)
-    return factory.deferred
-
-
-
-def serverForOrganizer(directory, organizer):
-    """
-    Return the URL for the server hosting the organizer, or None if podding
-    is not enabled or organizer is hosted locally.
-    Raises ServerNotFound if we can't find the record for the organizer.
-    @param directory: service to look for organizer in
-    @type directory: L{DirectoryService}
-    @param organizer: CUA of organizer
-    @type organizer: C{str}
-    @return: string URL
-    """
-    record = directory.recordWithCalendarUserAddress(organizer)
-    if record is None:
-        log.warn("Can't find server for %s" % (organizer,))
-        raise ServerNotFound()
-
-    srvr = record.server()  # None means hosted locally
-    if srvr is None:
-        return None
-    else:
-        return srvr.uri
-
-
-
-class ServerNotFound(Exception):
-    """
-    Can't determine which server is hosting a given user
-    """
-
-
-
 class MailGatewayTokensDatabase(AbstractSQLDatabase, LoggingMixIn):
     """
     A database to maintain "plus-address" tokens for IMIP requests.
@@ -560,6 +100,15 @@
         else:
             return None
 
+    def getAllTokens(self):
+        results = list(
+            self._db_execute(
+                """
+                select TOKEN, ORGANIZER, ATTENDEE, ICALUID from TOKENS
+                """
+            )
+        )
+        return results
 
     def deleteToken(self, token):
         self._db_execute(
@@ -665,1220 +214,20 @@
         pass
 
 
-
-#
-# Service
-#
-
-class MailGatewayService(service.MultiService):
-
-    def startService(self):
-        """
-        Purge old database tokens -- doing this in startService so that
-        it happens after we've shed privileges
-        """
-        service.MultiService.startService(self)
-        mailer = getattr(self, "mailer", None)
-        if mailer is not None:
-            mailer.purge()
-            mailer.lowercase()
-
-
-
-class MailGatewayServiceMaker(LoggingMixIn):
-    implements(IPlugin, service.IServiceMaker)
-
-    tapname = "caldav_mailgateway"
-    description = "Mail Gateway"
-    options = MailGatewayOptions
-
-    def makeService(self, options):
-        try:
-            from setproctitle import setproctitle
-        except ImportError:
-            pass
-        else:
-            setproctitle("CalendarServer [Mail Gateway]")
-
-        memcachepool.installPools(
-            config.Memcached.Pools,
-            config.Memcached.MaxClients,
-        )
-
-        mailGatewayService = MailGatewayService()
-
-        settings = config.Scheduling['iMIP']
-        if settings['Enabled']:
-            mailer = MailHandler()
-
-            mailType = settings['Receiving']['Type']
-            if mailType.lower().startswith('pop'):
-                self.log_info("Starting Mail Gateway Service: POP3")
-                client = POP3Service(settings['Receiving'], mailer)
-            elif mailType.lower().startswith('imap'):
-                self.log_info("Starting Mail Gateway Service: IMAP4")
-                client = IMAP4Service(settings['Receiving'], mailer)
-            else:
-                # TODO: raise error?
-                self.log_error("Invalid iMIP type in configuration: %s" %
-                    (mailType,))
-                return mailGatewayService
-
-            client.setServiceParent(mailGatewayService)
-
-            # Set up /inbox -- server POSTs to it to send out iMIP invites
-            IScheduleService(settings, mailer).setServiceParent(
-                mailGatewayService
-            )
-
-        else:
-            mailer = None
-            self.log_info("Mail Gateway Service not enabled")
-
-        mailGatewayService.mailer = mailer
-        return mailGatewayService
-
-
-
-class IScheduleService(service.MultiService, LoggingMixIn):
+ at inlineCallbacks
+def migrateTokensToStore(path, store):
     """
-    ISchedule Inbox
-    """
+    Copy all the tokens from the sqlite db into the new store.
 
-    def __init__(self, settings, mailer):
-        service.MultiService.__init__(self)
-        self.settings = settings
-        self.mailer = mailer
+    @param path: Filesystem path to directory containing the sqlite db file.
+    @type path: C{str}
 
-        # Disable since we're only interested in /principals (for auth)
-        config.EnableCalDAV = False
-        config.EnableCardDAV = False
-
-        rootResource = getRootResource(
-            config,
-            "IGNORED", # no need for a store - no /calendars nor /addressbooks
-            resources=[
-                ("inbox", IMIPInvitationInboxResource, (mailer,), ("digest",)),
-            ]
-        )
-
-        self.factory = HTTPFactory(server.Site(rootResource))
-        self.server = internet.TCPServer(settings['MailGatewayPort'],
-            self.factory)
-        self.server.setServiceParent(self)
-
-
-
-class MailHandler(LoggingMixIn):
-
-    def __init__(self, dataRoot=None, directory=None):
-        if dataRoot is None:
-            dataRoot = config.DataRoot
-        if directory is None:
-            directory = directoryFromConfig(config)
-        self.db = MailGatewayTokensDatabase(dataRoot)
-        self.days = config.Scheduling['iMIP']['InvitationDaysToLive']
-        self.directory = directory
-
-
-    def purge(self):
-        """
-        Purge old database tokens
-        """
-        self.db.purgeOldTokens(datetime.date.today() -
-            datetime.timedelta(days=self.days))
-
-
-    def lowercase(self):
-        """
-        Convert all mailto: to lowercase
-        """
-        self.db.lowercase()
-
-
-    def checkDSN(self, message):
-        # returns (isDSN, Action, icalendar attachment)
-
-        report = deliveryStatus = calBody = None
-
-        for part in message.walk():
-            content_type = part.get_content_type()
-            if content_type == "multipart/report":
-                report = part
-                continue
-            elif content_type == "message/delivery-status":
-                deliveryStatus = part
-                continue
-            elif content_type == "message/rfc822":
-                #original = part
-                continue
-            elif content_type == "text/calendar":
-                calBody = part.get_payload(decode=True)
-                continue
-
-        if report is not None and deliveryStatus is not None:
-            # we have what appears to be a DSN
-
-            lines = str(deliveryStatus).split("\n")
-            for line in lines:
-                lower = line.lower()
-                if lower.startswith("action:"):
-                    # found Action:
-                    action = lower.split(' ')[1]
-                    break
-            else:
-                action = None
-
-            return True, action, calBody
-
-        else:
-            # Not a DSN
-            return False, None, None
-
-
-    def _extractToken(self, text):
-        try:
-            pre, _ignore_post = text.split('@')
-            pre, token = pre.split('+')
-            return token
-        except ValueError:
-            return None
-
-
-    def processDSN(self, calBody, msgId, fn):
-        calendar = Component.fromString(calBody)
-        # Extract the token (from organizer property)
-        organizer = calendar.getOrganizer()
-        token = self._extractToken(organizer)
-        if not token:
-            self.log_error("Mail gateway can't find token in DSN %s" % (msgId,))
-            return
-
-        result = self.db.lookupByToken(token)
-        if result is None:
-            # This isn't a token we recognize
-            self.log_error("Mail gateway found a token (%s) but didn't "
-                           "recognize it in DSN %s" % (token, msgId))
-            return
-
-        organizer, attendee, icaluid = result
-        organizer = str(organizer)
-        attendee = str(attendee)
-        icaluid = str(icaluid)
-        calendar.removeAllButOneAttendee(attendee)
-        calendar.getOrganizerProperty().setValue(organizer)
-        for comp in calendar.subcomponents():
-            if comp.name() == "VEVENT":
-                comp.addProperty(Property("REQUEST-STATUS",
-                    ["5.1", "Service unavailable"]))
-                break
-        else:
-            # no VEVENT in the calendar body.
-            # TODO: what to do in this case?
-            pass
-
-        try:
-            hostname = serverForOrganizer(self.directory, organizer)
-        except ServerNotFound:
-            # We can't determine which server hosts the organizer
-            self.log_error("Unable to determine which server hosts organizer %s"
-                % (organizer,))
-            return succeed(None)
-
-        self.log_warn("Mail gateway processing DSN %s to server %s" % (msgId, hostname))
-        return fn(hostname, organizer, attendee, calendar, msgId)
-
-
-    def processReply(self, msg, injectFunction, testMode=False):
-        # extract the token from the To header
-        _ignore_name, addr = email.utils.parseaddr(msg['To'])
-        if addr:
-            # addr looks like: server_address+token at example.com
-            token = self._extractToken(addr)
-            if not token:
-                self.log_error("Mail gateway didn't find a token in message "
-                               "%s (%s)" % (msg['Message-ID'], msg['To']))
-                return
-        else:
-            self.log_error("Mail gateway couldn't parse To: address (%s) in "
-                           "message %s" % (msg['To'], msg['Message-ID']))
-            return
-
-        result = self.db.lookupByToken(token)
-        if result is None:
-            # This isn't a token we recognize
-            self.log_error("Mail gateway found a token (%s) but didn't "
-                           "recognize it in message %s"
-                           % (token, msg['Message-ID']))
-            return
-
-        organizer, attendee, icaluid = result
-        organizer = str(organizer)
-        attendee = str(attendee)
-        icaluid = str(icaluid)
-
-        for part in msg.walk():
-            if part.get_content_type() == "text/calendar":
-                calBody = part.get_payload(decode=True)
-                break
-        else:
-            # No icalendar attachment
-            self.log_warn("Mail gateway didn't find an icalendar attachment "
-                          "in message %s" % (msg['Message-ID'],))
-
-            toAddr = None
-            fromAddr = attendee[7:]
-
-            if organizer.startswith("mailto:"):
-                toAddr = organizer[7:]
-            elif organizer.startswith("urn:uuid:"):
-                guid = organizer[9:]
-                record = self.directory.recordWithGUID(guid)
-                if record and record.emailAddresses:
-                    toAddr = list(record.emailAddresses)[0]
-
-            if toAddr is None:
-                self.log_error("Don't have an email address for the organizer; "
-                               "ignoring reply.")
-                return
-
-            if testMode:
-                return (toAddr, fromAddr)
-
-            settings = config.Scheduling["iMIP"]["Sending"]
-            if settings["UseSSL"]:
-                contextFactory = ssl.ClientContextFactory()
-            else:
-                contextFactory = None
-
-            deferred = defer.Deferred()
-            del msg["From"]
-            msg["From"] = fromAddr
-            del msg["Reply-To"]
-            msg["Reply-To"] = fromAddr
-            del msg["To"]
-            msg["To"] = toAddr
-            factory = ESMTPSenderFactory(
-                settings["Username"], settings["Password"],
-                fromAddr, toAddr,
-                # per http://trac.calendarserver.org/ticket/416 ...
-                StringIO(msg.as_string().replace("\r\n", "\n")),
-                deferred,
-                contextFactory=contextFactory,
-                requireAuthentication=False,
-                requireTransportSecurity=settings["UseSSL"],
-            )
-
-            self.log_warn("Mail gateway forwarding reply back to organizer")
-            connect(GAIEndpoint(_reactor, settings["Server"], settings["Port"]),
-                    factory)
-            return deferred
-
-        # Process the imip attachment; inject to calendar server
-
-        self.log_debug(calBody)
-        calendar = Component.fromString(calBody)
-        event = calendar.mainComponent()
-
-        calendar.removeAllButOneAttendee(attendee)
-        organizerProperty = calendar.getOrganizerProperty()
-        if organizerProperty is None:
-            # ORGANIZER is required per rfc2446 section 3.2.3
-            self.log_warn("Mail gateway didn't find an ORGANIZER in REPLY %s"
-                          % (msg['Message-ID'],))
-            event.addProperty(Property("ORGANIZER", organizer))
-        else:
-            organizerProperty.setValue(organizer)
-
-        if not calendar.getAttendees():
-            # The attendee we're expecting isn't there, so add it back
-            # with a SCHEDULE-STATUS of SERVICE_UNAVAILABLE.
-            # The organizer will then see that the reply was not successful.
-            attendeeProp = Property("ATTENDEE", attendee,
-                params={
-                    "SCHEDULE-STATUS": iTIPRequestStatus.SERVICE_UNAVAILABLE,
-                }
-            )
-            event.addProperty(attendeeProp)
-
-            # TODO: We have talked about sending an email to the reply-to
-            # at this point, to let them know that their reply was missing
-            # the appropriate ATTENDEE.  This will require a new localizable
-            # email template for the message.
-
-        try:
-            hostname = serverForOrganizer(self.directory, organizer)
-        except ServerNotFound:
-            # We can't determine which server hosts the organizer
-            self.log_error("Unable to determine which server hosts organizer %s"
-                % (organizer,))
-            return succeed(None)
-
-        return injectFunction(hostname, organizer, attendee, calendar,
-            msg['Message-ID'])
-
-
-    def inbound(self, message, fn=injectMessage):
-        try:
-            msg = email.message_from_string(message)
-
-            isDSN, action, calBody = self.checkDSN(msg)
-            if isDSN:
-                if action == 'failed' and calBody:
-                    # This is a DSN we can handle
-                    return self.processDSN(calBody, msg['Message-ID'], fn)
-                else:
-                    # It's a DSN without enough to go on
-                    self.log_error("Mail gateway can't process DSN %s"
-                                   % (msg['Message-ID'],))
-                    return
-
-            self.log_info("Mail gateway received message %s from %s to %s" %
-                (msg['Message-ID'], msg['From'], msg['To']))
-
-            return self.processReply(msg, fn)
-
-        except Exception, e:
-            # Don't let a failure of any kind stop us
-            self.log_error("Failed to process message: %s" % (e,))
-
-
-    def outbound(self, originator, recipient, calendar, language='en',
-                 send=True, onlyAfter=None):
-        # create token, send email
-
-        settings = config.Scheduling['iMIP']['Sending']
-
-        if onlyAfter is None:
-            duration = PyCalendarDuration(days=settings.SuppressionDays)
-            onlyAfter = PyCalendarDateTime.getNowUTC() - duration
-
-        component = calendar.masterComponent()
-        if component is None:
-            component = calendar.mainComponent(True)
-        icaluid = component.propertyValue("UID")
-        method = calendar.propertyValue("METHOD")
-
-        # Clean up the attendee list which is purely used within the human
-        # readable email message (not modifying the calendar body)
-        attendees = []
-        for attendeeProp in calendar.getAllAttendeeProperties():
-            cutype = attendeeProp.parameterValue("CUTYPE", "INDIVIDUAL")
-            if cutype == "INDIVIDUAL":
-                cn = attendeeProp.parameterValue("CN", None)
-                if cn is not None:
-                    cn = cn.decode("utf-8")
-                cuaddr = normalizeCUAddr(attendeeProp.value())
-                if cuaddr.startswith("mailto:"):
-                    mailto = cuaddr[7:]
-                    if not cn:
-                        cn = mailto
-                else:
-                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
-                    if emailAddress:
-                        mailto = emailAddress
-                    else:
-                        mailto = None
-
-                if cn or mailto:
-                    attendees.append((cn, mailto))
-
-        toAddr = recipient
-        if not recipient.lower().startswith("mailto:"):
-            raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP "
-                             "operation." % (recipient,))
-        recipient = recipient[7:]
-
-        if method != "REPLY":
-            # Invites and cancellations:
-
-            # Reuse or generate a token based on originator, toAddr, and
-            # event uid
-            token = self.db.getToken(originator, toAddr.lower(), icaluid)
-            if token is None:
-
-                # Because in the past the originator was sometimes in mailto:
-                # form, lookup an existing token by mailto: as well
-                organizerProperty = calendar.getOrganizerProperty()
-                organizerEmailAddress = organizerProperty.parameterValue("EMAIL", None)
-                if organizerEmailAddress is not None:
-                    token = self.db.getToken("mailto:%s" % (organizerEmailAddress.lower(),), toAddr.lower(), icaluid)
-
-            if token is None:
-                token = self.db.createToken(originator, toAddr.lower(), icaluid)
-                self.log_debug("Mail gateway created token %s for %s "
-                               "(originator), %s (recipient) and %s (icaluid)"
-                               % (token, originator, toAddr, icaluid))
-                inviteState = "new"
-
-            else:
-                self.log_debug("Mail gateway reusing token %s for %s "
-                               "(originator), %s (recipient) and %s (icaluid)"
-                               % (token, originator, toAddr, icaluid))
-                inviteState = "update"
-
-            fullServerAddress = settings['Address']
-            _ignore_name, serverAddress = email.utils.parseaddr(fullServerAddress)
-            pre, post = serverAddress.split('@')
-            addressWithToken = "%s+%s@%s" % (pre, token, post)
-
-            organizerProperty = calendar.getOrganizerProperty()
-            organizerEmailAddress = organizerProperty.parameterValue("EMAIL",
-                                                                     None)
-            organizerValue = organizerProperty.value()
-            organizerProperty.setValue("mailto:%s" % (addressWithToken,))
-
-            # If the organizer is also an attendee, update that attendee value
-            # to match
-            organizerAttendeeProperty = calendar.getAttendeeProperty(
-                [organizerValue])
-            if organizerAttendeeProperty is not None:
-                organizerAttendeeProperty.setValue("mailto:%s" %
-                                                   (addressWithToken,))
-
-            # The email's From will include the originator's real name email
-            # address if available.  Otherwise it will be the server's email
-            # address (without # + addressing)
-            if organizerEmailAddress:
-                orgEmail = fromAddr = organizerEmailAddress
-            else:
-                fromAddr = serverAddress
-                orgEmail = None
-            cn = calendar.getOrganizerProperty().parameterValue('CN', None)
-            if cn is None:
-                cn = u'Calendar Server'
-                orgCN = orgEmail
-            else:
-                orgCN = cn = cn.decode("utf-8")
-
-            # a unicode cn (rather than an encode string value) means the
-            # from address will get properly encoded per rfc2047 within the
-            # MIMEMultipart in generateEmail
-            formattedFrom = "%s <%s>" % (cn, fromAddr)
-
-            # Reply-to address will be the server+token address
-
-        else: # REPLY
-            inviteState = "reply"
-
-            # Look up the attendee property corresponding to the originator
-            # of this reply
-            originatorAttendeeProperty = calendar.getAttendeeProperty(
-                [originator])
-            formattedFrom = fromAddr = originator = ""
-            if originatorAttendeeProperty:
-                originatorAttendeeEmailAddress = (
-                    originatorAttendeeProperty.parameterValue("EMAIL", None)
-                )
-                if originatorAttendeeEmailAddress:
-                    formattedFrom = fromAddr = originator = (
-                        originatorAttendeeEmailAddress
-                    )
-
-            organizerMailto = str(calendar.getOrganizer())
-            if not organizerMailto.lower().startswith("mailto:"):
-                raise ValueError("ORGANIZER address '%s' must be mailto: "
-                                 "for REPLY." % (organizerMailto,))
-            orgEmail = organizerMailto[7:]
-
-            orgCN = calendar.getOrganizerProperty().parameterValue('CN', None)
-            addressWithToken = formattedFrom
-
-        # At the point we've created the token in the db, which we always
-        # want to do, but if this message is for an event completely in
-        # the past we don't want to actually send an email.
-        if not calendar.hasInstancesAfter(onlyAfter):
-            self.log_debug("Skipping IMIP message for old event")
-            return succeed(True)
-
-        # Now prevent any "internal" CUAs from being exposed by converting
-        # to mailto: if we have one
-        for attendeeProp in calendar.getAllAttendeeProperties():
-            cutype = attendeeProp.parameterValue('CUTYPE', None)
-            if cutype == "INDIVIDUAL":
-                cuaddr = normalizeCUAddr(attendeeProp.value())
-                if not cuaddr.startswith("mailto:"):
-                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
-                    if emailAddress:
-                        attendeeProp.setValue("mailto:%s" % (emailAddress,))
-
-        msgId, message = self.generateEmail(inviteState, calendar, orgEmail,
-            orgCN, attendees, formattedFrom, addressWithToken, recipient,
-            language=language)
-
-        if send:
-            self.log_debug("Sending: %s" % (message,))
-            def _success(result, msgId, fromAddr, toAddr):
-                self.log_info("Mail gateway sent message %s from %s to %s" %
-                    (msgId, fromAddr, toAddr))
-                return True
-
-            def _failure(failure, msgId, fromAddr, toAddr):
-                self.log_error("Mail gateway failed to send message %s from %s "
-                               "to %s (Reason: %s)" %
-                               (msgId, fromAddr, toAddr,
-                                failure.getErrorMessage()))
-                return False
-
-            deferred = defer.Deferred()
-
-            if settings["UseSSL"]:
-                contextFactory = ssl.ClientContextFactory()
-            else:
-                contextFactory = None
-
-            factory = ESMTPSenderFactory(
-                settings['Username'], settings['Password'],
-                fromAddr, toAddr, StringIO(str(message)), deferred,
-                contextFactory=contextFactory,
-                requireAuthentication=False,
-                requireTransportSecurity=settings["UseSSL"])
-
-            connect(GAIEndpoint(_reactor, settings["Server"], settings["Port"]),
-                    factory)
-            deferred.addCallback(_success, msgId, fromAddr, toAddr)
-            deferred.addErrback(_failure, msgId, fromAddr, toAddr)
-            return deferred
-        else:
-            return succeed((inviteState, calendar, orgEmail, orgCN, attendees,
-                formattedFrom, recipient, addressWithToken))
-
-
-    def getIconPath(self, details, canceled, language='en'):
-        iconDir = config.Scheduling.iMIP.MailIconsDirectory.rstrip("/")
-
-        if canceled:
-            iconName = "canceled.png"
-            iconPath = os.path.join(iconDir, iconName)
-            if os.path.exists(iconPath):
-                return iconPath
-            else:
-                return None
-
-        else:
-            month = int(details['month'])
-            day = int(details['day'])
-            with translationTo(language) as trans:
-                monthName = trans.monthAbbreviation(month)
-            iconName = "%02d.png" % (day,)
-            iconPath = os.path.join(iconDir, monthName.encode("utf-8"), iconName)
-            if not os.path.exists(iconPath):
-                # Try the generic (numeric) version
-                iconPath = os.path.join(iconDir, "%02d" % (month,), iconName)
-                if not os.path.exists(iconPath):
-                    return None
-            return iconPath
-
-
-    def generateEmail(self, inviteState, calendar, orgEmail, orgCN,
-                      attendees, fromAddress, replyToAddress, toAddress,
-                      language='en'):
-        """
-        Generate MIME text containing an iMIP invitation, cancellation, update
-        or reply.
-
-        @param inviteState: 'new', 'update', or 'reply'.
-
-        @type inviteState: C{str}
-
-        @param calendar: the iCalendar component to attach to the email.
-
-        @type calendar: L{twistedcaldav.ical.Component}
-
-        @param orgEmail: The email for the organizer, in C{localhost at domain}
-            format, or C{None} if the organizer has no email address.
-
-        @type orgEmail: C{str} or C{NoneType}
-
-        @param orgCN: Common name / display name for the organizer.
-
-        @type orgCN: C{unicode}
-
-        @param attendees: A C{list} of 2-C{tuple}s of (common name, email
-            address) similar to (orgEmail, orgCN).
-
-        @param fromAddress: the address to use in the C{From:} header of the
-            email.
-
-        @type fromAddress: C{str}
-
-        @param replyToAddress: the address to use in the C{Reply-To} header.
-
-        @type replyToAddress: C{str}
-
-        @param toAddress: the address to use in the C{To} header.
-
-        @type toAddress: C{str}
-
-        @param language: a 2-letter language code describing the target
-            language that the email should be generated in.
-
-        @type language: C{str}
-
-        @return: a 2-tuple of C{str}s: (message ID, message text).  The message
-            ID is the value of the C{Message-ID} header, and the message text is
-            the full MIME message, ready for transport over SMTP.
-        """
-
-        details = self.getEventDetails(calendar, language=language)
-        canceled = (calendar.propertyValue("METHOD") == "CANCEL")
-        iconPath = self.getIconPath(details, canceled, language=language)
-
-        subjectFormat, labels = localizedLabels(language, canceled, inviteState)
-        details.update(labels)
-
-        details['subject'] = subjectFormat % {'summary' : details['summary']}
-        details['iconName'] = iconName = "calicon.png"
-
-        plainText = self.renderPlainText(details, (orgCN, orgEmail),
-                                         attendees, canceled)
-
-        [addIcon, htmlText] = self.renderHTML(details, (orgCN, orgEmail),
-                                              attendees, canceled)
-
-        msg = MIMEMultipart()
-        msg["From"] = fromAddress
-        msg["Subject"] = details['subject']
-        msg["Reply-To"] = replyToAddress
-        msg["To"] = toAddress
-        msg["Date"] = rfc822date()
-        msgId = messageid()
-        msg["Message-ID"] = msgId
-
-        msgAlt = MIMEMultipart("alternative")
-        msg.attach(msgAlt)
-
-        # plain version
-        msgPlain = MIMEText(plainText, "plain", "UTF-8")
-        msgAlt.attach(msgPlain)
-
-        # html version
-        msgHtmlRelated = MIMEMultipart("related", type="text/html")
-        msgAlt.attach(msgHtmlRelated)
-
-        msgHtml = MIMEText(htmlText, "html", "UTF-8")
-        msgHtmlRelated.attach(msgHtml)
-
-        # an image for html version
-        if addIcon and iconPath != None and os.path.exists(iconPath):
-
-            with open(iconPath) as iconFile:
-                msgIcon = MIMEImage(iconFile.read(),
-                    _subtype='png;x-apple-mail-type=stationery;name="%s"' %
-                    (iconName,))
-
-            msgIcon.add_header("Content-ID", "<%s>" % (iconName,))
-            msgIcon.add_header("Content-Disposition", "inline;filename=%s" %
-                (iconName,))
-            msgHtmlRelated.attach(msgIcon)
-
-        calendarText = str(calendar)
-        # the icalendar attachment
-        self.log_debug("Mail gateway sending calendar body: %s"
-                       % (calendarText,))
-        msgIcal = MIMEText(calendarText, "calendar", "UTF-8")
-        method = calendar.propertyValue("METHOD").lower()
-        msgIcal.set_param("method", method)
-        msgIcal.add_header("Content-ID", "<invitation.ics>")
-        msgIcal.add_header("Content-Disposition",
-            "inline;filename=invitation.ics")
-        msg.attach(msgIcal)
-
-        return msgId, msg.as_string()
-
-
-    def renderPlainText(self, details, (orgCN, orgEmail), attendees, canceled):
-        """
-        Render text/plain message part based on invitation details and a flag
-        indicating whether the message is a cancellation.
-
-        @return: UTF-8 encoded text.
-
-        @rtype: C{str}
-        """
-        plainAttendeeList = []
-        for cn, mailto in attendees:
-            if cn:
-                plainAttendeeList.append(cn if not mailto else
-                    "%s <%s>" % (cn, mailto))
-            elif mailto:
-                plainAttendeeList.append("<%s>" % (mailto,))
-
-        details['plainAttendees'] = ", ".join(plainAttendeeList)
-
-        details['plainOrganizer'] = (orgCN if not orgEmail else
-            "%s <%s>" % (orgCN, orgEmail))
-
-        # plain text version
-        if canceled:
-            plainTemplate = plainCancelTemplate
-        else:
-            plainTemplate = plainInviteTemplate
-
-        return (plainTemplate % details).encode("UTF-8")
-
-
-    def renderHTML(self, details, organizer, attendees, canceled):
-        """
-        Render HTML message part based on invitation details and a flag
-        indicating whether the message is a cancellation.
-
-        @return: a 2-tuple of (should add icon (C{bool}), html text (C{str},
-            representing utf-8 encoded bytes)).  The first element indicates
-            whether the MIME generator needs to add a C{cid:} icon image part to
-            satisfy the HTML links.
-        """
-        orgCN, orgEmail = organizer
-
-        # TODO: htmlAttendees needs to be a separate element with a separate
-        # template fragment.  Luckily that fragment is the same regardless
-        # of the rest of the template.
-        htmlAttendees = []
-        first = True
-        for cn, mailto in attendees:
-            if not first:
-                htmlAttendees.append(u", ")
-            else:
-                first = False
-
-            if mailto:
-                if not cn:
-                    cn = mailto
-                htmlAttendees.append(
-                    tags.a(href="mailto:%s" % (mailto,))(cn)
-                )
-            else:
-                htmlAttendees.append(cn)
-
-        details['htmlAttendees'] = htmlAttendees
-
-        # TODO: htmlOrganizer is also some HTML that requires additional
-        # 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)
-        else:
-            details['htmlOrganizer'] = orgCN
-
-        templateDir = config.Scheduling.iMIP.MailTemplatesDirectory.rstrip("/")
-        templateName = "cancel.html" if canceled else "invite.html"
-        templatePath = os.path.join(templateDir, templateName)
-
-        if not os.path.exists(templatePath):
-            # Fall back to built-in simple templates:
-            if canceled:
-                htmlTemplate = htmlCancelTemplate
-            else:
-                htmlTemplate = htmlInviteTemplate
-        else: # HTML template file exists
-
-            with open(templatePath) as templateFile:
-                htmlTemplate = templateFile.read()
-
-        class EmailElement(Element):
-            loader = StringFormatTemplateLoader(lambda : StringIO(htmlTemplate),
-                                                "email")
-
-            @renderer
-            def email(self, request, tag):
-                return tag.fillSlots(**details)
-
-        textCollector = []
-        flattenString(None, EmailElement()).addCallback(textCollector.append)
-        htmlText = textCollector[0]
-
-        # If the template refers to an icon in a cid: link, it needs to be added
-        # in the MIME.
-        addIcon = (htmlTemplate.find("cid:%(iconName)s") != -1)
-        return (addIcon, htmlText)
-
-
-    def getEventDetails(self, calendar, language='en'):
-        """
-        Create a dictionary mapping slot names - specifically: summary,
-        description, location, dateInfo, timeInfo, durationInfo, recurrenceInfo,
-        url - with localized string values that should be placed into the HTML
-        and plain-text templates.
-
-        @param calendar: a L{Component} upon which to base the language.
-        @type calendar: L{Component}
-
-        @param language: a 2-letter language code.
-        @type language: C{str}
-
-        @return: a mapping from template slot name to localized text.
-        @rtype: a C{dict} mapping C{bytes} to C{unicode}.
-        """
-
-        # Get the most appropriate component
-        component = calendar.masterComponent()
-        if component is None:
-            component = calendar.mainComponent(True)
-
-        results = {}
-
-        dtStart = component.propertyValue('DTSTART')
-        results['month'] = dtStart.getMonth()
-        results['day'] = dtStart.getDay()
-
-        for propertyToResult in ['summary', 'description', 'location', 'url']:
-            result = component.propertyValue(propertyToResult.upper())
-            if result is None:
-                result = u""
-            else:
-                result = result.decode('utf-8')
-            results[propertyToResult] = result
-
-        with translationTo(language) as trans:
-            results['dateInfo'] = trans.date(component).decode('utf-8')
-            results['timeInfo'], duration = (x.decode('utf-8') for x in trans.time(component))
-            results['durationInfo'] = u"(%s)" % (duration,) if duration else u""
-
-            for propertyName in ('RRULE', 'RDATE', 'EXRULE', 'EXDATE',
-                                 'RECURRENCE-ID'):
-                if component.hasProperty(propertyName):
-                    results['recurrenceInfo'] = _("(Repeating)").decode('utf-8')
-                    break
-            else:
-                results['recurrenceInfo'] = u""
-
-        return results
-
-
-
-#
-# POP3
-#
-
-class POP3Service(service.Service, LoggingMixIn):
-
-    def __init__(self, settings, mailer):
-        if settings["UseSSL"]:
-            self.client = internet.SSLClient(settings["Server"],
-                settings["Port"],
-                POP3DownloadFactory(settings, mailer),
-                ssl.ClientContextFactory())
-        else:
-            self.client = internet.TCPClient(settings["Server"],
-                settings["Port"],
-                POP3DownloadFactory(settings, mailer))
-
-        self.mailer = mailer
-
-
-    def startService(self):
-        self.client.startService()
-
-
-    def stopService(self):
-        self.client.stopService()
-
-
-
-class POP3DownloadProtocol(pop3client.POP3Client, LoggingMixIn):
-    allowInsecureLogin = False
-
-    def serverGreeting(self, greeting):
-        self.log_debug("POP servergreeting")
-        pop3client.POP3Client.serverGreeting(self, greeting)
-        login = self.login(self.factory.settings["Username"],
-            self.factory.settings["Password"])
-        login.addCallback(self.cbLoggedIn)
-        login.addErrback(self.cbLoginFailed)
-
-
-    def cbLoginFailed(self, reason):
-        self.log_error("POP3 login failed for %s" %
-            (self.factory.settings["Username"],))
-        return self.quit()
-
-
-    def cbLoggedIn(self, result):
-        self.log_debug("POP loggedin")
-        return self.listSize().addCallback(self.cbGotMessageSizes)
-
-
-    def cbGotMessageSizes(self, sizes):
-        self.log_debug("POP gotmessagesizes")
-        downloads = []
-        for i in range(len(sizes)):
-            downloads.append(self.retrieve(i).addCallback(self.cbDownloaded, i))
-        return defer.DeferredList(downloads).addCallback(self.cbFinished)
-
-
-    def cbDownloaded(self, lines, id):
-        self.log_debug("POP downloaded message %d" % (id,))
-        self.factory.handleMessage("\r\n".join(lines))
-        self.log_debug("POP deleting message %d" % (id,))
-        self.delete(id)
-
-
-    def cbFinished(self, results):
-        self.log_debug("POP finished")
-        return self.quit()
-
-
-
-class POP3DownloadFactory(protocol.ClientFactory, LoggingMixIn):
-    protocol = POP3DownloadProtocol
-
-    def __init__(self, settings, mailer, reactor=None):
-        self.settings = settings
-        self.mailer = mailer
-        if reactor is None:
-            from twisted.internet import reactor
-        self.reactor = reactor
-        self.nextPoll = None
-        self.noisy = False
-
-
-    def retry(self, connector=None):
-        # TODO: if connector is None:
-
-        if connector is None:
-            if self.connector is None:
-                self.log_error("No connector to retry")
-                return
-            else:
-                connector = self.connector
-
-        def reconnector():
-            self.nextPoll = None
-            connector.connect()
-
-        self.log_debug("Scheduling next POP3 poll")
-        self.nextPoll = self.reactor.callLater(self.settings["PollingSeconds"],
-            reconnector)
-
-
-    def clientConnectionLost(self, connector, reason):
-        self.connector = connector
-        self.log_debug("POP factory connection lost")
-        self.retry(connector)
-
-
-    def clientConnectionFailed(self, connector, reason):
-        self.connector = connector
-        self.log_info("POP factory connection failed")
-        self.retry(connector)
-
-
-    def handleMessage(self, message):
-        self.log_debug("POP factory handle message")
-        self.log_debug(message)
-
-        return self.mailer.inbound(message)
-
-
-
-#
-# IMAP4
-#
-
-class IMAP4Service(service.Service):
-
-    def __init__(self, settings, mailer):
-
-        if settings["UseSSL"]:
-            self.client = internet.SSLClient(settings["Server"],
-                settings["Port"],
-                IMAP4DownloadFactory(settings, mailer),
-                ssl.ClientContextFactory())
-        else:
-            self.client = internet.TCPClient(settings["Server"],
-                settings["Port"],
-                IMAP4DownloadFactory(settings, mailer))
-
-        self.mailer = mailer
-
-
-    def startService(self):
-        self.client.startService()
-
-
-    def stopService(self):
-        self.client.stopService()
-
-
-
-class IMAP4DownloadProtocol(imap4.IMAP4Client, LoggingMixIn):
-
-    def serverGreeting(self, capabilities):
-        self.log_debug("IMAP servergreeting")
-        return self.authenticate(self.factory.settings["Password"]
-            ).addCallback(self.cbLoggedIn
-            ).addErrback(self.ebAuthenticateFailed)
-
-
-    def ebLogError(self, error):
-        self.log_error("IMAP Error: %s" % (error,))
-
-
-    def ebAuthenticateFailed(self, reason):
-        self.log_debug("IMAP authenticate failed for %s, trying login" %
-            (self.factory.settings["Username"],))
-        return self.login(self.factory.settings["Username"],
-            self.factory.settings["Password"]
-            ).addCallback(self.cbLoggedIn
-            ).addErrback(self.ebLoginFailed)
-
-
-    def ebLoginFailed(self, reason):
-        self.log_error("IMAP login failed for %s" %
-            (self.factory.settings["Username"],))
-        self.transport.loseConnection()
-
-
-    def cbLoggedIn(self, result):
-        self.log_debug("IMAP logged in [%s]" % (self.state,))
-        self.select("Inbox").addCallback(self.cbInboxSelected)
-
-
-    def cbInboxSelected(self, result):
-        self.log_debug("IMAP Inbox selected [%s]" % (self.state,))
-        allMessages = imap4.MessageSet(1, None)
-        self.fetchUID(allMessages, True).addCallback(self.cbGotUIDs)
-
-
-    def cbGotUIDs(self, results):
-        self.log_debug("IMAP got uids [%s]" % (self.state,))
-        self.messageUIDs = [result['UID'] for result in results.values()]
-        self.messageCount = len(self.messageUIDs)
-        self.log_debug("IMAP Inbox has %d messages" % (self.messageCount,))
-        if self.messageCount:
-            self.fetchNextMessage()
-        else:
-            # No messages; close it out
-            self.close().addCallback(self.cbClosed)
-
-
-    def fetchNextMessage(self):
-        self.log_debug("IMAP in fetchnextmessage [%s]" % (self.state,))
-        if self.messageUIDs:
-            nextUID = self.messageUIDs.pop(0)
-            messageListToFetch = imap4.MessageSet(nextUID)
-            self.log_debug("Downloading message %d of %d (%s)" %
-                (self.messageCount - len(self.messageUIDs), self.messageCount,
-                nextUID))
-            self.fetchMessage(messageListToFetch, True).addCallback(
-                self.cbGotMessage, messageListToFetch).addErrback(
-                    self.ebLogError)
-        else:
-            self.log_debug("Seeing if anything new has arrived")
-            # Go back and see if any more messages have come in
-            self.expunge().addCallback(self.cbInboxSelected)
-
-
-    def cbGotMessage(self, results, messageList):
-        self.log_debug("IMAP in cbGotMessage [%s]" % (self.state,))
-        try:
-            messageData = results.values()[0]['RFC822']
-        except IndexError:
-            # 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!")
-            self.fetchNextMessage()
-            return
-
-        d = self.factory.handleMessage(messageData)
-        if isinstance(d, defer.Deferred):
-            d.addCallback(self.cbFlagDeleted, messageList)
-        else:
-            # No deferred returned, so no need for addCallback( )
-            self.cbFlagDeleted(None, messageList)
-
-
-    def cbFlagDeleted(self, results, messageList):
-        self.addFlags(messageList, ("\\Deleted",),
-            uid=True).addCallback(self.cbMessageDeleted, messageList)
-
-
-    def cbMessageDeleted(self, results, messageList):
-        self.log_debug("IMAP in cbMessageDeleted [%s]" % (self.state,))
-        self.log_debug("Deleted message")
-        self.fetchNextMessage()
-
-
-    def cbClosed(self, results):
-        self.log_debug("IMAP in cbClosed [%s]" % (self.state,))
-        self.log_debug("Mailbox closed")
-        self.logout().addCallback(
-            lambda _: self.transport.loseConnection())
-
-
-    def rawDataReceived(self, data):
-        self.log_debug("RAW RECEIVED: %s" % (data,))
-        imap4.IMAP4Client.rawDataReceived(self, data)
-
-
-    def lineReceived(self, line):
-        self.log_debug("RECEIVED: %s" % (line,))
-        imap4.IMAP4Client.lineReceived(self, line)
-
-
-    def sendLine(self, line):
-        self.log_debug("SENDING: %s" % (line,))
-        imap4.IMAP4Client.sendLine(self, line)
-
-
-
-class IMAP4DownloadFactory(protocol.ClientFactory, LoggingMixIn):
-    protocol = IMAP4DownloadProtocol
-
-    def __init__(self, settings, mailer, reactor=None):
-        self.log_debug("Setting up IMAPFactory")
-
-        self.settings = settings
-        self.mailer = mailer
-        if reactor is None:
-            from twisted.internet import reactor
-        self.reactor = reactor
-        self.noisy = False
-
-
-    def buildProtocol(self, addr):
-        p = protocol.ClientFactory.buildProtocol(self, addr)
-        username = self.settings["Username"]
-        p.registerAuthenticator(imap4.CramMD5ClientAuthenticator(username))
-        p.registerAuthenticator(imap4.LOGINAuthenticator(username))
-        p.registerAuthenticator(imap4.PLAINAuthenticator(username))
-        return p
-
-
-    def handleMessage(self, message):
-        self.log_debug("IMAP factory handle message")
-        self.log_debug(message)
-
-        return self.mailer.inbound(message)
-
-
-    def retry(self, connector=None):
-        # TODO: if connector is None:
-
-        if connector is None:
-            if self.connector is None:
-                self.log_error("No connector to retry")
-                return
-            else:
-                connector = self.connector
-
-        def reconnector():
-            self.nextPoll = None
-            connector.connect()
-
-        self.log_debug("Scheduling next IMAP4 poll")
-        self.nextPoll = self.reactor.callLater(self.settings["PollingSeconds"],
-            reconnector)
-
-
-    def clientConnectionLost(self, connector, reason):
-        self.connector = connector
-        self.log_debug("IMAP factory connection lost")
-        self.retry(connector)
-
-
-    def clientConnectionFailed(self, connector, reason):
-        self.connector = connector
-        self.log_warn("IMAP factory connection failed")
-        self.retry(connector)
+    @param store: The store to copy tokens into
+    @type store: L{CommonDataStore}
+    """
+    oldDB = MailGatewayTokensDatabase(path)
+    txn = store.newTransaction()
+    for token, organizer, attendee, icaluid in oldDB.getAllTokens():
+        yield txn.imipCreateToken(organizer, attendee, icaluid, token=token)
+    yield txn.commit()
+    os.remove(oldDB.dbpath)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/resource.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/resource.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/scheduler.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/scheduler.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/scheduler.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -15,22 +15,17 @@
 ##
 
 from twext.python.log import Logger
-from twext.web2 import responsecode
 from twext.web2.dav.http import ErrorResponse
-from twext.web2.http import HTTPError
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet.defer import succeed
 from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.scheduling import addressmapping
 from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
-from twistedcaldav.scheduling.scheduler import RemoteScheduler, \
-    ScheduleResponseQueue
-import itertools
-import socket
+from twistedcaldav.scheduling.scheduler import RemoteScheduler
+from twistedcaldav.scheduling.scheduler import ScheduleResponseQueue
 
 
 """
-L{IMIPScheduler} - handles deliveries for scheduling messages being POSTed to the iMIP inbox.
+L{IMIPScheduler} - handles deliveries for scheduling messages retrieved via
+mail
 """
 
 __all__ = [
@@ -63,26 +58,14 @@
         pass
 
 
-    @inlineCallbacks
     def checkOriginator(self):
         """
-        Check the validity of the Originator header.
+        The originator always comes out of the tokens db
         """
+        self.originator = RemoteCalendarUser(self.originator)
+        return succeed(None)
 
-        # For remote requests we do not allow the originator to be a local user or one within our domain.
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
-        if originatorPrincipal or localUser:
-            log.err("Cannot use originator that is on this server: %s" % (self.originator,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["originator-denied"],
-                "Originator cannot be local to server",
-            ))
-        else:
-            self.originator = RemoteCalendarUser(self.originator)
 
-
     def checkOrganizerAsOriginator(self):
         pass
 
@@ -92,21 +75,4 @@
 
 
     def securityChecks(self):
-        """
-        Check that the connection is from the mail gateway
-        """
-        allowed = config.Scheduling['iMIP']['MailGatewayServer']
-        # Get the request IP and map to hostname.
-        clientip = self.request.remoteAddr.host
-        host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
-        for host in itertools.chain((host, clientip), aliases):
-            if host == allowed:
-                break
-        else:
-            log.err("Only %s is allowed to submit internal scheduling requests, not %s" % (allowed, host))
-            # TODO: verify this is the right response:
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["originator-denied"],
-                "Originator server not allowed to send to this server",
-            ))
+        pass

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_delivery.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_delivery.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_delivery.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_mailgateway.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_mailgateway.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_mailgateway.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2008-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,858 +14,42 @@
 # limitations under the License.
 ##
 
+import os
 
-from cStringIO import StringIO
-
-from pycalendar.datetime import PyCalendarDateTime
-
 from twisted.internet.defer import inlineCallbacks
-from twisted.python.modules import getModule
-from twisted.web.template import Element, renderer, flattenString
-
-from twistedcaldav.config import config, ConfigDict
-from twistedcaldav.directory import augment
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.ical import Component
-from twistedcaldav.scheduling.imip.mailgateway import MailGatewayTokensDatabase
-from twistedcaldav.scheduling.imip.mailgateway import MailHandler
-from twistedcaldav.scheduling.imip.mailgateway import StringFormatTemplateLoader
-from twistedcaldav.scheduling.imip.mailgateway import injectionSettingsFromURL
-from twistedcaldav.scheduling.imip.mailgateway import serverForOrganizer
-from twistedcaldav.scheduling.ischedule.localservers import Servers
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
 from twistedcaldav.test.util import TestCase
-from twistedcaldav.test.util import xmlFile, augmentsFile
+from twistedcaldav.test.util import xmlFile
+from txdav.common.datastore.test.util import buildStore
+from calendarserver.tap.util import getRootResource
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.imip.mailgateway import MailGatewayTokensDatabase
+from twistedcaldav.scheduling.imip.mailgateway import migrateTokensToStore
 
-import datetime
-import email
-import os
 
+class MailGatewayTokenDBTests(TestCase):
 
-def echo(*args):
-    return args
-
-initialInviteText = u"""BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REQUEST
-BEGIN:VEVENT
-UID:CFDD5E46-4F74-478A-9311-B3FF905449C3
-DTSTART:20100325T154500Z
-DTEND:20100325T164500Z
-ATTENDEE;CN=Th\xe9 Attendee;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TRU
- E:mailto:attendee at example.com
-ATTENDEE;CN=Th\xe9 Organizer;CUTYPE=INDIVIDUAL;EMAIL=organizer at example.com;P
- ARTSTAT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
-ATTENDEE;CN=An Attendee without CUTYPE;EMAIL=nocutype at example.com;PARTSTAT=A
- CCEPTED:urn:uuid:4DB528DC-3E60-44FA-9546-2A00FCDCFFAB
-ATTENDEE;EMAIL=nocn at example.com;PARTSTAT=ACCEPTED:urn:uuid:A592CF8B-4FC8-4E4
- F-B543-B2F29A7EEB0B
-ORGANIZER;CN=Th\xe9 Organizer;EMAIL=organizer at example.com:urn:uuid:C3B38B00-
- 4166-11DD-B22C-A07C87E02F6A
-SUMMARY:t\xe9sting outbound( )
-DESCRIPTION:awesome description with "<" and "&"
-END:VEVENT
-END:VCALENDAR
-"""
-
-class MailHandlerTests(TestCase):
-
+    @inlineCallbacks
     def setUp(self):
-        super(MailHandlerTests, self).setUp()
+        super(MailGatewayTokenDBTests, self).setUp()
 
-        self._setupServers(serverData)
-        self.directory = XMLDirectoryService(
-            {
-                'xmlFile' : xmlFile,
-                'augmentService' :
-                    augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
-            }
-        )
-        self.handler = MailHandler(dataRoot=":memory:", directory=self.directory)
-        module = getModule(__name__)
-        self.dataPath = module.filePath.sibling("data")
+        self.store = yield buildStore(self, None)
+        self.patch(config.DirectoryService.params, "xmlFile", xmlFile)
+        self.root = getRootResource(config, self.store)
+        self.directory = self.root.getDirectory()
 
-
-    def _setupServers(self, data):
-        self.patch(config, "ServerHostName", "caldav1.example.com")
-        self.patch(config, "HTTPPort", 8008)
-        self.patch(config.Servers, "Enabled", True)
-
-        xmlFile = StringIO(data)
-        servers = Servers
-        servers.load(xmlFile, ignoreIPLookupFailures=True)
-
-
-    def dataFile(self, name):
-        """
-        Get the contents of a given data file from the 'data/mail' test
-        fixtures directory.
-        """
-        return self.dataPath.child(name).getContent()
-
-
-    def test_serverDetection(self):
-        wsanchez = self.directory.recordWithShortName("users",
-            "wsanchez")
-        cdaboo = self.directory.recordWithShortName("users",
-            "cdaboo")
-        server = wsanchez.server()
-        self.assertEquals(server.uri, "http://caldav1.example.com:8008")
-        server = cdaboo.server()
-        self.assertEquals(server.uri, "https://caldav2.example.com:8843")
-
-        url = serverForOrganizer(self.directory,
-            "mailto:wsanchez at example.com")
-        self.assertEquals(url, "http://caldav1.example.com:8008")
-        url = serverForOrganizer(self.directory,
-            "mailto:cdaboo at example.com")
-        self.assertEquals(url, "https://caldav2.example.com:8843")
-
-
-    def test_purge_and_lowercase(self):
-        """
-        Ensure that purge( ) cleans out old tokens, and that lowercase( )
-        converts all mailto: to lowercase, since earlier server versions
-        didn't do that before inserting into the database.
-        """
-
-        # Insert an "old" token
-        token = "test_token_1"
-        organizer = "urn:uuid:19BFE23D-0269-46CA-877C-D4B521A7A9A5"
-        attendee = "mailto:you at example.com"
-        icaluid = "123"
-        pastDate = datetime.date(2009, 1, 1)
-        self.handler.db._db_execute(
-            """
-            insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
-            values (:1, :2, :3, :4, :5)
-            """, token, organizer, attendee, icaluid, pastDate
-        )
-        self.handler.db._db_commit()
-
-        # purge, and make sure we don't see that token anymore
-        self.handler.purge()
-        retrieved = self.handler.db.getToken(organizer, attendee, icaluid)
-        self.assertEquals(retrieved, None)
-
-        # Insert a token with (old-format) mailto:
-        token = "test_token_2"
-        organizer = "MailTo:Organizer at Example.com"
-        attendee = "MAILTO:YouTwo at Example.com"
-        icaluid = "456"
-        futureDate = datetime.date(2100, 1, 1)
-        self.handler.db._db_execute(
-            """
-            insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
-            values (:1, :2, :3, :4, :5)
-            """, token, organizer, attendee, icaluid, futureDate
-        )
-        self.handler.db._db_commit()
-
-        self.handler.lowercase()
-        retrieved = self.handler.db.getToken(organizer.lower(),
-            attendee.lower(), icaluid)
-        self.assertIsInstance(retrieved, str)
-        self.assertEquals(retrieved, token)
-
-        # Insert a token with (new-format) urn:uuid:
-        token = "test_token_3"
-        organizer = "urn:uuid:E0CF4031-676B-4668-A9D3-8F33A0212F70"
-        attendee = "MAILTO:YouTwo at Example.com"
-        icaluid = "789"
-        futureDate = datetime.date(2100, 1, 1)
-        self.handler.db._db_execute(
-            """
-            insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
-            values (:1, :2, :3, :4, :5)
-            """, token, organizer, attendee, icaluid, futureDate
-        )
-        self.handler.db._db_commit()
-
-        self.handler.lowercase()
-        retrieved = self.handler.db.getToken(organizer,
-            attendee.lower(), icaluid)
-        self.assertEquals(retrieved, token)
-
-
-    def test_checkDSNFailure(self):
-
-        data = {
-            'good_reply' : (False, None, None),
-            'dsn_failure_no_original' : (True, 'failed', None),
-            'dsn_failure_no_ics' : (True, 'failed', None),
-            'dsn_failure_with_ics' : (True, 'failed', '''BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-PRODID:-//example Inc.//iCal 3.0//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
-DTSTART;TZID=US/Pacific:20080812T094500
-DTEND;TZID=US/Pacific:20080812T104500
-ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:user01 at exam
- ple.com
-ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
- CTION;CN=nonexistant at example.com:mailto:nonexistant at example.com
-CREATED:20080812T191857Z
-DTSTAMP:20080812T191932Z
-ORGANIZER;CN=User 01:mailto:xyzzy+8e16b897-d544-4217-88e9-a363d08
- 46f6c at example.com
-SEQUENCE:2
-SUMMARY:New Event
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-'''),
-        }
-
-        for filename, expected in data.iteritems():
-            msg = email.message_from_string(self.dataFile(filename))
-            self.assertEquals(self.handler.checkDSN(msg), expected)
-
-
-    def test_processDSN(self):
-
-        template = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-PRODID:-//example Inc.//iCal 3.0//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
-DTSTART;TZID=US/Pacific:20080812T094500
-DTEND;TZID=US/Pacific:20080812T104500
-ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:user01 at exam
- ple.com
-ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
- CTION;CN=nonexistant at example.com:mailto:nonexistant at example.com
-CREATED:20080812T191857Z
-DTSTAMP:20080812T191932Z
-ORGANIZER;CN=User 01:mailto:xyzzy+%s at example.com
-SEQUENCE:2
-SUMMARY:New Event
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-"""
-
-        # Make sure an unknown token is not processed
-        calBody = template % "bogus_token"
-        self.assertEquals(self.handler.processDSN(calBody, "xyzzy", echo),
-           None)
-
-        # Make sure a known token *is* processed
-        token = self.handler.db.createToken(
-            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
-            "mailto:user02 at example.com", "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C")
-        calBody = template % token
-        _ignore_url, organizer, attendee, calendar, msgId = self.handler.processDSN(calBody,
-            "xyzzy", echo)
-        self.assertEquals(organizer, 'urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500')
-        self.assertEquals(attendee, 'mailto:user02 at example.com')
-        self.assertEquals(str(calendar), """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-PRODID:-//example Inc.//iCal 3.0//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
-DTSTART;TZID=US/Pacific:20080812T094500
-DTEND;TZID=US/Pacific:20080812T104500
-CREATED:20080812T191857Z
-DTSTAMP:20080812T191932Z
-ORGANIZER;CN=User 01:urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500
-REQUEST-STATUS:5.1;Service unavailable
-SEQUENCE:2
-SUMMARY:New Event
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"))
-        self.assertEquals(msgId, 'xyzzy')
-
-
-    def test_processReply(self):
-        msg = email.message_from_string(self.dataFile('good_reply'))
-
-        # Make sure an unknown token is not processed
-        result = self.handler.processReply(msg, echo)
-        self.assertEquals(result, None)
-
-        # Make sure a known token *is* processed
-        self.handler.db.createToken(
-            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
-            "mailto:xyzzy at example.com",
-            icaluid="1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
-            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
-        )
-        url, organizer, attendee, _ignore_calendar, msgId = self.handler.processReply(msg, echo)
-        self.assertEquals(url, "https://caldav2.example.com:8843")
-        self.assertEquals(organizer,
-                          'urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500')
-        self.assertEquals(attendee, 'mailto:xyzzy at example.com')
-        self.assertEquals(msgId,
-                          '<1983F777-BE86-4B98-881E-06D938E60920 at example.com>')
-
-
-    def test_injectionSettingsFromURL(self):
-        testData = (
-            (
-                None,
-                {
-                    "Scheduling": {
-                        "iMIP" : {
-                            "MailGatewayServer" : "localhost",
-                        },
-                    },
-                    "EnableSSL" : True,
-                    "ServerHostName" : "calendar.example.com",
-                    "HTTPPort" : 1111,
-                    "SSLPort" : 2222,
-                },
-                "https://localhost:2222/inbox/",
-            ),
-            (
-                None,
-                {
-                    "Scheduling": {
-                        "iMIP" : {
-                            "MailGatewayServer" : "mailgateway.example.com",
-                        },
-                    },
-                    "EnableSSL" : False,
-                    "ServerHostName" : "calendar.example.com",
-                    "HTTPPort" : 1111,
-                    "SSLPort" : 2222,
-                },
-                "http://calendar.example.com:1111/inbox/",
-            ),
-            (
-                "https://calendar.example.com:1234/",
-                { },
-                "https://calendar.example.com:1234/inbox/",
-            ),
-            (
-                "https://calendar.example.com:1234",
-                { },
-                "https://calendar.example.com:1234/inbox/",
-            ),
-        )
-
-        for url, configData, expected in testData:
-            self.assertEquals(
-                expected,
-                injectionSettingsFromURL(url, ConfigDict(mapping=configData))
-            )
-
-
-    def test_processReplyMissingOrganizer(self):
-        msg = email.message_from_string(self.dataFile('reply_missing_organizer'))
-        # stick the token in the database first
-        self.handler.db.createToken(
-            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
-            "mailto:xyzzy at example.com",
-            icaluid="1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
-            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
-        )
-
-        _ignore_url, organizer, _ignore_attendee, calendar, _ignore_msgId = self.handler.processReply(
-            msg, echo)
-        organizerProp = calendar.mainComponent().getOrganizerProperty()
-        self.assertTrue(organizerProp is not None)
-        self.assertEquals(organizer,
-                          "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500")
-
-
-    def test_processReplyMissingAttendee(self):
-        msg = email.message_from_string(self.dataFile('reply_missing_attendee'))
-
-        # stick the token in the database first
-        self.handler.db.createToken(
-            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
-            "mailto:xyzzy at example.com",
-            icaluid="1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
-            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
-        )
-
-        _ignore_url, _ignore_organizer, attendee, calendar, _ignore_msgId = self.handler.processReply(
-            msg, echo)
-
-        # Since the expected attendee was missing, the reply processor should
-        # have added an attendee back in with a "5.1;Service unavailable"
-        # schedule-status
-        attendeeProp = calendar.mainComponent().getAttendeeProperty([attendee])
-        self.assertEquals(attendeeProp.parameterValue("SCHEDULE-STATUS"),
-                          iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
-
-    def test_processReplyMissingAttachment(self):
-
-        msg = email.message_from_string(
-            self.dataFile('reply_missing_attachment')
-        )
-        # stick the token in the database first
-        self.handler.db.createToken(
-            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
-            "mailto:xyzzy at example.com",
-            icaluid="1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
-            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
-        )
-
-        self.assertEquals(
-            self.handler.processReply(msg, echo, testMode=True),
-            ("cdaboo at example.com", "xyzzy at example.com")
-        )
-
-
     @inlineCallbacks
-    def test_outbound(self):
-        """
-        Make sure outbound( ) stores tokens properly so they can be looked up
-        """
+    def test_migrate(self):
+        self.path = self.mktemp()
+        os.mkdir(self.path)
+        oldDB = MailGatewayTokensDatabase(self.path)
+        oldDB.createToken("urn:uuid:user01", "mailto:attendee at example.com",
+            "icaluid1", token="token1")
+        yield migrateTokensToStore(self.path, self.store)
+        txn = self.store.newTransaction()
+        results = yield (txn.imipLookupByToken("token1"))
+        organizer, attendee, icaluid = results[0]
+        yield txn.commit()
+        self.assertEquals(organizer, "urn:uuid:user01")
+        self.assertEquals(attendee, "mailto:attendee at example.com")
+        self.assertEquals(icaluid, "icaluid1")
 
-        config.Scheduling.iMIP.Sending.Address = "server at example.com"
-        self.patch(config.Localization, "LocalesDirectory", os.path.join(os.path.dirname(__file__), "locales"))
-
-        data = (
-            # Initial invite
-            (
-                initialInviteText,
-                "CFDD5E46-4F74-478A-9311-B3FF905449C3",
-                "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
-                "mailto:attendee at example.com",
-                "new",
-                "organizer at example.com",
-                u"Th\xe9 Organizer",
-                [
-                    (u'Th\xe9 Attendee', u'attendee at example.com'),
-                    (u'Th\xe9 Organizer', u'organizer at example.com'),
-                    (u'An Attendee without CUTYPE', u'nocutype at example.com'),
-                    (None, u'nocn at example.com'),
-                ],
-                u"Th\xe9 Organizer <organizer at example.com>",
-                "attendee at example.com",
-            ),
-
-            # Update
-            (
-                """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:
- mailto:attendee at example.com
-ATTENDEE;CN=The 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
- 66-11DD-B22C-A07C87E02F6A
-SUMMARY:testing outbound( ) *update*
-END:VEVENT
-END:VCALENDAR
-""",
-                "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'The Attendee', u'attendee at example.com'),
-                    (u'The Organizer', u'organizer at example.com')
-                ],
-                "The Organizer <organizer at example.com>",
-                "attendee at example.com",
-            ),
-
-            # Reply
-            (
-                """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
- AT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
-ORGANIZER;CN=The Organizer;EMAIL=organizer at example.com:mailto:organizer at exam
- ple.com
-SUMMARY:testing outbound( ) *reply*
-END:VEVENT
-END:VCALENDAR
-""",
-                None,
-                "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
-                "mailto:organizer at example.com",
-                "reply",
-                "organizer at example.com",
-                "The Organizer",
-                [
-                    (u'The Attendee', u'attendee at example.com'),
-                ],
-                "attendee at example.com",
-                "organizer at example.com",
-            ),
-
-        )
-        for (inputCalendar, UID, inputOriginator, inputRecipient, inviteState,
-            outputOrganizerEmail, outputOrganizerName, outputAttendeeList,
-            outputFrom, outputRecipient) in data:
-
-            (actualInviteState, actualCalendar, actualOrganizerEmail,
-                actualOrganizerName, actualAttendeeList, actualFrom,
-                actualRecipient, actualReplyTo) = (yield self.handler.outbound(
-                    inputOriginator,
-                    inputRecipient,
-                    Component.fromString(inputCalendar.replace("\n", "\r\n")),
-                    language="ja",
-                    send=False,
-                    onlyAfter=PyCalendarDateTime(2010, 1, 1, 0, 0, 0))
-                )
-
-            self.assertEquals(actualInviteState, inviteState)
-            self.assertEquals(actualOrganizerEmail, outputOrganizerEmail)
-            self.assertEquals(actualOrganizerName, outputOrganizerName)
-            self.assertEquals(actualAttendeeList, outputAttendeeList)
-            self.assertEquals(actualFrom, outputFrom)
-            self.assertEquals(actualRecipient, outputRecipient)
-
-            if UID: # The organizer is local, and server is sending to remote
-                    # attendee
-
-                token = self.handler.db.getToken(inputOriginator,
-                    inputRecipient, UID)
-                self.assertNotEquals(token, None)
-                self.assertEquals(actualReplyTo,
-                    "server+%s at example.com" % (token,))
-
-                # Make sure attendee property for organizer exists and matches
-                # the CUA of the organizer property
-                orgValue = actualCalendar.getOrganizerProperty().value()
-                self.assertEquals(
-                    orgValue,
-                    actualCalendar.getAttendeeProperty([orgValue]).value()
-                )
-
-            else: # Reply only -- the attendee is local, and server is sending reply to remote organizer
-
-                self.assertEquals(actualReplyTo, actualFrom)
-
-            # Check that we don't send any messages for events completely in
-            # the past.
-            result = (yield self.handler.outbound(
-                    inputOriginator,
-                    inputRecipient,
-                    Component.fromString(inputCalendar.replace("\n", "\r\n")),
-                    send=False,
-                    onlyAfter=PyCalendarDateTime(2012, 1, 1, 0, 0, 0))
-                )
-            self.assertEquals(result, True)
-
-
-    @inlineCallbacks
-    def test_mailtoTokens(self):
-        """
-        Make sure old mailto tokens are still honored
-        """
-
-        organizerEmail = "mailto:organizer at example.com"
-
-        config.Scheduling.iMIP.Sending.Address = "server at example.com"
-
-        # Explictly store a token with mailto: CUA for organizer
-        # (something that doesn't happen any more, but did in the past)
-        origToken = self.handler.db.createToken(organizerEmail,
-            "mailto:attendee at example.com",
-            "CFDD5E46-4F74-478A-9311-B3FF905449C3")
-
-        inputCalendar = initialInviteText
-        UID = "CFDD5E46-4F74-478A-9311-B3FF905449C3"
-        inputOriginator = "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A"
-        inputRecipient = "mailto:attendee at example.com"
-
-        (_ignore_actualInviteState, _ignore_actualCalendar, _ignore_actualOrganizerEmail,
-            _ignore_actualOrganizerName, _ignore_actualAttendeeList, _ignore_actualFrom,
-            _ignore_actualRecipient, _ignore_actualReplyTo) = (yield self.handler.outbound(
-                inputOriginator,
-                inputRecipient,
-                Component.fromString(inputCalendar.replace("\n", "\r\n")),
-                send=False,
-                onlyAfter=PyCalendarDateTime(2010, 1, 1, 0, 0, 0))
-            )
-
-        # Verify we didn't create a new token...
-        token = self.handler.db.getToken(inputOriginator,
-            inputRecipient, UID)
-        self.assertEquals(token, None)
-
-        # But instead kept the old one...
-        token = self.handler.db.getToken(organizerEmail,
-            inputRecipient, UID)
-        self.assertEquals(token, origToken)
-
-
-    def generateSampleEmail(self):
-        """
-        Invoke L{MailHandler.generateEmail} and parse the result.
-        """
-        calendar = Component.fromString(initialInviteText)
-        msgID, msgTxt = self.handler.generateEmail(
-            inviteState='new',
-            calendar=calendar,
-            orgEmail=u"user01 at localhost",
-            orgCN=u"User Z\xe9ro One",
-            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)
-        return msgID, message
-
-
-    def test_generateEmail(self):
-        """
-        L{MailHandler.generateEmail} generates a MIME-formatted email with a
-        text/plain part, a text/html part, and a text/calendar part.
-        """
-        msgID, message = self.generateSampleEmail()
-        self.assertEquals(message['Message-ID'], msgID)
-        expectedTypes = set(["text/plain", "text/html", "text/calendar"])
-        actualTypes = set([
-            part.get_content_type() for part in message.walk()
-            if part.get_content_type().startswith("text/")
-        ])
-        self.assertEquals(actualTypes, expectedTypes)
-
-
-    def test_emailEncoding(self):
-        """
-        L{MailHandler.generateEmail} will preserve any non-ASCII characters
-        present in the fields that it formats in the message body.
-        """
-        _ignore_msgID, message = self.generateSampleEmail()
-        textPart = partByType(message, "text/plain")
-        htmlPart = partByType(message, "text/html")
-
-        plainText = textPart.get_payload(decode=True).decode(
-            textPart.get_content_charset()
-        )
-        htmlText = htmlPart.get_payload(decode=True).decode(
-            htmlPart.get_content_charset()
-        )
-
-        self.assertIn(u"Us\u00e9r One", plainText)
-        self.assertIn(u'<a href="mailto:user01 at localhost">Us\u00e9r One</a>',
-                      htmlText)
-
-        # The same assertion, but with the organizer's form.
-        self.assertIn(
-            u'<a href="mailto:user01 at localhost">User Z\u00e9ro One</a>',
-            htmlText)
-
-
-    def test_emailQuoting(self):
-        """
-        L{MailHandler.generateEmail} will HTML-quote all relevant fields in the
-        HTML part, but not the text/plain part.
-        """
-        _ignore_msgID, message = self.generateSampleEmail()
-        htmlPart = partByType(message, "text/html").get_payload(decode=True)
-        plainPart = partByType(message, "text/plain").get_payload(decode=True)
-        expectedPlain = 'awesome description with "<" and "&"'
-        expectedHTML = expectedPlain.replace("&", "&amp;").replace("<", "&lt;")
-
-        self.assertIn(expectedPlain, plainPart)
-        self.assertIn(expectedHTML, htmlPart)
-
-
-    def test_stringFormatTemplateLoader(self):
-        """
-        L{StringFormatTemplateLoader.load} will convert a template with
-        C{%(x)s}-format slots by converting it to a template with C{<t:slot
-        name="x" />} slots, and a renderer on the document element named
-        according to the constructor argument.
-        """
-        class StubElement(Element):
-            loader = StringFormatTemplateLoader(
-                lambda : StringIO(
-                    "<test><alpha>%(slot1)s</alpha>%(other)s</test>"
-                ),
-                "testRenderHere"
-            )
-
-            @renderer
-            def testRenderHere(self, request, tag):
-                return tag.fillSlots(slot1="hello",
-                                     other="world")
-        result = []
-        flattenString(None, StubElement()).addCallback(result.append)
-        self.assertEquals(result,
-                          ["<test><alpha>hello</alpha>world</test>"])
-
-
-    def test_templateLoaderWithAttributes(self):
-        """
-        L{StringFormatTemplateLoader.load} will convert a template with
-        C{%(x)s}-format slots inside attributes into t:attr elements containing
-        t:slot slots.
-        """
-        class StubElement(Element):
-            loader = StringFormatTemplateLoader(
-                lambda : StringIO(
-                    '<test><alpha beta="before %(slot1)s after">inner</alpha>'
-                    '%(other)s</test>'
-                ),
-                "testRenderHere"
-            )
-
-            @renderer
-            def testRenderHere(self, request, tag):
-                return tag.fillSlots(slot1="hello",
-                                     other="world")
-        result = []
-        flattenString(None, StubElement()).addCallback(result.append)
-        self.assertEquals(result,
-                          ['<test><alpha beta="before hello after">'
-                           'inner</alpha>world</test>'])
-
-
-    def test_templateLoaderTagSoup(self):
-        """
-        L{StringFormatTemplateLoader.load} will convert a template with
-        C{%(x)s}-format slots into t:slot slots, and render a well-formed output
-        document, even if the input is malformed (i.e. missing necessary closing
-        tags).
-        """
-        class StubElement(Element):
-            loader = StringFormatTemplateLoader(
-                lambda : StringIO(
-                    '<test><alpha beta="before %(slot1)s after">inner</alpha>'
-                    '%(other)s'
-                ),
-                "testRenderHere"
-            )
-
-            @renderer
-            def testRenderHere(self, request, tag):
-                return tag.fillSlots(slot1="hello",
-                                     other="world")
-        result = []
-        flattenString(None, StubElement()).addCallback(result.append)
-        self.assertEquals(result,
-                          ['<test><alpha beta="before hello after">'
-                           'inner</alpha>world</test>'])
-
-
-
-def partByType(message, contentType):
-    """
-    Retrieve a MIME part from an L{email.message.Message} based on a content
-    type.
-    """
-    for part in message.walk():
-        if part.get_content_type() == contentType:
-            return part
-    raise KeyError(contentType)
-
-
-
-class MailGatewayTokensDatabaseTests(TestCase):
-
-    def setUp(self):
-        TestCase.setUp(self)
-        self.db = MailGatewayTokensDatabase(":memory:")
-
-
-    def test_tokens(self):
-        self.assertEquals(self.db.lookupByToken("xyzzy"), None)
-
-        token = self.db.createToken("organizer", "attendee", "icaluid")
-        self.assertEquals(self.db.getToken("organizer", "attendee", "icaluid"),
-                          token)
-        self.assertEquals(self.db.lookupByToken(token),
-            ("organizer", "attendee", "icaluid"))
-        self.db.deleteToken(token)
-        self.assertEquals(self.db.lookupByToken(token), None)
-
-
-serverData = """<?xml version="1.0" encoding="utf-8"?>
-<servers>
-  <server>
-    <id>00001</id>
-    <uri>http://caldav1.example.com:8008</uri>
-    <allowed-from>127.0.0.1</allowed-from>
-    <shared-secret>foobar</shared-secret>
-  </server>
-  <server>
-    <id>00002</id>
-    <uri>https://caldav2.example.com:8843</uri>
-    <partitions>
-        <partition>
-            <id>A</id>
-            <uri>https://machine1.example.com:8443</uri>
-        </partition>
-        <partition>
-            <id>B</id>
-            <uri>https://machine2.example.com:8443</uri>
-        </partition>
-    </partitions>
-  </server>
-</servers>
-"""

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/implicit.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/implicit.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -452,7 +452,7 @@
 
 
     @inlineCallbacks
-    def hasCalendarResourceUIDSomewhereElse(self, check_resource, check_uri, type):
+    def hasCalendarResourceUIDSomewhereElse(self, check_resource, check_uri, mode):
         """
         See if a calendar component with a matching UID exists anywhere in the calendar home of the
         current recipient owner and is not the resource being targeted.
@@ -467,7 +467,7 @@
         calendar_home = yield calendar_owner_principal.calendarHome(self.request)
 
         # Check for matching resource somewhere else in the home
-        foundElsewhere = (yield calendar_home.hasCalendarResourceUIDSomewhereElse(self.uid, check_resource, type))
+        foundElsewhere = (yield calendar_home.hasCalendarResourceUIDSomewhereElse(self.uid, check_resource, mode))
         if foundElsewhere:
             log.debug("Implicit - found component with same UID in a different collection: %s" % (check_uri,))
             raise HTTPError(ErrorResponse(
@@ -613,6 +613,8 @@
                 # Check for removed attendees
                 if not recurrence_reschedule:
                     self.findRemovedAttendees()
+                else:
+                    self.findRemovedAttendeesOnRecurrenceChange()
 
                 # For now we always bump the sequence number on modifications because we cannot track DTSTAMP on
                 # the Attendee side. But we check the old and the new and only bump if the client did not already do it.
@@ -736,6 +738,10 @@
         """
         Look for attendees that have been removed from any instances. Save those off
         as users that need to be sent a cancel.
+
+        This method does not handle a full recurrence change (one where the RRULE pattern
+        changes or the associated DTSTART changes). For the full change we will have another
+        method to handle that.
         """
 
         # Several possibilities for when CANCELs need to be sent:
@@ -795,15 +801,39 @@
                 # as the set of attendees in the override may be different from the master set, but the override
                 # will have been accounted for by the previous attendee/instance logic.
                 if exdate not in removedInstances:
-                    self.cancelledAttendees.add((attendee, exdate))
+                    self.cancelledAttendees.add((attendee, exdate,))
 
         # For overridden instances added, check whether any attendees were removed from the master
         for attendee, _ignore in master_attendees:
             for rid in addedInstances:
                 if (attendee, rid) not in mappedNew and rid not in oldexdates:
-                    self.cancelledAttendees.add((attendee, rid))
+                    self.cancelledAttendees.add((attendee, rid,))
 
 
+    def findRemovedAttendeesOnRecurrenceChange(self):
+        """
+        Look for attendees that have been removed during a change to the overall recurrence.
+
+        This is a special case to try and minimize the number of cancels sent to just those
+        attendees actually removed. The basic policy is this:
+
+        1) If an attendee is present in the master component of the new event, they never
+        receive a CANCEL as they will always receive a REQUEST with the entire new event
+        data. i.e., they will see an event "replacement" rather than a cancel+new request.
+
+        2) For all attendees in the old event, not in the new master, send a cancel of
+        the master or each override they appear in. That happens even if they appear in an
+        override in the new calendar, since in all likelihood there is no guaranteed exact
+        mapping between old and new instances.
+        """
+
+        self.cancelledAttendees = set()
+        new_master_attendees = set([attendee for attendee, _ignore in self.calendar.masterComponent().getAttendeesByInstance(onlyScheduleAgentServer=True)])
+        for attendee, rid in self.oldAttendeesByInstance:
+            if attendee not in new_master_attendees:
+                self.cancelledAttendees.add((attendee, rid,))
+
+
     def coerceAttendeesPartstatOnCreate(self):
         """
         Make sure any attendees handled by the server start off with PARTSTAT=NEEDS-ACTION as
@@ -1239,6 +1269,13 @@
         if oldcalendar is None:
             oldcalendar = self.organizer_calendar
             oldcalendar.attendeesView((self.attendee,), onlyScheduleAgentServer=True)
+            if oldcalendar.mainType() is None:
+                log.debug("Implicit - attendee '%s' cannot use an event they are not an attendee of, UID: '%s'" % (self.attendee, self.uid))
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (caldav_namespace, "valid-attendee-change"),
+                    "Cannot use an event when not listed as an attendee in the organizer's copy",
+                ))
         differ = iCalDiff(oldcalendar, self.calendar, self.do_smart_merge)
         return differ.attendeeMerge(self.attendee)
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/__init__.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/__init__.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/delivery.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/delivery.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/delivery.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,27 +16,28 @@
 
 from StringIO import StringIO
 
-from twisted.internet.defer import inlineCallbacks, DeferredList, returnValue
-from twisted.internet.protocol import Factory
+from calendarserver.version import version
 
-from twisted.python.failure import Failure
-
+from twext.internet.gaiendpoint import GAIEndpoint
+from twext.python.log import Logger
 from twext.web2 import responsecode
 from twext.web2.client.http import ClientRequest
 from twext.web2.client.http import HTTPClientProtocol
+from twext.web2.dav.http import ErrorResponse
 from twext.web2.dav.util import davXMLFromStream, joinURL, allDataFromStream
 from twext.web2.http import HTTPError
 from twext.web2.http_headers import Headers
 from twext.web2.http_headers import MimeType
 from twext.web2.stream import MemoryStream
 
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.python.log import Logger
-from twext.web2.dav.http import ErrorResponse
+from twisted.internet.defer import inlineCallbacks, DeferredList, returnValue
+from twisted.internet.protocol import Factory
+from twisted.python.failure import Failure
 
 from twistedcaldav.accounting import accountingEnabledForCategory, emitAccounting
 from twistedcaldav.client.pool import _configuredClientContextFactory
 from twistedcaldav.config import config
+from twistedcaldav.ical import normalizeCUAddress, Component
 from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser, RemoteCalendarUser, \
     OtherServerCalendarUser
 from twistedcaldav.scheduling.delivery import DeliveryService
@@ -49,8 +50,8 @@
     ResponseDescription, Error
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
 from twistedcaldav.util import utf8String, normalizationLookup
+
 from urlparse import urlsplit
-from twistedcaldav.ical import normalizeCUAddress
 
 """
 Handles the sending of iSchedule scheduling messages. Used for both cross-domain scheduling,
@@ -346,11 +347,11 @@
         whilst following redirects.
         """
 
-        self._prepareHeaders(host, port)
-        self._prepareData()
+        component, method = self._prepareData()
+        self._prepareHeaders(host, port, component, method)
 
 
-    def _prepareHeaders(self, host, port):
+    def _prepareHeaders(self, host, port, component, method):
         """
         Always generate a new set of headers because the Host may varying during redirects,
         or we may need to dump DKIM added headers during a redirect.
@@ -359,7 +360,6 @@
 
         self.headers = Headers()
         self.headers.setHeader("Host", utf8String(host + ":%s" % (port,)))
-        self.sign_headers.append("Host")
 
         # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
         originator = self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee
@@ -370,14 +370,24 @@
         for recipient in self.recipients:
             self.headers.addRawHeader("Recipient", utf8String(recipient.cuaddr))
 
-        # Remember to "over sign" the Recipient header
-        self.sign_headers.append("Recipient+")
+        # Only one Recipient header as they get concatenated in ischedule-relaxed canonicalization
+        self.sign_headers.append("Recipient")
 
         self._doAuthentication()
 
-        self.headers.setHeader("Content-Type", MimeType("text", "calendar", params={"charset": "utf-8"}))
+        self.headers.setHeader("Content-Type", MimeType(
+            "text", "calendar",
+            params={
+                "charset": "utf-8",
+                "component": component,
+                "method": method,
+            }
+        ))
         self.sign_headers.append("Content-Type")
 
+        self.headers.setHeader("User-Agent", "CalendarServer/%s" % (version,))
+        self.sign_headers.append("User-Agent")
+
         # Add any additional headers
         for name, value in self.server.moreHeaders:
             self.headers.addRawHeader(name, value)
@@ -416,7 +426,15 @@
             if self.scheduler.isfreebusy:
                 normalizedCalendar.removeAllButTheseAttendees([recipient.cuaddr for recipient in self.recipients])
 
+            component = normalizedCalendar.mainType()
+            method = normalizedCalendar.propertyValue("METHOD")
             self.data = str(normalizedCalendar)
+            return component, method
+        else:
+            cal = Component.fromString(self.data)
+            component = cal.mainType()
+            method = cal.propertyValue("METHOD")
+            return component, method
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/dkim.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/dkim.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/dkim.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -33,8 +33,6 @@
 from Crypto.Signature import PKCS1_v1_5
 
 import base64
-import binascii
-import collections
 import hashlib
 import os
 import textwrap
@@ -61,9 +59,9 @@
 ISCHEDULE_VERSION = "iSchedule-Version"
 ISCHEDULE_VERSION_VALUE = "1.0"
 ISCHEDULE_MESSAGE_ID = "iSchedule-Message-ID"
+ISCHEDULE_CAPABILITIES = "iSchedule-Capabilities"
 
 
-
 class DKIMUtils(object):
     """
     Some useful functions.
@@ -207,7 +205,18 @@
 
 
     @staticmethod
-    def canonicalizeHeader(name, value, remove_b=None):
+    def canonicalizeHeader(name, value, dkim_tags=None, method="ischedule-relaxed"):
+
+        if method == "relaxed":
+            return DKIMUtils.relaxedHeader(name, value, dkim_tags)
+        elif method == "ischedule-relaxed":
+            return DKIMUtils.ischeduleHeader(name, value, dkim_tags)
+        else:
+            assert "Invalid header canonicalization method: %s" % (method,)
+
+
+    @staticmethod
+    def relaxedHeader(name, value, dkim_tags=None):
         """
         Canonicalize the header using "relaxed" method. Optionally remove the b= value from
         any DKIM-Signature present.
@@ -220,25 +229,70 @@
         @type name: C{str}
         @param value: header value
         @type value: C{str}
-        @param remove_b: the b= value to remove, or C{None} if no removal needed
-        @type remove_b: C{str} or C{None}
+        @param dkim_tags: the extracted DKIM tags, or C{None} if no removal needed
+        @type dkim_tags: C{dict} or C{None}
         """
 
+        # Special case DKIM-Signature: remove the b= value for signature
+        name = name.lower()
+        if dkim_tags is not None and name == DKIM_SIGNATURE.lower():
+            value = DKIMUtils.canonicalizeDKIMHeaderFields(value, dkim_tags)
+
         # Basic relaxed behavior
-        name = name.lower()
         value = " ".join(value.split())
 
+        crlf = "" if name == DKIM_SIGNATURE.lower() else "\r\n"
+        return "%s:%s%s" % (name, value, crlf)
+
+
+    @staticmethod
+    def ischeduleHeader(name, value, dkim_tags=None):
+        """
+        Canonicalize the header using "ischedule-relaxed" method. Optionally remove the b= value from
+        any DKIM-Signature present.
+
+        FIXME: this needs to be smarter about where valid WSP can occur in a header. Right now it will
+        blindly collapse all runs of SP/HTAB into a single SP. That could be wrong if a legitimate sequence of
+        SP/HTAB occurs in a header value.
+
+        @param name: header name
+        @type name: C{str}
+        @param value: header value
+        @type value: C{str}
+        @param dkim_tags: the extracted DKIM tags, or C{None} if no removal needed
+        @type dkim_tags: C{dict} or C{None}
+        """
+
         # Special case DKIM-Signature: remove the b= value for signature
-        if remove_b is not None and name == DKIM_SIGNATURE.lower():
-            pos = value.find(remove_b)
-            value = value[:pos] + value[pos + len(remove_b):]
-            value = " ".join(value.split())
+        name = name.lower()
+        if dkim_tags is not None and name == DKIM_SIGNATURE.lower():
+            value = DKIMUtils.canonicalizeDKIMHeaderFields(value, dkim_tags)
 
+        # Basic relaxed behavior
+        value = " ".join(value.split())
+        value = value.replace(" ,", ",")
+        value = value.replace(", ", ",")
+
         crlf = "" if name == DKIM_SIGNATURE.lower() else "\r\n"
         return "%s:%s%s" % (name, value, crlf)
 
 
     @staticmethod
+    def canonicalizeDKIMHeaderFields(value, dkim_tags):
+        """
+        DKIM-Signature b= value needs to be stripped.
+
+        @param value: header value to process
+        @type value: C{str}
+        """
+
+        pos = value.find(dkim_tags["b"])
+        value = value[:pos] + value[pos + len(dkim_tags["b"]):]
+        value = " ".join(value.split())
+        return value
+
+
+    @staticmethod
     def canonicalizeBody(data):
         if not data.endswith("\r\n"):
             data += "\r\n"
@@ -402,13 +456,11 @@
         sign_headers = []
         raw = dict([(name.lower(), values) for name, values in self.headers.getAllRawHeaders()])
         for name in self.sign_headers:
-            oversign = name[-1] == "+"
-            name = name.rstrip("+")
-            for value in reversed(raw.get(name.lower(), ())):
-                headers.append(DKIMUtils.canonicalizeHeader(name, value))
-                sign_headers.append(name)
-            if oversign:
-                sign_headers.append(name)
+            # ischedule-relaxed canonicalization requires headers with the same name concatenated
+            # with a comma in between
+            value = ",".join(raw.get(name.lower(), ()))
+            headers.append(DKIMUtils.canonicalizeHeader(name, value))
+            sign_headers.append(name)
 
         # Generate the DKIM header tags we care about
         dkim_tags = []
@@ -419,8 +471,7 @@
         dkim_tags.append(("x", self.expire,))
         dkim_tags.append(("a", self.algorithm,))
         dkim_tags.append(("q", ":".join(self.keyMethods),))
-        dkim_tags.append(("http", base64.encodestring("%s:%s" % (self.method, self.uri,)).strip()))
-        dkim_tags.append(("c", "relaxed/simple",))
+        dkim_tags.append(("c", "ischedule-relaxed/simple",))
         dkim_tags.append(("h", ":".join(sign_headers),))
         dkim_tags.append(("bh", (yield self.bodyHash()),))
         dkim_tags.append(("b", "",))
@@ -501,7 +552,7 @@
 
         # Do header verification
         try:
-            DKIMUtils.verify(headers, self.dkim_tags["b"], pubkey, self.hash_func)
+            DKIMUtils.verify(headers, self.dkim_tags["_b"], pubkey, self.hash_func)
         except ValueError:
             msg = "Could not verify signature"
             _debug_msg = """
@@ -524,7 +575,7 @@
         self.request.stream.doStartReading = None
         body = DKIMUtils.canonicalizeBody(data)
         bh = base64.b64encode(self.hash_method(body).digest())
-        if bh != self.dkim_tags["bh"]:
+        if bh != self.dkim_tags["_bh"]:
             msg = "Could not verify the DKIM body hash"
             _debug_msg = """
 DKIM-Signature:%s
@@ -566,7 +617,7 @@
         self.dkim_tags = DKIMUtils.extractTags(dkim)
 
         # Verify validity of tags
-        required_tags = ("v", "a", "b", "bh", "c", "d", "h", "s", "http",)
+        required_tags = ("v", "a", "b", "bh", "c", "d", "h", "s",)
         for tag in required_tags:
             if tag not in self.dkim_tags:
                 msg = "Missing DKIM-Signature tag: %s" % (tag,)
@@ -576,7 +627,7 @@
         check_values = {
             "v": ("1",),
             "a": (RSA1, RSA256,),
-            "c": ("relaxed", "relaxed/simple",),
+            "c": ("ischedule-relaxed", "ischedule-relaxed/simple",),
             "q": (Q_DNS, Q_HTTP, Q_PRIVATE,),
         }
         for tag, values in check_values.items():
@@ -594,35 +645,25 @@
                     log.debug("DKIM: " + msg)
                     raise DKIMVerificationError(msg)
 
+        # Check time stamp
+        if "t" in self.dkim_tags and self.time:
+            diff_time = self.time - int(self.dkim_tags["t"])
+            if diff_time < -360:
+                msg = "Signature time too far in the future: %d seconds" % (diff_time,)
+                log.debug("DKIM: " + msg)
+                raise DKIMVerificationError(msg)
+
         # Check expiration
-        if "x" in self.dkim_tags:
+        if "x" in self.dkim_tags and self.time:
             diff_time = self.time - int(self.dkim_tags["x"])
             if diff_time > 0:
                 msg = "Signature expired: %d seconds" % (diff_time,)
                 log.debug("DKIM: " + msg)
                 raise DKIMVerificationError(msg)
 
-        # Check HTTP method/request-uri
-        try:
-            http_tag = base64.decodestring(self.dkim_tags["http"])
-        except binascii.Error:
-            msg = "Tag: http is not valid base64"
-            log.debug("DKIM: " + msg)
-            raise DKIMVerificationError(msg)
-        try:
-            method, uri = http_tag.split(":", 1)
-        except ValueError:
-            msg = "Tag: base64-decoded http is not valid: %s" % (http_tag,)
-            log.debug("DKIM: " + msg)
-            raise DKIMVerificationError(msg)
-        if method != self.request.method:
-            msg = "Tag: http method does not match: %s" % (method,)
-            log.debug("DKIM: " + msg)
-            raise DKIMVerificationError(msg)
-        if uri != self.request.uri:
-            msg = "Tag: http request-URI does not match: %s" % (uri,)
-            log.debug("DKIM: " + msg)
-            raise DKIMVerificationError(msg)
+        # Base64 encoded tags might include WSP which we need to ignore
+        for tag in ("b", "bh",):
+            self.dkim_tags["_%s" % (tag,)] = "".join(self.dkim_tags[tag].split())
 
         # Some useful bits
         self.hash_method = DKIMUtils.hashlib_method(self.dkim_tags["a"])
@@ -636,26 +677,21 @@
         and return the expected signed data.
         """
 
-        # Extract all the expected signed headers taking into account the possibility of "over_counting"
-        # headers - a technique used to ensure headers cannot be added in transit
+        # Extract all the expected signed headers taking into account multiple occurrences of a header
+        # which get concatenated with a single comma in between.
         header_list = [hdr.strip() for hdr in self.dkim_tags["h"].split(":")]
-        header_counter = collections.defaultdict(int)
 
         headers = []
         for header in header_list:
             actual_headers = self.request.headers.getRawHeaders(header)
             if actual_headers:
-                try:
-                    headers.append((header, actual_headers[-1 - header_counter[header]],))
-                except IndexError:
-                    pass
-            header_counter[header] += 1
+                headers.append((header, ",".join(actual_headers),))
 
         # DKIM-Signature is always included at the end
         headers.append((DKIM_SIGNATURE, self.request.headers.getRawHeaders(DKIM_SIGNATURE)[0],))
 
         # Now canonicalize the values
-        return "".join([DKIMUtils.canonicalizeHeader(name, value, remove_b=self.dkim_tags["b"]) for name, value in headers])
+        return "".join([DKIMUtils.canonicalizeHeader(name, value, dkim_tags=self.dkim_tags) for name, value in headers])
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/localservers.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/localservers.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/localservers.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2011-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/remoteservers.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/remoteservers.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/remoteservers.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/resource.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/resource.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -30,6 +30,8 @@
 from twistedcaldav.scheduling.ischedule.scheduler import IScheduleScheduler
 from txdav.xml import element as davxml
 import twistedcaldav.scheduling.ischedule.xml  as ischedulexml
+from twistedcaldav.directory.util import transactionFromRequest
+from twistedcaldav.scheduling.ischedule.dkim import ISCHEDULE_CAPABILITIES
 
 __all__ = [
     "IScheduleInboxResource",
@@ -156,6 +158,7 @@
         result = ischedulexml.QueryResult(
 
             ischedulexml.Capabilities(
+                ischedulexml.Version.fromString(config.Scheduling.iSchedule.SerialNumber),
                 ischedulexml.Versions(
                     ischedulexml.Version.fromString("1.0"),
                 ),
@@ -194,7 +197,9 @@
                 ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")),
             ),
         )
-        return XMLResponse(responsecode.OK, result)
+        response = XMLResponse(responsecode.OK, result)
+        response.headers.addRawHeader(ISCHEDULE_CAPABILITIES, str(config.Scheduling.iSchedule.SerialNumber))
+        return response
 
 
     @inlineCallbacks
@@ -207,7 +212,7 @@
         scheduler = IScheduleScheduler(request, self)
 
         # Need a transaction to work with
-        txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
+        txn = transactionFromRequest(request, self._newStore)
         request._newStoreTransaction = txn
 
         # Do the POST processing treating this as a non-local schedule
@@ -218,7 +223,9 @@
             raise e
         else:
             yield txn.commit()
-        returnValue(result.response())
+        response = result.response()
+        response.headers.addRawHeader(ISCHEDULE_CAPABILITIES, str(config.Scheduling.iSchedule.SerialNumber))
+        returnValue(response)
 
     ##
     # ACL

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/scheduler.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/scheduler.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/scheduler.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_delivery.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_delivery.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_delivery.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_dkim.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_dkim.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_dkim.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -149,7 +149,7 @@
 recipient:mailto:user02 at example.com
 content-type:%s
 ischedule-version:1.0
-dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], str(int(time.time())), str(int(time.time() + 3600)), algorithm, bodyhash)
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http/well-known; c=ischedule-relaxed/simple; h=Originator:Recipient; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], str(int(time.time())), str(int(time.time() + 3600)), algorithm, bodyhash)
 
             result = request.generateSignature(sign_this)
 
@@ -183,7 +183,7 @@
 content-type:%s
 ischedule-version:1.0
 ischedule-message-id:%s
-dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.message_id, request.time, request.expire, algorithm, bodyhash)
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; c=ischedule-relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.message_id, request.time, request.expire, algorithm, bodyhash)
 
             self.assertEqual(result, sign_this)
 
@@ -211,14 +211,14 @@
 content-type:%s
 ischedule-version:1.0
 ischedule-message-id:%s
-dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.message_id, request.time, request.expire, algorithm, bodyhash)
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; c=ischedule-relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.message_id, request.time, request.expire, algorithm, bodyhash)
             key = RSA.importKey(open(self.private_keyfile).read())
             signature = DKIMUtils.sign(sign_this, key, DKIMUtils.hash_func(algorithm))
 
             self.assertEqual(result, signature)
 
             # Make sure header is updated in the request
-            updated_header = "v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=%s" % (request.time, request.expire, algorithm, bodyhash, signature,)
+            updated_header = "v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; c=ischedule-relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=%s" % (request.time, request.expire, algorithm, bodyhash, signature,)
             self.assertEqual(request.headers.getRawHeaders("DKIM-Signature")[0], updated_header)
 
             # Try to verify result using public key
@@ -254,24 +254,24 @@
 
             # More than one
             ((
-                ("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),
-                ("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha256; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),
+                ("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; c=ischedule-relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),
+                ("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha256; q=dns/txt:http/well-known; c=ischedule-relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),
             ), False,),
 
             # Valid
-            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), True,),
-            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha256; q=dns/txt; http=UE9TVDov; c=relaxed; h=Originator:Recipient; bh=abc; b=def"),), True,),
-            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=UE9TVDov; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() + 30),)),), True,),
+            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; c=ischedule-relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), True,),
+            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha256; q=dns/txt; c=ischedule-relaxed; h=Originator:Recipient; bh=abc; b=def"),), True,),
+            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=ischedule-relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() + 30),)),), True,),
 
             # Invalid
-            ((("DKIM-Signature", "v=2; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
-            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha512; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
-            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/relaxed; h=Originator:Recipient; bh=abc; b=def"),), False,),
-            ((("DKIM-Signature", "v=1; d=example.com; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
-            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=UE9TVDov; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
-            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
-            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=UE9TVDovaXNjaGVkdWxl; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
-            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=POST:/; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
+            ((("DKIM-Signature", "v=2; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; c=ischedule-relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
+            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha512; q=dns/txt:http/well-known; c=ischedule-relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
+            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; c=ischedule-relaxed/relaxed; h=Originator:Recipient; bh=abc; b=def"),), False,),
+            ((("DKIM-Signature", "v=1; d=example.com; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; c=ischedule-relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
+            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=ischedule-relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
+            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=ischedule-relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
+            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=ischedule-relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
+            ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=ischedule-relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
         )
 
         for headers, result in data:
@@ -291,17 +291,17 @@
         data = (
             ("Content-Type", " text/calendar  ; charset =  \"utf-8\"  ", "content-type:text/calendar ; charset = \"utf-8\"\r\n"),
             ("Originator", "  mailto:user01 at example.com  ", "originator:mailto:user01 at example.com\r\n"),
-            ("Recipient", "  mailto:user02 at example.com  ,\t mailto:user03 at example.com\t\t  ", "recipient:mailto:user02 at example.com , mailto:user03 at example.com\r\n"),
+            ("Recipient", "  mailto:user02 at example.com  ,\t mailto:user03 at example.com\t\t  ", "recipient:mailto:user02 at example.com,mailto:user03 at example.com\r\n"),
             ("iSchedule-Version", " 1.0 ", "ischedule-version:1.0\r\n"),
             (
                 "DKIM-Signature",
-                "  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def",
-                "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=",
+                "  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; c=ischedule-relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=a b  c; b=d ef",
+                "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; c=ischedule-relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=a b c; b=",
             ),
             (
                 "DKIM-Signature",
-                "  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; b= def ; http=\tUE9TVDov   ; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc",
-                "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; b= ; http= UE9TVDov ; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc",
+                "  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; b= def ; c=ischedule-relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=a\t bc",
+                "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; b= ; c=ischedule-relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=a bc",
             ),
         )
 
@@ -310,7 +310,7 @@
             verifier = DKIMVerifier(request)
             if name == "DKIM-Signature":
                 verifier.processDKIMHeader()
-            canonicalized = DKIMUtils.canonicalizeHeader(name, value, remove_b=verifier.dkim_tags["b"] if name == "DKIM-Signature" else None)
+            canonicalized = DKIMUtils.canonicalizeHeader(name, value, verifier.dkim_tags if name == "DKIM-Signature" else None)
             self.assertEqual(canonicalized, result)
 
 
@@ -320,21 +320,21 @@
         """
 
         data = (
-            # Over count on Recipient
+            # Count on Recipient
             ("""Host:example.com
 Content-Type: text/calendar  ; charset =  "utf-8"
 Originator:  mailto:user01 at example.com
 Recipient:  mailto:user02 at example.com  ,\t mailto:user03 at example.com\t\t
 iSchedule-Version: 1.0
-DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
 Cache-Control:no-cache
 Connection:close
 """,
             """content-type:text/calendar ; charset = "utf-8"
 originator:mailto:user01 at example.com
-recipient:mailto:user02 at example.com , mailto:user03 at example.com
+recipient:mailto:user02 at example.com,mailto:user03 at example.com
 ischedule-version:1.0
-dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
             ),
             # Exact count on Recipient
             ("""Host:example.com
@@ -343,51 +343,31 @@
 Recipient:  mailto:user02 at example.com  ,\t mailto:user03 at example.com\t\t
 Recipient:\t\t  mailto:user04 at example.com
 iSchedule-Version: 1.0
-DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
 Cache-Control:no-cache
 Connection:close
 """,
             """content-type:text/calendar ; charset = "utf-8"
 originator:mailto:user01 at example.com
-recipient:mailto:user04 at example.com
-recipient:mailto:user02 at example.com , mailto:user03 at example.com
+recipient:mailto:user02 at example.com,mailto:user03 at example.com,mailto:user04 at example.com
 ischedule-version:1.0
-dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
             ),
-            # Under count on Recipient
-            ("""Host:example.com
-Content-Type: text/calendar  ; charset =  "utf-8"
-iSchedule-Version: 1.0
-Originator:  mailto:user01 at example.com
-Recipient:  mailto:user02 at example.com  ,\t mailto:user03 at example.com\t\t
-Recipient:\t\t  mailto:user04 at example.com
-Recipient:\t\t  mailto:user05 at example.com
-DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
-Cache-Control:no-cache
-Connection:close
-""",
-            """content-type:text/calendar ; charset = "utf-8"
-originator:mailto:user01 at example.com
-recipient:mailto:user05 at example.com
-recipient:mailto:user04 at example.com
-ischedule-version:1.0
-dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
-            ),
             # Re-ordered Content-Type
             ("""Host:example.com
 iSchedule-Version: 1.0
 Originator:  mailto:user01 at example.com
 Recipient:  mailto:user02 at example.com  ,\t mailto:user03 at example.com\t\t
-DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
 Content-Type: text/calendar  ; charset =  "utf-8"
 Cache-Control:no-cache
 Connection:close
 """,
             """content-type:text/calendar ; charset = "utf-8"
 originator:mailto:user01 at example.com
-recipient:mailto:user02 at example.com , mailto:user03 at example.com
+recipient:mailto:user02 at example.com,mailto:user03 at example.com
 ischedule-version:1.0
-dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
             ),
         )
 
@@ -411,7 +391,7 @@
 Content-Type: text/calendar  ; charset =  "utf-8"
 Originator:  mailto:user01 at example.com
 Recipient:  mailto:user02 at example.com  ,\t mailto:user03 at example.com\t\t
-DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
 Cache-Control:no-cache
 Connection:close
 """,
@@ -423,7 +403,7 @@
 Content-Type: text/calendar  ; charset =  "utf-8"
 Originator:  mailto:user01 at example.com
 Recipient:  mailto:user02 at example.com  ,\t mailto:user03 at example.com\t\t
-DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt\t\t; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
 Cache-Control:no-cache
 Connection:close
 """,
@@ -435,7 +415,7 @@
 Content-Type: text/calendar  ; charset =  "utf-8"
 Originator:  mailto:user01 at example.com
 Recipient:  mailto:user02 at example.com  ,\t mailto:user03 at example.com\t\t
-DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+DKIM-Signature:  v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
 Cache-Control:no-cache
 Connection:close
 """,
@@ -574,7 +554,7 @@
             manipulate_request=lambda request: setattr(request, "stream", MemoryStream("BEGIN:DATA\n")),
         )
 
-        # Valid - extra header no over sign
+        # Invalid - extra header
         yield _verify(
             """Host:example.com
 Content-Type: text/calendar  ; charset =  "utf-8"
@@ -587,11 +567,11 @@
 END:DATA
 """,
             [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            True,
+            False,
             manipulate_request=lambda request: request.headers.getRawHeaders("Recipient").insert(0, "mailto:user04 at example.com"),
         )
 
-        # Valid - over sign header
+        # Valid - header
         yield _verify(
             """Host:example.com
 Content-Type: text/calendar  ; charset =  "utf-8"
@@ -604,8 +584,8 @@
 END:DATA
 """,
             [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            False,
-            sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
+            True,
+            sign_headers=("Originator", "Recipient", "Content-Type",),
         )
 
         # Invalid - over sign header extra header
@@ -622,7 +602,7 @@
 """,
             [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
             False,
-            sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
+            sign_headers=("Originator", "Recipient", "Content-Type",),
             manipulate_request=lambda request: request.headers.addRawHeader("Recipient", ("mailto:user04 at example.com",))
         )
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_localservers.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_localservers.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_localservers.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_resource.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_resource.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -28,7 +28,9 @@
         super(iSchedulePOST, self).setUp()
         self.createStockDirectoryService()
         self.setupCalendars()
-        self.site.resource.putChild("ischedule", IScheduleInboxResource(self.site.resource, self._newStore))
+        self.site.resource.putChild(
+            "ischedule", IScheduleInboxResource(self.site.resource,
+                                                self.createDataStore()))
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_utils.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_utils.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_utils.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/utils.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/utils.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/utils.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -58,7 +58,7 @@
     lookup = "%s._tcp.%s" % (service, domain,)
     log.debug("DNS SRV: lookup: %s" % (lookup,))
     try:
-        answers = (yield DebugResolver.lookupService(lookup))
+        answers = (yield DebugResolver.lookupService(lookup))[0]
     except (DomainError, AuthoritativeDomainError), e:
         log.debug("DNS SRV: lookup failed: %s" % (e,))
         returnValue(None)
@@ -117,7 +117,7 @@
     lookup = "%s.%s" % (prefix, domain,) if prefix else domain
     log.debug("DNS TXT: lookup: %s" % (lookup,))
     try:
-        answers = (yield DebugResolver.lookupText(lookup))
+        answers = (yield DebugResolver.lookupText(lookup))[0]
     except (DomainError, AuthoritativeDomainError), e:
         log.debug("DNS TXT: lookup failed: %s" % (e,))
         answers = ()
@@ -142,7 +142,7 @@
         log.debug("DNS FakeBindAuthority: lookup: %s %s %s" % (name, cls, type,))
         result = yield BindAuthority._lookup(self, name, cls, type, timeout)
         log.debug("DNS FakeBindAuthority: lookup results: %s %s %s\n%s" % (name, cls, type, result[0]))
-        returnValue(result[0])
+        returnValue(result)
 
 
     def stripComments(self, lines):

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/xml.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/xml.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/xml.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2011-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -48,6 +48,7 @@
     name = "capabilities"
 
     allowed_children = {
+        (ischedule_namespace, "serial-number"): (1, 1),
         (ischedule_namespace, "versions"): (1, 1),
         (ischedule_namespace, "scheduling-messages"): (1, 1),
         (ischedule_namespace, "calendar-data-types"): (1, 1),
@@ -64,6 +65,13 @@
 
 
 @registerElement
+class SerialNumber (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "serial-number"
+
+
+
+ at registerElement
 class Versions (WebDAVElement):
     namespace = ischedule_namespace
     name = "versions"

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/itip.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/itip.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -119,6 +119,11 @@
             organizer_schedule_status = organizer.parameterValue("SCHEDULE-STATUS", None) if organizer else None
             attendee = current_master.getAttendeeProperty((recipient,))
             attendee_dtstamp = attendee.parameterValue("X-CALENDARSERVER-DTSTAMP") if attendee else None
+            other_props = {}
+            for pname in config.Scheduling.CalDAV.PerAttendeeProperties:
+                props = tuple(current_master.properties(pname))
+                if props:
+                    other_props[pname] = props
         else:
             master_valarms = ()
             private_comments = ()
@@ -126,6 +131,7 @@
             completeds = ()
             organizer_schedule_status = None
             attendee_dtstamp = None
+            other_props = {}
 
         if itip_message.masterComponent() is not None:
 
@@ -150,11 +156,13 @@
                 attendee = master_component.getAttendeeProperty((recipient,))
                 if attendee:
                     attendee.setParameter("X-CALENDARSERVER-DTSTAMP", attendee_dtstamp)
+            for props in other_props.values():
+                [master_component.replaceProperty(prop) for prop in props]
 
             # Now try to match recurrences in the new calendar
             for component in tuple(new_calendar.subcomponents()):
                 if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
-                    iTipProcessing.transferItems(calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, attendee_dtstamp, component, recipient)
+                    iTipProcessing.transferItems(calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, attendee_dtstamp, other_props, component, recipient)
 
             # Now try to match recurrences from the old calendar
             for component in calendar.subcomponents():
@@ -165,7 +173,7 @@
                         new_component = new_calendar.deriveInstance(rid, allowCancelled=allowCancelled)
                         if new_component:
                             new_calendar.addComponent(new_component)
-                            iTipProcessing.transferItems(calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, attendee_dtstamp, new_component, recipient)
+                            iTipProcessing.transferItems(calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, attendee_dtstamp, other_props, new_component, recipient)
 
             # Replace the entire object
             return new_calendar, rids
@@ -182,7 +190,7 @@
                         calendar.addComponent(component)
                 else:
                     component = component.duplicate()
-                    missingDeclined = iTipProcessing.transferItems(calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, attendee_dtstamp, component, recipient, remove_matched=True)
+                    missingDeclined = iTipProcessing.transferItems(calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, attendee_dtstamp, other_props, component, recipient, remove_matched=True)
                     if not missingDeclined:
                         calendar.addComponent(component)
                         if recipient:
@@ -493,7 +501,7 @@
 
 
     @staticmethod
-    def transferItems(from_calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, attendee_dtstamp, to_component, recipient, remove_matched=False):
+    def transferItems(from_calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, attendee_dtstamp, other_props, to_component, recipient, remove_matched=False):
         """
         Transfer properties from a calendar to a component by first trying to match the component in the original calendar and
         use the properties from that, or use the values provided as arguments (which have been derived from the original calendar's
@@ -533,6 +541,9 @@
 
             if attendee and attendee_dtstamp:
                 attendee.setParameter("X-CALENDARSERVER-DTSTAMP", attendee_dtstamp)
+
+            for pname in config.Scheduling.CalDAV.PerAttendeeProperties:
+                [to_component.replaceProperty(prop) for prop in matched.properties(pname)]
         else:
             # Check for incoming DECLINED
             attendee = to_component.getAttendeeProperty((recipient,))
@@ -545,6 +556,7 @@
             [to_component.addProperty(comment) for comment in private_comments]
             [to_component.replaceProperty(transp) for transp in transps]
             [to_component.replaceProperty(completed) for completed in completeds]
+
             if organizer_schedule_status:
                 organizer = to_component.getProperty("ORGANIZER")
                 if organizer:
@@ -554,6 +566,9 @@
                 if attendee:
                     attendee.setParameter("X-CALENDARSERVER-DTSTAMP", attendee_dtstamp)
 
+            for props in other_props.values():
+                [to_component.replaceProperty(prop) for prop in props]
+
         return False
 
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/processing.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/processing.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -14,30 +14,34 @@
 # limitations under the License.
 ##
 
-from twisted.python.log import err as log_traceback
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.duration import PyCalendarDuration
+from pycalendar.timezone import PyCalendarTimezone
+
 from twext.python.log import Logger
+from twext.web2.dav.method.report import NumberOfMatchesWithinLimits
+from twext.web2.dav.util import joinURL
+from twext.web2.http import HTTPError
 
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
-from twext.web2.dav.method.report import NumberOfMatchesWithinLimits
-from twext.web2.dav.util import joinURL
-from twext.web2.http import HTTPError
+from twisted.python.log import err as log_traceback
+
 from twistedcaldav import customxml, caldavxml
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.ical import Property
 from twistedcaldav.instance import InvalidOverriddenInstanceError
+from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
+from twistedcaldav.memcacher import Memcacher
 from twistedcaldav.method import report_common
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
 from twistedcaldav.scheduling.itip import iTipProcessing, iTIPRequestStatus
 from twistedcaldav.scheduling.utils import getCalendarObjectForPrincipals
-from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from twistedcaldav.memcacher import Memcacher
-from pycalendar.duration import PyCalendarDuration
-from pycalendar.datetime import PyCalendarDateTime
-from pycalendar.timezone import PyCalendarTimezone
+
+import collections
+import hashlib
 import uuid
-import hashlib
 
 """
 CalDAV implicit processing.
@@ -197,6 +201,8 @@
             log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REPLY, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
             self.organizer_calendar_resource = (yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar))
 
+            organizer = self.recipient_calendar.getOrganizer()
+
             # Build the schedule-changes XML element
             attendeeReplying, rids = processed
             partstatChanged = False
@@ -228,7 +234,7 @@
             # and only if the request does not indicate we should skip attendee refresh
             # (e.g. inbox item processing during migration from non-implicit server)
             if partstatChanged and not getattr(self.request, "noAttendeeRefresh", False):
-                yield self.queueAttendeeUpdate((attendeeReplying,))
+                yield self.queueAttendeeUpdate((attendeeReplying, organizer,))
 
             result = (True, False, True, changes,)
 
@@ -278,27 +284,26 @@
             try:
                 # Get all attendees to refresh
                 allAttendees = sorted(list(self.recipient_calendar.getAllUniqueAttendees()))
+                allAttendees = filter(lambda x: x not in exclude_attendees, allAttendees)
 
-                # Always need to refresh every attendee
-                exclude_attendees = ()
+                if allAttendees:
+                    # See if there is already a pending refresh and merge current attendees into that list,
+                    # otherwise just mark all attendees as pending
+                    cache = Memcacher("BatchRefreshAttendees", pickle=True)
+                    pendingAttendees = yield cache.get(self.uid)
+                    firstTime = False
+                    if pendingAttendees:
+                        for attendee in allAttendees:
+                            if attendee not in pendingAttendees:
+                                pendingAttendees.append(attendee)
+                    else:
+                        firstTime = True
+                        pendingAttendees = allAttendees
+                    yield cache.set(self.uid, pendingAttendees)
 
-                # See if there is already a pending refresh and merge current attendees into that list,
-                # otherwise just mark all attendees as pending
-                cache = Memcacher("BatchRefreshAttendees", pickle=True)
-                pendingAttendees = yield cache.get(self.uid)
-                firstTime = False
-                if pendingAttendees:
-                    for attendee in allAttendees:
-                        if attendee not in pendingAttendees:
-                            pendingAttendees.append(attendee)
-                else:
-                    firstTime = True
-                    pendingAttendees = allAttendees
-                yield cache.set(self.uid, pendingAttendees)
-
-                # Now start the first batch off
-                if firstTime:
-                    reactor.callLater(config.Scheduling.Options.AttendeeRefreshBatchDelaySeconds, self._doBatchRefresh)
+                    # Now start the first batch off
+                    if firstTime:
+                        self._enqueueBatchRefresh()
             finally:
                 yield lock.clean()
 
@@ -379,6 +384,13 @@
             yield uidlock.clean()
 
 
+    def _enqueueBatchRefresh(self):
+        """
+        Mostly here to help unit test by being able to stub this out.
+        """
+        reactor.callLater(config.Scheduling.Options.AttendeeRefreshBatchDelaySeconds, self._doBatchRefresh)
+
+
     @inlineCallbacks
     def _doBatchRefresh(self):
         """
@@ -422,7 +434,7 @@
 
                 # Queue the next refresh if needed
                 if pendingAttendees:
-                    reactor.callLater(config.Scheduling.Options.AttendeeRefreshBatchIntervalSeconds, self._doBatchRefresh)
+                    self._enqueueBatchRefresh()
             else:
                 yield cache.delete(self.uid)
                 yield lock.release()
@@ -504,8 +516,7 @@
             if self.recipient.principal.canAutoSchedule(organizer=organizer):
                 # auto schedule mode can depend on who the organizer is
                 mode = self.recipient.principal.getAutoScheduleMode(organizer=organizer)
-                send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar,
-                    mode))
+                send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar, mode))
 
                 # Only store inbox item when reply is not sent or always for users
                 store_inbox = store_inbox or self.recipient.principal.getCUType() == "INDIVIDUAL"
@@ -541,8 +552,7 @@
                 if self.recipient.principal.canAutoSchedule(organizer=organizer):
                     # auto schedule mode can depend on who the organizer is
                     mode = self.recipient.principal.getAutoScheduleMode(organizer=organizer)
-                    send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar,
-                        mode))
+                    send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar, mode))
 
                     # Only store inbox item when reply is not sent or always for users
                     store_inbox = store_inbox or self.recipient.principal.getCUType() == "INDIVIDUAL"
@@ -724,14 +734,13 @@
     @inlineCallbacks
     def checkAttendeeAutoReply(self, calendar, automode):
         """
-        Check whether a reply to the given iTIP message is needed. We will not process a reply
-        A reply will either be positive (accepted invitation) or negative (denied invitation).
-        In addition we will modify calendar to reflect
-        any new state (e.g. set PARTSTAT to ACCEPTED or DECLINED).
+        Check whether a reply to the given iTIP message is needed and if so make the
+        appropriate changes to the calendar data. Changes are only made for the case
+        where the PARTSTAT of the attendee is NEEDS-ACTION - i.e., any existing state
+        is left unchanged. This allows, e.g., proxies to decline events that would
+        otherwise have been auto-accepted and those stay declined as non-schedule-change
+        updates are received.
 
-        BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
-        At the moment we will treat a failure on one instance as a DECLINE of the entire set.
-
         @param calendar: the iTIP message to process
         @type calendar: L{Component}
         @param automode: the auto-schedule mode for the recipient
@@ -749,102 +758,106 @@
 
         log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - checking for auto-reply with mode: %s" % (self.recipient.cuaddr, self.uid, automode,))
 
-        # The accept-always and decline-always modes do not need any freebusy checks
-        if automode in ("accept-always", "decline-always",):
-            all_accepted = automode == "accept-always"
-            all_declined = automode == "decline-always"
+        cuas = self.recipient.principal.calendarUserAddresses()
 
-        # Other modes need freebusy check
-        else:
-            # First expand current one to get instances (only go 1 year into the future)
-            default_future_expansion_duration = PyCalendarDuration(days=356 * 1)
-            expand_max = PyCalendarDateTime.getToday() + default_future_expansion_duration
-            instances = calendar.expandTimeRanges(expand_max, ignoreInvalidInstances=True)
-            instance_states = dict([(instance, True) for instance in instances.instances.itervalues()])
+        # First expand current one to get instances (only go 1 year into the future)
+        default_future_expansion_duration = PyCalendarDuration(days=365)
+        expand_max = PyCalendarDateTime.getToday() + default_future_expansion_duration
+        instances = calendar.expandTimeRanges(expand_max, ignoreInvalidInstances=True)
 
-            # Extract UID from primary component as we want to ignore this one if we match it
-            # in any calendars.
-            comp = calendar.mainComponent(allow_multiple=True)
-            uid = comp.propertyValue("UID")
+        # Cache the current attendee partstat on the instance object for later use, and
+        # also mark whether the instance time slot would be free
+        for instance in instances.instances.itervalues():
+            attendee = instance.component.getAttendeeProperty(cuas)
+            instance.partstat = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") if attendee else None
+            instance.free = True
 
-            # Now compare each instance time-range with the index and see if there is an overlap
-            calendars = (yield self._getCalendarsToMatch())
+        # Extract UID from primary component as we want to ignore this one if we match it
+        # in any calendars.
+        comp = calendar.mainComponent(allow_multiple=True)
+        uid = comp.propertyValue("UID")
 
-            for calURL in calendars:
-                testcal = (yield self.request.locateResource(calURL))
+        # Now compare each instance time-range with the index and see if there is an overlap
+        calendars = (yield self._getCalendarsToMatch())
 
-                # Get the timezone property from the collection, and store in the query filter
-                # for use during the query itself.
-                has_prop = (yield testcal.hasProperty((caldav_namespace, "calendar-timezone"), self.request))
-                if has_prop:
-                    tz = (yield testcal.readProperty((caldav_namespace, "calendar-timezone"), self.request))
-                    tzinfo = tz.calendar().gettimezone()
-                else:
-                    tzinfo = PyCalendarTimezone(utc=True)
+        for calURL in calendars:
+            testcal = (yield self.request.locateResource(calURL))
 
-                # Now do search for overlapping time-range
-                for instance in instances.instances.itervalues():
-                    if instance_states[instance]:
-                        try:
-                            # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-                            fbinfo = ([], [], [])
+            # Get the timezone property from the collection, and store in the query filter
+            # for use during the query itself.
+            has_prop = (yield testcal.hasProperty((caldav_namespace, "calendar-timezone"), self.request))
+            if has_prop:
+                tz = (yield testcal.readProperty((caldav_namespace, "calendar-timezone"), self.request))
+                tzinfo = tz.calendar().gettimezone()
+            else:
+                tzinfo = PyCalendarTimezone(utc=True)
 
-                            def makeTimedUTC(dt):
-                                dt = dt.duplicate()
-                                if dt.isDateOnly():
-                                    dt.setDateOnly(False)
-                                    dt.setHHMMSS(0, 0, 0)
-                                if dt.floating():
-                                    dt.setTimezone(tzinfo)
-                                    dt.adjustToUTC()
-                                return dt
+            # Now do search for overlapping time-range and set instance.free based
+            # on whether there is an overlap or not
+            for instance in instances.instances.itervalues():
+                if instance.partstat == "NEEDS-ACTION" and instance.free:
+                    try:
+                        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+                        fbinfo = ([], [], [])
 
-                            tr = caldavxml.TimeRange(
-                                start=str(makeTimedUTC(instance.start)),
-                                end=str(makeTimedUTC(instance.end)),
-                            )
+                        def makeTimedUTC(dt):
+                            dt = dt.duplicate()
+                            if dt.isDateOnly():
+                                dt.setDateOnly(False)
+                                dt.setHHMMSS(0, 0, 0)
+                            if dt.floating():
+                                dt.setTimezone(tzinfo)
+                                dt.adjustToUTC()
+                            return dt
 
-                            yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid, servertoserver=True)
+                        tr = caldavxml.TimeRange(
+                            start=str(makeTimedUTC(instance.start)),
+                            end=str(makeTimedUTC(instance.end)),
+                        )
 
-                            # If any fbinfo entries exist we have an overlap
-                            if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
-                                instance_states[instance] = False
-                        except NumberOfMatchesWithinLimits:
-                            instance_states[instance] = False
-                            log.info("Exceeded number of matches whilst trying to find free-time.")
+                        yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid, servertoserver=True)
 
-                # If everything is declined we can exit now
-                if not any(instance_states.itervalues()):
-                    break
+                        # If any fbinfo entries exist we have an overlap
+                        if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
+                            instance.free = False
+                    except NumberOfMatchesWithinLimits:
+                        instance.free[instance] = False
+                        log.info("Exceeded number of matches whilst trying to find free-time.")
 
-            # TODO: here we should do per-instance ACCEPT/DECLINE behavior
-            # For now we will assume overall ACCEPT/DECLINE
+            # If everything is declined we can exit now
+            if not any([instance.free for instance in instances.instances.itervalues()]):
+                break
 
-            # Collect all the accepted and declined states
-            all_accepted = all(instance_states.itervalues())
-            all_declined = not any(instance_states.itervalues())
+        # Now adjust the instance.partstat currently set to "NEEDS-ACTION" to the
+        # value determined by auto-accept logic based on instance.free state
+        partstat_counts = collections.defaultdict(int)
+        for instance in instances.instances.itervalues():
+            if instance.partstat == "NEEDS-ACTION":
+                if automode == "accept-always":
+                    freePartstat = busyPartstat = "ACCEPTED"
+                elif automode == "decline-always":
+                    freePartstat = busyPartstat = "DECLINED"
+                else:
+                    freePartstat = "ACCEPTED" if automode in ("accept-if-free", "automatic",) else "NEEDS-ACTION"
+                    busyPartstat = "DECLINED" if automode in ("decline-if-busy", "automatic",) else "NEEDS-ACTION"
+                instance.partstat = freePartstat if instance.free else busyPartstat
+            partstat_counts[instance.partstat] += 1
 
-        # Do the simple case of all accepted or decline separately
-        cuas = self.recipient.principal.calendarUserAddresses()
-        if all_accepted or all_declined:
+        if len(partstat_counts) == 0:
+            # Nothing to do
+            returnValue((False, True, "",))
+
+        elif len(partstat_counts) == 1:
+            # Do the simple case of all PARTSTATs the same separately
             # Extract the ATTENDEE property matching current recipient from the calendar data
             attendeeProps = calendar.getAttendeeProperties(cuas)
             if not attendeeProps:
                 returnValue((False, True, "",))
 
-            if automode == "accept-always":
-                freePartstat = busyPartstat = "ACCEPTED"
-            elif automode == "decline-always":
-                freePartstat = busyPartstat = "DECLINED"
-            else:
-                freePartstat = "ACCEPTED" if automode in ("accept-if-free", "automatic",) else "NEEDS-ACTION"
-                busyPartstat = "DECLINED" if automode in ("decline-if-busy", "automatic",) else "NEEDS-ACTION"
-            freeStateOpaque = freePartstat == "ACCEPTED"
-
-            partstat = freePartstat if all_accepted else busyPartstat
-            calendar.replacePropertyInAllComponents(Property("TRANSP", "OPAQUE" if all_accepted and freeStateOpaque else "TRANSPARENT"))
-
-            made_changes = self.changeAttendeePartstat(attendeeProps, partstat)
+            made_changes = False
+            partstat = partstat_counts.keys()[0]
+            for component in calendar.subcomponents():
+                made_changes |= self.resetAttendeePartstat(component, cuas, partstat)
             store_inbox = partstat == "NEEDS-ACTION"
 
         else:
@@ -857,12 +870,8 @@
             store_inbox = False
             partstat = "MIXED RESPONSE"
 
-            freePartstat = "ACCEPTED" if automode in ("accept-if-free", "automatic",) else "NEEDS-ACTION"
-            busyPartstat = "DECLINED" if automode in ("decline-if-busy", "automatic",) else "NEEDS-ACTION"
-            freeStateOpaque = freePartstat == "ACCEPTED"
-
             # Default state is whichever of free or busy has most instances
-            defaultStateFree = len(filter(lambda x: x, instance_states.values())) >= len(instance_states.keys()) / 2
+            defaultPartStat = max(partstat_counts.items(), key=lambda x: x[1])[0]
 
             # See if there is a master component first
             hadMasterRsvp = False
@@ -871,29 +880,23 @@
                 attendee = master.getAttendeeProperty(cuas)
                 if attendee:
                     hadMasterRsvp = attendee.parameterValue("RSVP", "FALSE") == "TRUE"
-                    new_partstat = freePartstat if defaultStateFree else busyPartstat
-                    if new_partstat == "NEEDS-ACTION":
+                    if defaultPartStat == "NEEDS-ACTION":
                         store_inbox = True
-                    made_changes |= self.changeAttendeePartstat(attendee, new_partstat)
-                    master.replaceProperty(Property("TRANSP", "OPAQUE" if defaultStateFree and freeStateOpaque else "TRANSPARENT"))
+                    made_changes |= self.resetAttendeePartstat(master, cuas, defaultPartStat)
 
             # Look at expanded instances and change partstat accordingly
-            for instance, free in sorted(instance_states.iteritems(), key=lambda x: x[0].rid):
+            for instance in sorted(instances.instances.values(), key=lambda x: x.rid):
 
                 overridden = calendar.overriddenComponent(instance.rid)
-                if not overridden and free == defaultStateFree:
+                if not overridden and instance.partstat == defaultPartStat:
                     # Nothing to do as state matches the master
                     continue
 
                 if overridden:
                     # Change ATTENDEE property to match new state
-                    attendee = overridden.getAttendeeProperty(cuas)
-                    if attendee:
-                        new_partstat = freePartstat if free else busyPartstat
-                        if new_partstat == "NEEDS-ACTION":
-                            store_inbox = True
-                        made_changes |= self.changeAttendeePartstat(attendee, new_partstat)
-                        overridden.replaceProperty(Property("TRANSP", "OPAQUE" if free and freeStateOpaque else "TRANSPARENT"))
+                    if instance.partstat == "NEEDS-ACTION":
+                        store_inbox = True
+                    made_changes |= self.resetAttendeePartstat(overridden, cuas, instance.partstat)
                 else:
                     # Derive a new overridden component and change partstat. We also need to make sure we restore any RSVP
                     # value that may have been overwritten by any change to the master itself.
@@ -901,13 +904,11 @@
                     if derived:
                         attendee = derived.getAttendeeProperty(cuas)
                         if attendee:
-                            new_partstat = freePartstat if free else busyPartstat
-                            if new_partstat == "NEEDS-ACTION":
+                            if instance.partstat == "NEEDS-ACTION":
                                 store_inbox = True
-                            self.changeAttendeePartstat(attendee, new_partstat, hadMasterRsvp)
-                            derived.replaceProperty(Property("TRANSP", "OPAQUE" if free and freeStateOpaque else "TRANSPARENT"))
+                            self.resetAttendeePartstat(derived, cuas, instance.partstat, hadMasterRsvp)
+                            made_changes = True
                             calendar.addComponent(derived)
-                            made_changes = True
 
         # Fake a SCHEDULE-STATUS on the ORGANIZER property
         if made_changes:
@@ -981,12 +982,16 @@
         yield delchild.storeRemove(self.request, False, childURL)
 
 
-    def changeAttendeePartstat(self, attendees, partstat, hadRSVP=False):
+    def resetAttendeePartstat(self, component, cuas, partstat, hadRSVP=False):
         """
-        Change the PARTSTAT on any ATTENDEE properties passed in.
+        Change the PARTSTAT on any ATTENDEE properties that match the list of calendar user
+        addresses on the component passed in. Also adjust the TRANSP property to match the
+        new PARTSTAT value.
 
-        @param attendees: a single ATTENDEE property or a list of them
-        @type attendees: L{Property}, C{list} or C{tuple}
+        @param component: an iCalendar component to modify
+        @type attendees: L{Component}
+        @param cuas: a list of calendar user addresses to match
+        @type attendees: C{list} or C{tuple}
         @param partstat: new PARTSTAT to set
         @type partstat: C{str}
         @param hadRSVP: indicates whether RSVP should be added when changing to NEEDS-ACTION
@@ -995,11 +1000,9 @@
         @return: C{True} if any change was made, C{False} otherwise
         """
 
-        if isinstance(attendees, Property):
-            attendees = (attendees,)
-
         madeChanges = False
-        for attendee in attendees:
+        attendee = component.getAttendeeProperty(cuas)
+        if attendee:
             if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") != partstat:
                 attendee.setParameter("PARTSTAT", partstat)
                 madeChanges = True
@@ -1014,6 +1017,9 @@
             except KeyError:
                 pass
 
+            # Adjust TRANSP to OPAQUE if PARTSTAT is ACCEPTED, otherwise TRANSPARENT
+            component.replaceProperty(Property("TRANSP", "OPAQUE" if partstat == "ACCEPTED" else "TRANSPARENT"))
+
         return madeChanges
 
 

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/accounts.xml (from rev 10753, CalendarServer/trunk/twistedcaldav/scheduling/test/accounts.xml)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/accounts.xml	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/accounts.xml	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2013 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE accounts SYSTEM "../../../conf/auth/accounts.dtd">
+
+<accounts realm="Test">
+  <user repeat="99">
+    <uid>user%02d</uid>
+    <uid>User %02d</uid>
+    <guid>user%02d</guid>
+    <password>user%02d</password>
+    <name>User %02d</name>
+    <first-name>User</first-name>
+    <last-name>%02d</last-name>
+    <email-address>user%02d at example.com</email-address>
+  </user>
+</accounts>

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/resources.xml (from rev 10753, CalendarServer/trunk/twistedcaldav/scheduling/test/resources.xml)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/resources.xml	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/resources.xml	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<accounts realm="/Search">
+</accounts>

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_icaldiff.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_icaldiff.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -21,6 +21,7 @@
 
 import itertools
 import re
+from twistedcaldav.config import config
 
 class ICalDiff (twistedcaldav.test.util.TestCase):
     """
@@ -2670,6 +2671,7 @@
             ),
         )
 
+        self.patch(config, "EnableDropBox", True)
         for description, calendar1, calendar2, attendee, result in data:
             differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
             diffResult = differ.attendeeMerge(attendee)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_pocessing.py (from rev 10753, CalendarServer/trunk/twistedcaldav/scheduling/test/test_pocessing.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_pocessing.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_pocessing.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,106 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.web2.test.test_server import SimpleRequest
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.ical import Component
+from twistedcaldav.scheduling.processing import ImplicitProcessor
+import twistedcaldav.test.util
+from twistedcaldav.config import config
+
+class FakeImplicitProcessor(ImplicitProcessor):
+    """
+    A fake ImplicitProcessor that tracks batch refreshes.
+    """
+
+    def __init__(self):
+        self.batches = 0
+
+
+    def _enqueueBatchRefresh(self):
+        self.batches += 1
+
+
+
+class FakePrincipal(object):
+
+    def __init__(self, cuaddr):
+        self.cuaddr = cuaddr
+
+
+    def calendarUserAddresses(self):
+        return (self.cuaddr,)
+
+
+
+class BatchRefresh (twistedcaldav.test.util.TestCase):
+    """
+    iCalendar support tests
+    """
+
+    @inlineCallbacks
+    def test_queueAttendeeUpdate_no_refresh(self):
+
+        self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
+
+        calendar = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+        request = SimpleRequest(self.site, "PUT", "/calendar/1.ics")
+        processor = FakeImplicitProcessor()
+        processor.request = request
+        processor.uid = "12345-67890"
+        processor.recipient_calendar = calendar
+        yield processor.queueAttendeeUpdate(("urn:uuid:user02", "urn:uuid:user01",))
+        self.assertEqual(processor.batches, 0)
+
+
+    @inlineCallbacks
+    def test_queueAttendeeUpdate_with_refresh(self):
+
+        self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
+
+        calendar = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+END:VEVENT
+END:VCALENDAR
+""")
+        request = SimpleRequest(self.site, "PUT", "/calendar/1.ics")
+        processor = FakeImplicitProcessor()
+        processor.request = request
+        processor.uid = "12345-67890"
+        processor.recipient_calendar = calendar
+        yield processor.queueAttendeeUpdate(("urn:uuid:user02", "urn:uuid:user01",))
+        self.assertEqual(processor.batches, 1)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_utils.py (from rev 10753, CalendarServer/trunk/twistedcaldav/scheduling/test/test_utils.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_utils.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_utils.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,200 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from twistedcaldav.scheduling.utils import getCalendarObjectForPrincipals
+
+"""
+Tests for calendarserver.tools.purge
+"""
+
+from calendarserver.tap.util import getRootResource, FakeRequest
+
+from pycalendar.datetime import PyCalendarDateTime
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.trial import unittest
+
+from twistedcaldav.config import config
+
+from txdav.common.datastore.test.util import buildStore, populateCalendarsFrom, CommonCommonTests
+
+import os
+
+
+now = PyCalendarDateTime.getToday().getYear()
+
+ORGANIZER_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:685BC3A1-195A-49B3-926D-388DDACA78A6
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:%(year)s0307T111500Z
+DURATION:PT1H
+DTSTAMP:20100303T181220Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:user02
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % {"year": now + 1}
+
+ATTENDEE_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:685BC3A1-195A-49B3-926D-388DDACA78A6
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:%(year)s0307T111500Z
+DURATION:PT1H
+DTSTAMP:20100303T181220Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:user02
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % {"year": now + 1}
+
+
+
+class RecipientCopy(CommonCommonTests, unittest.TestCase):
+    """
+    Tests for deleting events older than a given date
+    """
+
+    metadata = {
+        "accessMode": "PUBLIC",
+        "isScheduleObject": True,
+        "scheduleTag": "abc",
+        "scheduleEtags": (),
+        "hasPrivateComment": False,
+    }
+
+    requirements = {
+        "user01" : {
+            "calendar1" : {
+                "1.ics" : (ORGANIZER_ICS, metadata,),
+            }
+        },
+        "user02" : {
+            "calendar1" : {
+                "2.ics" : (ATTENDEE_ICS, metadata,),
+            },
+            "calendar3" : {
+                "3.ics" : (ATTENDEE_ICS, metadata,),
+            }
+        }
+    }
+
+    @inlineCallbacks
+    def setUp(self):
+
+        yield super(RecipientCopy, self).setUp()
+        self._sqlCalendarStore = yield buildStore(self, self.notifierFactory)
+        yield self.populate()
+
+        self.patch(config.DirectoryService.params, "xmlFile",
+            os.path.join(
+                os.path.dirname(__file__), "accounts.xml"
+            )
+        )
+        self.patch(config.ResourceService.params, "xmlFile",
+            os.path.join(
+                os.path.dirname(__file__), "resources.xml"
+            )
+        )
+        self.rootResource = getRootResource(config, self._sqlCalendarStore)
+        self.directory = self.rootResource.getDirectory()
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    def storeUnderTest(self):
+        """
+        Create and return a L{CalendarStore} for testing.
+        """
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def test_getCalendarObjectForPrincipals(self):
+        """
+        Test that L{twistedcaldav.scheduling.utils.getCalendarObjectForPrincipals} detects and removes
+        resources with duplicate UIDs in the same calendar home.
+        """
+
+        # Check that expected resources are present
+        request = FakeRequest(self.rootResource, "PUT", path='/user01/outbox')
+        for uri in (
+            "/calendars/__uids__/user01/calendar1/1.ics",
+            "/calendars/__uids__/user02/calendar1/2.ics",
+            "/calendars/__uids__/user02/calendar2/3.ics",
+        ):
+            resource = (yield request.locateResource(uri))
+            self.assertNotEqual(resource, None)
+        yield request._newStoreTransaction.commit()
+
+        # Look up resource by UID in home where only one exists
+        request = FakeRequest(self.rootResource, "PUT", path='/user01/outbox')
+        principalCollection = self.directory.principalCollection
+        principal = principalCollection.principalForUID("user01")
+        _ignore_resource, rname, _ignore_calendar, calendar_uri = (yield getCalendarObjectForPrincipals(request, principal, "685BC3A1-195A-49B3-926D-388DDACA78A6"))
+        self.assertEqual(rname, "1.ics")
+        self.assertEqual(calendar_uri, "/calendars/__uids__/user01/calendar1")
+        yield request._newStoreTransaction.commit()
+
+        # Check that expected resources are still present
+        request = FakeRequest(self.rootResource, "PUT", path='/user01/outbox')
+        for uri in (
+            "/calendars/__uids__/user01/calendar1/1.ics",
+            "/calendars/__uids__/user02/calendar1/2.ics",
+            "/calendars/__uids__/user02/calendar2/3.ics",
+        ):
+            resource = (yield request.locateResource(uri))
+            self.assertNotEqual(resource, None)
+
+        # Look up resource by UID in home where two exists
+        request = FakeRequest(self.rootResource, "PUT", path='/user01/outbox')
+        principalCollection = self.directory.principalCollection
+        principal = principalCollection.principalForUID("user02")
+        _ignore_resource, rname, _ignore_calendar, calendar_uri = (yield getCalendarObjectForPrincipals(request, principal, "685BC3A1-195A-49B3-926D-388DDACA78A6"))
+        self.assertTrue(
+            (rname, calendar_uri) in
+            (
+                ("2.ics", "/calendars/__uids__/user02/calendar2"),
+                ("3.ics", "/calendars/__uids__/user02/calendar3"),
+            )
+        )
+        yield request._newStoreTransaction.commit()
+
+        # Check that expected resources are still present, but the duplicate missing
+        request = FakeRequest(self.rootResource, "PUT", path='/user01/outbox')
+        resource = (yield request.locateResource("/calendars/__uids__/user01/calendar1/1.ics"))
+        self.assertNotEqual(resource, None)
+        resource2 = (yield request.locateResource("/calendars/__uids__/user02/calendar2/2.ics"))
+        resource3 = (yield request.locateResource("/calendars/__uids__/user02/calendar3/3.ics"))
+        self.assertTrue(resource2.exists() ^ resource3.exists())

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/utils.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/utils.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/utils.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -24,6 +24,9 @@
 def getCalendarObjectForPrincipals(request, principal, uid, allow_shared=False):
     """
     Get a copy of the event for a principal.
+
+    NOTE: if more than one resource with the same UID is found, we will delete all but
+    one of them to avoid scheduling problems.
     """
 
     result = {
@@ -44,13 +47,18 @@
         # Get matching newstore objects
         objectResources = (yield calendar_home.getCalendarResourcesForUID(uid, allow_shared))
 
+        if len(objectResources) > 1:
+            # Delete all but the first one
+            log.debug("Should only have zero or one scheduling object resource with UID '%s' in calendar home: %s" % (uid, calendar_home,))
+            for resource in objectResources[1:]:
+                yield resource._parentCollection.removeObjectResource(resource)
+            objectResources = objectResources[:1]
+
         # We really want only one or zero of these
         if len(objectResources) == 1:
             result["calendar_collection_uri"] = joinURL(calendar_home.url(), objectResources[0]._parentCollection.name())
             result["calendar_collection"] = (yield request.locateResource(result["calendar_collection_uri"]))
             result["resource_name"] = objectResources[0].name()
             result["resource"] = (yield request.locateResource(joinURL(result["calendar_collection_uri"], result["resource_name"])))
-        elif len(objectResources):
-            log.debug("Should only have zero or one scheduling object resource with UID '%s' in calendar home: %s" % (uid, calendar_home,))
 
     returnValue((result["resource"], result["resource_name"], result["calendar_collection"], result["calendar_collection_uri"],))

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/sharing.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/sharing.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -57,7 +57,15 @@
 
 
 class SharedResourceMixin(object):
+    """
+    A mix-in for calendar/addressbook resources that implements sharing-related
+    functionality.
 
+    @ivar _share: If this L{SharedCollectionMixin} is the sharee's version of a
+        resource, this refers to the L{Share} that describes it.
+    @type _share: L{Share} or L{NoneType}
+    """
+
     @inlineCallbacks
     def inviteProperty(self, request):
         """
@@ -89,7 +97,7 @@
                 ))
 
             # See if it is on the sharee calendar
-            if self.isShareeCollection():
+            if self.isShareeResource():
                 original = (yield request.locateResource(self._share.url()))
                 yield original.validateInvites(request)
                 invitations = yield original._allInvitations()
@@ -110,8 +118,10 @@
 
 
     def upgradeToShare(self):
-        """ Upgrade this collection to a shared state """
-
+        """
+        Set the resource-type property on this resource to indicate that this
+        is the owner's version of a resource which has been shared.
+        """
         # Change resourcetype
         rtype = self.resourceType()
         rtype = element.ResourceType(*(rtype.children + (customxml.SharedOwner(),)))
@@ -160,11 +170,16 @@
     @inlineCallbacks
     def directShare(self, request):
         """
-        Directly bind an accessible calendar/address book collection into the current
-        principal's calendar/addressbook home.
+        Directly bind an accessible calendar/address book collection into the
+        current principal's calendar/addressbook home.
 
         @param request: the request triggering this action
         @type request: L{IRequest}
+
+        @return: the (asynchronous) HTTP result to respond to the direct-share
+            request.
+        @rtype: L{Deferred} firing L{twext.web2.http.Response}, failing with
+            L{HTTPError}
         """
 
         # Need to have at least DAV:read to do this
@@ -228,19 +243,27 @@
 
     @inlineCallbacks
     def isShared(self, request):
-        """ Return True if this is an owner shared calendar collection """
+        """
+        Return True if this is an owner shared calendar collection.
+        """
         returnValue((yield self.isSpecialCollection(customxml.SharedOwner)) or
                     bool((yield self._allInvitations()))) # same as, len(SharedAs() + InvitedAs())
 
 
     def setShare(self, share):
-        self._isShareeCollection = True #  _isShareeCollection attr is used by self tests
+        """
+        Set the L{Share} associated with this L{SharedCollectionMixin}.  (This
+        is only invoked on the sharee's resource, not the owner's.)
+        """
+        self._isShareeResource = True #  _isShareeResource attr is used by self tests
         self._share = share
 
 
-    def isShareeCollection(self):
-        """ Return True if this is a sharee view of a shared calendar collection """
-        return hasattr(self, "_isShareeCollection")
+    def isShareeResource(self):
+        """
+        Return True if this is a sharee view of a shared calendar collection.
+        """
+        return hasattr(self, "_isShareeResource")
 
 
     @inlineCallbacks
@@ -265,8 +288,8 @@
         else:
             rtype = superMethod()
 
-        isShareeCollection = self.isShareeCollection()
-        if isShareeCollection:
+        isShareeResource = self.isShareeResource()
+        if isShareeResource:
             rtype = element.ResourceType(
                 *(
                     tuple([child for child in rtype.children if child.qname() != customxml.SharedOwner.qname()]) +
@@ -293,49 +316,82 @@
 
 
     @inlineCallbacks
-    def shareeAccessControlList(self, request, *args, **kwargs):
+    def _checkAccessControl(self):
         """
-        Return WebDAV ACLs appropriate for the current user accessing the shared collection. For
-        an "invite" share we take the privilege granted to the sharee in the invite and map that
-        to WebDAV ACLs. For a "direct" share, if it is a wiki collection we map the wiki privileges
-        into WebDAV ACLs, otherwise we use whatever privileges exist on the underlying shared
-        collection.
+        Check the shared access mode of this resource, potentially consulting
+        an external access method if necessary.
 
-        @return: the appropriate WebDAV ACL for the sharee
-        @rtype: L{davxml.ACL}
+        @return: a L{Deferred} firing a L{bytes} or L{None}, with one of the
+            potential values: C{"own"}, which means that the home is the owner
+            of the collection and it is not shared; C{"read-only"}, meaning
+            that the home that this collection is bound into has only read
+            access to this collection; C{"read-write"}, which means that the
+            home has both read and write access; C{"original"}, which means
+            that it should inherit the ACLs of the owner's collection, whatever
+            those happen to be, or C{None}, which means that the external
+            access control mechanism has dictate the home should no longer have
+            any access at all.
         """
-
-        assert self._isShareeCollection, "Only call this for a sharee collection"
-
-        wikiAccessMethod = kwargs.get("wikiAccessMethod", getWikiAccess)
-
-        sharee = self.principalForUID(self._share.shareeUID())
-
-        # Direct shares use underlying privileges of shared collection
         if self._share.direct():
-            original = (yield request.locateResource(self._share.url()))
-            owner = yield original.ownerPrincipal(request)
+            ownerUID = self._share.ownerUID()
+            owner = self.principalForUID(ownerUID)
             if owner.record.recordType == WikiDirectoryService.recordType_wikis:
                 # Access level comes from what the wiki has granted to the
                 # sharee
+                sharee = self.principalForUID(self._share.shareeUID())
                 userID = sharee.record.guid
                 wikiID = owner.record.shortNames[0]
-                access = (yield wikiAccessMethod(userID, wikiID))
+                access = (yield getWikiAccess(userID, wikiID))
                 if access == "read":
-                    access = "read-only"
+                    returnValue("read-only")
                 elif access in ("write", "admin"):
-                    access = "read-write"
+                    returnValue("read-write")
                 else:
-                    access = None
+                    returnValue(None)
             else:
-                result = (yield original.accessControlList(request, *args,
-                    **kwargs))
-                returnValue(result)
+                returnValue("original")
         else:
             # Invited shares use access mode from the invite
             # Get the access for self
-            access = Invitation(self._newStoreObject).access()
+            returnValue(Invitation(self._newStoreObject).access())
 
+
+    @inlineCallbacks
+    def shareeAccessControlList(self, request, *args, **kwargs):
+        """
+        Return WebDAV ACLs appropriate for the current user accessing the
+        shared collection.  For an "invite" share we take the privilege granted
+        to the sharee in the invite and map that to WebDAV ACLs.  For a
+        "direct" share, if it is a wiki collection we map the wiki privileges
+        into WebDAV ACLs, otherwise we use whatever privileges exist on the
+        underlying shared collection.
+
+        @param request: the request used to locate the owner resource.
+        @type request: L{twext.web2.iweb.IRequest}
+
+        @param args: The arguments for
+            L{twext.web2.dav.idav.IDAVResource.accessControlList}
+
+        @param kwargs: The keyword arguments for
+            L{twext.web2.dav.idav.IDAVResource.accessControlList}, plus
+            keyword-only arguments.
+
+        @return: the appropriate WebDAV ACL for the sharee
+        @rtype: L{davxml.ACL}
+        """
+
+        assert self._isShareeResource, "Only call this for a sharee collection"
+
+        sharee = self.principalForUID(self._share.shareeUID())
+        access = yield self._checkAccessControl()
+
+        if access == "original":
+            original = (yield request.locateResource(self._share.url()))
+            result = (yield original.accessControlList(request, *args,
+                **kwargs))
+            returnValue(result)
+
+        # Direct shares use underlying privileges of shared collection
         userprivs = [
         ]
         if access in ("read-only", "read-write",):
@@ -464,7 +520,7 @@
         if type(cn) is not list:
             cn = [cn]
 
-        dl = [self.inviteSingleUserToShare(user, cn, ace, summary, request) for user, cn in zip(userid, cn)]
+        dl = [self.inviteSingleUserToShare(_user, _cn, ace, summary, request) for _user, _cn in zip(userid, cn)]
         return self._processShareActionList(dl, resultIsList)
 
 
@@ -494,7 +550,7 @@
         if type(cn) is not list:
             cn = [cn]
 
-        dl = [self.inviteSingleUserUpdateToShare(user, cn, aceOLD, aceNEW, summary, request) for user, cn in zip(userid, cn)]
+        dl = [self.inviteSingleUserUpdateToShare(_user, _cn, aceOLD, aceNEW, summary, request) for _user, _cn in zip(userid, cn)]
         return self._processShareActionList(dl, resultIsList)
 
 
@@ -515,11 +571,11 @@
         elif self.isAddressBookCollection() or self.isGroup():
             shareeHome = yield self._newStoreObject._txn.addressbookHomeWithUID(shareeUID, create=True)
 
-        shareeStoreObject = yield self._newStoreObject.shareWith(shareeHome,
+        shareUID = yield self._newStoreObject.shareWith(shareeHome,
                                                     mode=invitationAccessToBindModeMap[access],
                                                     status=_BIND_STATUS_INVITED,
                                                     message=summary)
-
+        shareeStoreObject = yield self._newStoreHome.objectWithShareUID(shareUID)
         invitation = Invitation(shareeStoreObject)
         returnValue(invitation)
 
@@ -546,6 +602,7 @@
         if not self.exists():
             returnValue([])
 
+        #TODO: Cache
         if True:#not hasattr(self, "_invitations"):
 
             acceptedHomeChildren = yield self._newStoreObject.asShared()
@@ -1055,9 +1112,9 @@
     @inlineCallbacks
     def _shareForUID(self, shareUID, request):
 
-        child = yield self._newStoreHome.objectWithShareUID(shareUID)
-        if child:
-            share = yield self._shareForStoreObject(child, request)
+        shareeStoreObject = yield self._newStoreHome.objectWithShareUID(shareUID)
+        if shareeStoreObject:
+            share = yield self._shareForStoreObject(shareeStoreObject, request)
             if share and share.uid() == shareUID:
                 returnValue(share)
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -628,12 +628,20 @@
             "OldDraftCompatibility"      : True, # Whether to maintain compatibility with non-implicit mode
             "ScheduleTagCompatibility"   : True, # Whether to support older clients that do not use Schedule-Tag feature
             "EnablePrivateComments"      : True, # Private comments from attendees to organizer
+            "PerAttendeeProperties"      : [     # Names of iCalendar properties that are preserved when an Attendee does an invite PUT
+                "X-APPLE-NEEDS-REPLY",
+                "X-APPLE-TRAVEL-DURATION",
+                "X-APPLE-TRAVEL-START",
+                "X-APPLE-TRAVEL-RETURN-DURATION",
+                "X-APPLE-TRAVEL-RETURN",
+            ],
         },
 
         "iSchedule": {
             "Enabled"          : False, # iSchedule protocol
             "AddressPatterns"  : [], # Reg-ex patterns to match iSchedule-able calendar user addresses
             "RemoteServers"    : "remoteservers.xml", # iSchedule server configurations
+            "SerialNumber"     : 1,  # Capabilities serial number
             "DNSDebug"         : "", # File where a fake Bind zone exists for creating fake DNS results
             "DKIM"             : {      # DKIM options
                 "Enabled"               : True, # DKIM signing/verification enabled
@@ -653,12 +661,6 @@
 
         "iMIP": {
             "Enabled"          : False, # Server-to-iMIP protocol
-            "MailGatewayServer" : "localhost",
-            "MailGatewayPort"   : 62310,
-            "Username"          : "com.apple.calendarserver", # For account injecting replies
-            "Password"          : "", # For account injecting replies
-            "GUID"              : "B86ED9D3-49BD-44F8-8F5E-C89D08753DAC", # GUID for special internal user
-            "Header"            : "x-calendarserver-internal", # HTTP header for internal authentication
             "Sending": {
                 "Server"        : "", # SMTP server to relay messages through
                 "Port"          : 587, # SMTP server port to relay messages through
@@ -727,16 +729,8 @@
     "Notifications" : {
         "Enabled": False,
         "CoalesceSeconds" : 3,
-        "InternalNotificationHost" : "localhost",
-        "InternalNotificationPort" : 62309,
-        "BindAddress" : "127.0.0.1",
 
         "Services" : {
-            "SimpleLineNotifier" : {
-                "Service" : "twistedcaldav.notify.SimpleLineNotifierService",
-                "Enabled" : False,
-                "Port" : 62308,
-            },
             "ApplePushNotifier" : {
                 "Service" : "calendarserver.push.applepush.ApplePushNotifierService",
                 "Enabled" : False,
@@ -770,37 +764,12 @@
             },
             "AMPNotifier" : {
                 "Service" : "calendarserver.push.amppush.AMPPushNotifierService",
-                "Enabled" : True,
+                "Enabled" : False,
                 "Port" : 62311,
                 "EnableStaggering" : False,
                 "StaggerSeconds" : 3,
+                "DataHost" : "",
             },
-            "XMPPNotifier" : {
-                "Service" : "twistedcaldav.notify.XMPPNotifierService",
-                "Enabled" : False,
-                "Host" : "", # "xmpp.host.name"
-                "Port" : 5222,
-                "JID" : "", # "jid at xmpp.host.name/resource"
-                "Password" : "",
-                "ServiceAddress" : "", # "pubsub.xmpp.host.name"
-                "CalDAV" : {
-                    "APSBundleID" : "",
-                    "SubscriptionURL" : "",
-                    "APSEnvironment" : "PRODUCTION",
-                },
-                "CardDAV" : {
-                    "APSBundleID" : "",
-                    "SubscriptionURL" : "",
-                    "APSEnvironment" : "PRODUCTION",
-                },
-                "NodeConfiguration" : {
-                    "pubsub#deliver_payloads" : "1",
-                    "pubsub#persist_items" : "1",
-                },
-                "KeepAliveSeconds" : 120,
-                "HeartbeatMinutes" : 30,
-                "AllowedJIDs": [],
-            },
         }
     },
 
@@ -1274,17 +1243,17 @@
 
     configDict.AdminACEs = tuple(
         davxml.ACE(
-            davxml.Principal(davxml.HRef(principal)),
+            davxml.Principal(davxml.HRef(admin_principal)),
             davxml.Grant(davxml.Privilege(davxml.All())),
             davxml.Protected(),
             TwistedACLInheritable(),
         )
-        for principal in configDict.AdminPrincipals
+        for admin_principal in configDict.AdminPrincipals
     )
 
     configDict.ReadACEs = tuple(
         davxml.ACE(
-            davxml.Principal(davxml.HRef(principal)),
+            davxml.Principal(davxml.HRef(read_principal)),
             davxml.Grant(
                 davxml.Privilege(davxml.Read()),
                 davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
@@ -1292,7 +1261,7 @@
             davxml.Protected(),
             TwistedACLInheritable(),
         )
-        for principal in configDict.ReadPrincipals
+        for read_principal in configDict.ReadPrincipals
     )
 
     configDict.RootResourceACL = davxml.ACL(
@@ -1312,7 +1281,7 @@
         # Add read and read-acl access for admins
         * [
             davxml.ACE(
-                davxml.Principal(davxml.HRef(principal)),
+                davxml.Principal(davxml.HRef(_principal)),
                 davxml.Grant(
                     davxml.Privilege(davxml.Read()),
                     davxml.Privilege(davxml.ReadACL()),
@@ -1320,7 +1289,7 @@
                 ),
                 davxml.Protected(),
             )
-            for principal in configDict.AdminPrincipals
+            for _principal in configDict.AdminPrincipals
         ]
     )
 
@@ -1417,39 +1386,18 @@
                 except KeychainPasswordNotFound:
                     # The password doesn't exist in the keychain.
                     log.info("%s APN certificate passphrase not found in keychain" % (protocol,))
-
+                    
         if (
-            service["Service"] == "twistedcaldav.notify.XMPPNotifierService" and
+            service["Service"] == "calendarserver.push.amppush.AMPPushNotifierService" and
             service["Enabled"]
         ):
-            # If we already have the password, don't fetch it again
-            if service["Password"]:
-                continue
+            # The default for apple push DataHost is ServerHostName
+            if service["DataHost"] == "":
+                service["DataHost"] = configDict.ServerHostName
 
-            # Get password from keychain.  If not there, fall back to what
-            # is in the plist.
-            try:
-                password = getPasswordFromKeychain(service["JID"])
-                service["Password"] = password
-                log.info("XMPP password retreived from keychain")
-            except KeychainAccessError:
-                # The system doesn't support keychain
-                pass
-            except KeychainPasswordNotFound:
-                # The password doesn't exist in the keychain.
-                log.info("XMPP password not found in keychain")
 
-            # Check for empty fields
-            for key, value in service.iteritems():
-                if not value and key not in (
-                    "AllowedJIDs", "HeartbeatMinutes", "Password",
-                    "SubscriptionURL", "APSBundleID"
-                ):
-                    raise ConfigurationError("Invalid %s for XMPPNotifierService: %r"
-                                             % (key, value))
 
 
-
 def _updateScheduling(configDict, reloading=False):
     #
     # Scheduling
@@ -1463,22 +1411,6 @@
 
     if service["Enabled"]:
 
-        # If we already have the password, don't fetch it again
-        if service["Password"]:
-            return
-
-        # Get password for the user that is allowed to inject iMIP replies
-        # to the server's /inbox; if not available, fall back to plist
-        if service["Username"]:
-            try:
-                service["Password"] = getPasswordFromKeychain(service["Username"])
-            except KeychainAccessError:
-                # The system doesn't support keychain
-                pass
-            except KeychainPasswordNotFound:
-                # The password doesn't exist in the keychain.
-                log.info("iMIP injecting password not found in keychain")
-
         for direction in ("Sending", "Receiving"):
             if service[direction].Username:
                 # Get password from keychain.  If not there, fall back to
@@ -1630,12 +1562,9 @@
     re-fetched after the process has shed privileges
     """
     iMIP = configDict.Scheduling.iMIP
-    XMPP = configDict.Notifications.Services.XMPPNotifier
     preserved = {
-        "iMIPPassword" : iMIP.Password,
         "MailSendingPassword" : iMIP.Sending.Password,
         "MailReceivingPassword" : iMIP.Receiving.Password,
-        "XMPPPassword" : XMPP.Password,
     }
     return preserved
 
@@ -1647,11 +1576,8 @@
     re-fetched after the process has shed privileges
     """
     iMIP = configDict.Scheduling.iMIP
-    XMPP = configDict.Notifications.Services.XMPPNotifier
-    iMIP.Password = preserved["iMIPPassword"]
     iMIP.Sending.Password = preserved["MailSendingPassword"]
     iMIP.Receiving.Password = preserved["MailReceivingPassword"]
-    XMPP.Password = preserved["XMPPPassword"]
 
 
 config.addResetHooks(_preserveConfig, _restoreConfig)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/storebridge.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/storebridge.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -15,66 +15,66 @@
 # limitations under the License.
 ##
 
-import time
-import hashlib
-from urlparse import urlsplit
+from pycalendar.datetime import PyCalendarDateTime
 
-from twisted.python.hashlib import md5
-from twisted.python.log import err as logDefaultException
-from twisted.python.util import FancyEqMixin
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue, maybeDeferred
-from twisted.internet.protocol import Protocol
-
 from twext.python.log import Logger
-
-from txdav.xml import element as davxml
-from txdav.xml.base import dav_namespace, WebDAVUnknownElement, encodeXMLName
-from txdav.base.propertystore.base import PropertyName
-from txdav.caldav.icalendarstore import QuotaExceeded, AttachmentStoreFailed, \
-    AttachmentStoreValidManagedID, AttachmentRemoveFailed
-from txdav.common.icommondatastore import NoSuchObjectResourceError
-from txdav.common.datastore.sql_tables import _BIND_MODE_READ, _BIND_MODE_WRITE
-from txdav.idav import PropertyChangeNotAllowedError
-
-from twext.web2.stream import ProducerStream, readStream, MemoryStream
-from twext.web2.http import HTTPError, StatusResponse, Response
-from twext.web2.http_headers import ETag, MimeType, MimeDisposition
 from twext.web2.dav.http import ErrorResponse, ResponseQueue, MultiStatusResponse
 from twext.web2.dav.noneprops import NonePropertyStore
 from twext.web2.dav.resource import TwistedACLInheritable, AccessDeniedError, \
     davPrivilegeSet
 from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL, davXMLFromStream
-from twext.web2.responsecode import (
-    FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED,
-    BAD_REQUEST, OK, INSUFFICIENT_STORAGE_SPACE, SERVICE_UNAVAILABLE
-, INTERNAL_SERVER_ERROR)
+from twext.web2.filter.location import addLocation
+from twext.web2.http import HTTPError, StatusResponse, Response
+from twext.web2.http_headers import ETag, MimeType, MimeDisposition
+from twext.web2.responsecode import \
+    FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED, \
+    BAD_REQUEST, OK, INSUFFICIENT_STORAGE_SPACE, SERVICE_UNAVAILABLE, \
+    INTERNAL_SERVER_ERROR
+from twext.web2.stream import ProducerStream, readStream, MemoryStream
 
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue, maybeDeferred
+from twisted.internet.protocol import Protocol
+from twisted.python.hashlib import md5
+from twisted.python.log import err as logDefaultException
+from twisted.python.util import FancyEqMixin
+
 from twistedcaldav import customxml, carddavxml, caldavxml
 from twistedcaldav.cache import CacheStoreNotifier, ResponseCacheMixin, \
     DisabledCacheNotifier
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.carddavxml import carddav_namespace
 from twistedcaldav.config import config
+from twistedcaldav.directory.wiki import WikiDirectoryService, getWikiAccess
 from twistedcaldav.ical import Component as VCalendar, Property as VProperty, \
     InvalidICalendarDataError, iCalendarProductID, allowedComponents
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
 from twistedcaldav.method.put_common import StoreCalendarObjectResource
-from twistedcaldav.notifications import (
-    NotificationCollectionResource, NotificationResource
-)
+from twistedcaldav.notifications import NotificationCollectionResource, NotificationResource
 from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource, \
     DefaultAlarmPropertyMixin
 from twistedcaldav.scheduling.caldav.resource import ScheduleInboxResource
 from twistedcaldav.scheduling.implicit import ImplicitScheduler
 from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
-from pycalendar.datetime import PyCalendarDateTime
-import uuid
-from twext.web2.filter.location import addLocation
 
+from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.icalendarstore import QuotaExceeded, AttachmentStoreFailed, \
+    AttachmentStoreValidManagedID, AttachmentRemoveFailed, \
+    AttachmentDropboxNotAllowed
 from txdav.carddav.iaddressbookstore import GroupWithUnsharedAddressNotAllowedError, \
     GroupForSharedAddressBookDeleteNotAllowedError, SharedGroupDeleteNotAllowedError
+from txdav.common.datastore.sql_tables import _BIND_MODE_READ, _BIND_MODE_WRITE, \
+    _BIND_MODE_DIRECT
+from txdav.common.icommondatastore import NoSuchObjectResourceError
+from txdav.idav import PropertyChangeNotAllowedError
+from txdav.xml import element as davxml
+from txdav.xml.base import dav_namespace, WebDAVUnknownElement, encodeXMLName
 
+from urlparse import urlsplit
+import hashlib
+import time
+import uuid
+
 """
 Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
 L{txdav.carddav.iaddressbookstore} and those in L{twistedcaldav}.
@@ -446,8 +446,8 @@
         """
 
         # Check sharee collection first
-        isShareeCollection = self.isShareeCollection()
-        if isShareeCollection:
+        isShareeResource = self.isShareeResource()
+        if isShareeResource:
             log.debug("Removing shared collection %s" % (self,))
             yield self.removeShareeCollection(request)
             returnValue(NO_CONTENT)
@@ -1813,7 +1813,7 @@
         All principals identified as ATTENDEEs on the event for this dropbox
         may read all its children. Also include proxies of ATTENDEEs. Ignore
         unknown attendees. Do not allow attendees to write as we don't support
-        that with managed attachments.
+        that with managed attachments. Also include sharees of the event.
         """
         originalACL = yield super(
             AttachmentsChildCollection, self).accessControlList(request, *a, **kw)
@@ -1874,24 +1874,63 @@
 
 
     @inlineCallbacks
+    def _sharedAccessControl(self, calendar, shareMode):
+        """
+        Check the shared access mode of this resource, potentially consulting
+        an external access method if necessary.
+
+        @return: a L{Deferred} firing a L{bytes} or L{None}, with one of the
+            potential values: C{"own"}, which means that the home is the owner
+            of the collection and it is not shared; C{"read-only"}, meaning
+            that the home that this collection is bound into has only read
+            access to this collection; C{"read-write"}, which means that the
+            home has both read and write access; C{"original"}, which means
+            that it should inherit the ACLs of the owner's collection, whatever
+            those happen to be, or C{None}, which means that the external
+            access control mechanism has dictate the home should no longer have
+            any access at all.
+        """
+        if shareMode in (_BIND_MODE_DIRECT,):
+            ownerUID = calendar.ownerHome().uid()
+            owner = self.principalForUID(ownerUID)
+            shareeUID = calendar.viewerHome().uid()
+            if owner.record.recordType == WikiDirectoryService.recordType_wikis:
+                # Access level comes from what the wiki has granted to the
+                # sharee
+                sharee = self.principalForUID(shareeUID)
+                userID = sharee.record.guid
+                wikiID = owner.record.shortNames[0]
+                access = (yield getWikiAccess(userID, wikiID))
+                if access == "read":
+                    returnValue("read-only")
+                elif access in ("write", "admin"):
+                    returnValue("read-write")
+                else:
+                    returnValue(None)
+            else:
+                returnValue("original")
+        elif shareMode in (_BIND_MODE_READ,):
+            returnValue("read-only")
+        elif shareMode in (_BIND_MODE_WRITE,):
+            returnValue("read-write")
+        returnValue("original")
+
+
+    @inlineCallbacks
     def sharedDropboxACEs(self):
 
         aces = ()
         calendars = yield self._newStoreCalendarObject._parentCollection.asShared()
         for calendar in calendars:
 
-            userprivs = [
+            privileges = [
+                davxml.Privilege(davxml.Read()),
+                davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
             ]
-            if calendar.shareMode() in (_BIND_MODE_READ, _BIND_MODE_WRITE,):
-                userprivs.append(davxml.Privilege(davxml.Read()))
-                userprivs.append(davxml.Privilege(davxml.ReadACL()))
-                userprivs.append(davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()))
-            if calendar.shareMode() in (_BIND_MODE_READ,):
-                userprivs.append(davxml.Privilege(davxml.WriteProperties()))
-            if calendar.shareMode() in (_BIND_MODE_WRITE,):
-                userprivs.append(davxml.Privilege(davxml.Write()))
-            proxyprivs = list(userprivs)
-            proxyprivs.remove(davxml.Privilege(davxml.ReadACL()))
+            userprivs = []
+            access = (yield self._sharedAccessControl(calendar, calendar.shareMode()))
+            if access in ("read-only", "read-write",):
+                userprivs.extend(privileges)
 
             principal = self.principalForUID(calendar._home.uid())
             aces += (
@@ -1909,17 +1948,14 @@
                     # 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(
-                            davxml.Privilege(davxml.Read()),
-                            davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
-                        ),
+                        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(*proxyprivs),
+                        davxml.Grant(*userprivs),
                         davxml.Protected(),
                         TwistedACLInheritable(),
                     ),
@@ -1970,6 +2006,11 @@
                         self.attachmentName))
             t = self._newStoreAttachment.store(content_type)
             yield readStream(request.stream, t.write)
+
+        except AttachmentDropboxNotAllowed:
+            log.error("Dropbox cannot be used after migration to managed attachments")
+            raise HTTPError(FORBIDDEN)
+
         except Exception, e:
             log.error("Unable to store attachment: %s" % (e,))
             raise HTTPError(SERVICE_UNAVAILABLE)
@@ -2047,101 +2088,7 @@
         return succeed(davPrivilegeSet)
 
 
-    @inlineCallbacks
-    def accessControlList(self, request, *a, **kw):
-        """
-        Special case managed attachments, but not dropbox (which is handled by parent collection).
-        All principals identified as ATTENDEEs on the event for this attachment
-        may read it. Also include proxies of ATTENDEEs. Ignore unknown attendees.
-        """
 
-        originalACL = yield super(CalendarAttachment, self).accessControlList(request, *a, **kw)
-        if not self._managed or not self.exists():
-            returnValue(originalACL)
-        originalACEs = list(originalACL.children)
-
-        # Look at attendees
-        if self._newStoreCalendarObject is None:
-            self._newStoreCalendarObject = (yield self._newStoreAttachment.objectResource())
-
-        cuas = (yield self._newStoreCalendarObject.component()).getAttendees()
-        newACEs = []
-        for calendarUserAddress in cuas:
-            principal = self.principalForCalendarUserAddress(
-                calendarUserAddress
-            )
-            if principal is None:
-                continue
-
-            principalURL = principal.principalURL()
-            privileges = (
-                davxml.Privilege(davxml.Read()),
-                davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
-            )
-            newACEs.append(davxml.ACE(
-                davxml.Principal(davxml.HRef(principalURL)),
-                davxml.Grant(*privileges),
-                davxml.Protected(),
-            ))
-            newACEs.append(davxml.ACE(
-                davxml.Principal(davxml.HRef(joinURL(principalURL, "calendar-proxy-write/"))),
-                davxml.Grant(*privileges),
-                davxml.Protected(),
-            ))
-            newACEs.append(davxml.ACE(
-                davxml.Principal(davxml.HRef(joinURL(principalURL, "calendar-proxy-read/"))),
-                davxml.Grant(*privileges),
-                davxml.Protected(),
-            ))
-
-        # Now also need sharees
-        newACEs.extend((yield self.sharedManagedACEs()))
-
-        returnValue(davxml.ACL(*tuple(originalACEs + newACEs)))
-
-
-    @inlineCallbacks
-    def sharedManagedACEs(self):
-
-        aces = ()
-        calendars = yield self._newStoreCalendarObject._parentCollection.asShared()
-        for calendar in calendars:
-
-            read_privs = (
-                davxml.Privilege(davxml.Read()),
-                davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
-            )
-
-            principal = self.principalForUID(calendar._home.uid())
-            aces += (
-                # Specific access for the resource's associated principal.
-                davxml.ACE(
-                    davxml.Principal(davxml.HRef(principal.principalURL())),
-                    davxml.Grant(*read_privs),
-                    davxml.Protected(),
-                ),
-            )
-
-            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(*read_privs),
-                        davxml.Protected(),
-                    ),
-                    # 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(*read_privs),
-                        davxml.Protected(),
-                    ),
-                )
-
-        returnValue(aces)
-
-
-
 class NoParent(CalDAVResource):
 
     def http_MKCALENDAR(self, request):
@@ -2545,7 +2492,7 @@
         @type request: L{twext.web2.iweb.IRequest}
 
         @param implicitly: Should implicit scheduling operations be triggered
-            as a resut of this C{DELETE}?
+            as a result of this C{DELETE}?
 
         @type implicitly: C{bool}
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_config.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_config.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -43,18 +43,6 @@
     <string>debug</string>
   </dict>
 
-  <key>Notifications</key>
-  <dict>
-    <key>Services</key>
-    <dict>
-      <key>XMPPNotifier</key>
-      <dict>
-          <key>Password</key>
-          <string>xmpp</string>
-      </dict>
-    </dict>
-  </dict>
-
   <key>Scheduling</key>
   <dict>
     <key>iMIP</key>
@@ -168,26 +156,20 @@
         self.assertEquals(config.HTTPPort, 8008)
 
     def testPreserveAcrossReload(self):
-        self.assertEquals(config.Scheduling.iMIP.Password, "")
         self.assertEquals(config.Scheduling.iMIP.Sending.Password, "")
         self.assertEquals(config.Scheduling.iMIP.Receiving.Password, "")
-        self.assertEquals(config.Notifications.Services.XMPPNotifier.Password, "")
 
         config.load(self.testConfig)
 
-        self.assertEquals(config.Scheduling.iMIP.Password, "imip")
         self.assertEquals(config.Scheduling.iMIP.Sending.Password, "sending")
         self.assertEquals(config.Scheduling.iMIP.Receiving.Password, "receiving")
-        self.assertEquals(config.Notifications.Services.XMPPNotifier.Password, "xmpp")
 
         writePlist({}, self.testConfig)
 
         config.reload()
 
-        self.assertEquals(config.Scheduling.iMIP.Password, "imip")
         self.assertEquals(config.Scheduling.iMIP.Sending.Password, "sending")
         self.assertEquals(config.Scheduling.iMIP.Receiving.Password, "receiving")
-        self.assertEquals(config.Notifications.Services.XMPPNotifier.Password, "xmpp")
 
     def testSetAttr(self):
         self.assertNotIn("BindAddresses", config.__dict__)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_resource.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_resource.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -36,7 +36,7 @@
 class StubHome(object):
     def properties(self):
         return []
-    
+
     def addNotifier(self, notifier):
         pass
 
@@ -73,66 +73,20 @@
         resource = CalendarHomeResource(None, None, None, StubHome())
         self.assertTrue(('http://calendarserver.org/ns/', 'push-transports') in resource.liveProperties())
         self.assertTrue(('http://calendarserver.org/ns/', 'pushkey') in resource.liveProperties())
-        self.assertTrue(('http://calendarserver.org/ns/', 'xmpp-uri') in resource.liveProperties())
-        self.assertTrue(('http://calendarserver.org/ns/', 'xmpp-heartbeat-uri') in resource.liveProperties())
-        self.assertTrue(('http://calendarserver.org/ns/', 'xmpp-server') in resource.liveProperties())
 
 
     def test_addressBookHomeliveProperties(self):
         resource = AddressBookHomeResource(None, None, None, StubHome())
         self.assertTrue(('http://calendarserver.org/ns/', 'push-transports') in resource.liveProperties())
         self.assertTrue(('http://calendarserver.org/ns/', 'pushkey') in resource.liveProperties())
-        self.assertTrue(('http://calendarserver.org/ns/', 'xmpp-uri') not in resource.liveProperties())
-        self.assertTrue(('http://calendarserver.org/ns/', 'xmpp-heartbeat-uri') not in resource.liveProperties())
-        self.assertTrue(('http://calendarserver.org/ns/', 'xmpp-server') not in resource.liveProperties())
 
     def test_notificationCollectionLiveProperties(self):
         resource = NotificationCollectionResource()
         self.assertTrue(('http://calendarserver.org/ns/', 'getctag') in resource.liveProperties())
 
 
-    @inlineCallbacks
-    def test_push404(self):
-        """
-        If push is configured, yet we can't communicate with the XMPP server
-        for whatever reason, readProperty on the various push-related properties
-        should return None
-        """
-        resource = CalendarHomeResource(None, None, None, StubHome())
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'push-transports'), None)), None)
 
-        self.patch(config, "ServerHostName", "cal.example.com")
-        self.patch(config, "SSLPort", 8443)
-        self.patch(config.Notifications, "Enabled", True)
-        self.patch(config.Notifications.Services, "XMPPNotifier", 
-            {
-                "Enabled" : True,
-                "Host" : "xmpp.example.com",
-                "Port" : 5218,
-                "ServiceAddress" : "pubsub.xmpp.example.com",
-                "Service" : "twistedcaldav.notify.XMPPNotifierService",
-                "HeartbeatMinutes" : 30,
-            }
-        )
 
-        # Verify that when push is "working" we get a value
-        resource._newStoreHome.setPushWorking(True)
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'push-transports'), None)).toxml(), "<?xml version='1.0' encoding='UTF-8'?>\n<push-transports xmlns='http://calendarserver.org/ns/'>\r\n  <transport type='XMPP'>\r\n    <xmpp-server>xmpp.example.com:5218</xmpp-server>\r\n    <xmpp-uri>xmpp:pubsub.xmpp.example.com?pubsub;node=/cal.example.com/xyzzy/</xmpp-uri>\r\n  </transport>\r\n</push-transports>")
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'pushkey'), None)).toxml(), "<?xml version='1.0' encoding='UTF-8'?>\n<pushkey xmlns='http://calendarserver.org/ns/'>xyzzy</pushkey>")
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'xmpp-uri'), None)).toxml(), "<?xml version='1.0' encoding='UTF-8'?>\n<xmpp-uri xmlns='http://calendarserver.org/ns/'>xmpp:pubsub.xmpp.example.com?pubsub;node=/cal.example.com/xyzzy/</xmpp-uri>")
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'xmpp-heartbeat-uri'), None)).toxml(), "<?xml version='1.0' encoding='UTF-8'?>\n<xmpp-heartbeat xmlns='http://calendarserver.org/ns/'>\r\n  <xmpp-heartbeat-uri>xmpp:pubsub.xmpp.example.com?pubsub;node=/cal.example.com/</xmpp-heartbeat-uri>\r\n  <xmpp-heartbeat-minutes>30</xmpp-heartbeat-minutes>\r\n</xmpp-heartbeat>")
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'xmpp-server'), None)).toxml(), "<?xml version='1.0' encoding='UTF-8'?>\n<xmpp-server xmlns='http://calendarserver.org/ns/'>xmpp.example.com:5218</xmpp-server>")
-
-        # Verify that when push is "not working" we get None
-        resource._newStoreHome.setPushWorking(False)
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'push-transports'), None)), None)
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'pushkey'), None)), None)
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'xmpp-uri'), None)), None)
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'xmpp-heartbeat-uri'), None)), None)
-        self.assertEqual((yield resource.readProperty(('http://calendarserver.org/ns/', 'xmpp-server'), None)), None)
-
-
-
 class OwnershipTests(TestCase):
     """
     L{CalDAVResource.isOwner} determines if the authenticated principal of the
@@ -228,8 +182,8 @@
         """
         Make calendar
         """
-        
 
+
         request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
         home = yield request.locateResource("/addressbooks/users/wsanchez")
 
@@ -257,8 +211,8 @@
         """
         Make adbk
         """
-        
 
+
         request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
         home = yield request.locateResource("/addressbooks/users/wsanchez")
 
@@ -277,7 +231,7 @@
             HRef("/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newadbk/")
         ))
         request._newStoreTransaction.commit()
-        
+
         # Delete the normal adbk
         request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
         home = yield request.locateResource("/addressbooks/users/wsanchez")
@@ -285,7 +239,7 @@
         yield adbk.storeRemove(request, False, "/addressbooks/users/wsanchez/addressbook")
 
         home.removeDeadProperty(carddavxml.DefaultAddressBookURL)
-        
+
         # default property not present
         try:
             home.readDeadProperty(carddavxml.DefaultAddressBookURL)
@@ -313,8 +267,8 @@
         """
         Make calendar
         """
-        
 
+
         request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
         home = yield request.locateResource("/addressbooks/users/wsanchez")
 
@@ -330,10 +284,10 @@
             self.fail("carddavxml.DefaultAddressBookURL is not present")
         else:
             self.assertEqual(str(default.children[0]), "/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newadbk/")
-        
+
         # Force the new calendar to think it is a sharee collection
-        newadbk._isShareeCollection = True
-        
+        newadbk._isShareeResource = True
+
         try:
             default = yield home.readProperty(carddavxml.DefaultAddressBookURL, request)
         except HTTPError:

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_sharing.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_sharing.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -14,87 +14,174 @@
 # limitations under the License.
 ##
 
+from xml.etree.cElementTree import XML
 
+from txdav.xml import element as davxml
+from txdav.xml.parser import WebDAVDocument
+
 from twext.web2 import responsecode
-from txdav.xml import element as davxml
-from twext.web2.http_headers import MimeType
-from twext.web2.iweb import IResource
-from twext.web2.stream import MemoryStream
 from twext.web2.test.test_server import SimpleRequest
+
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twistedcaldav import customxml
 from twistedcaldav.config import config
 from twistedcaldav.test.util import HomeTestCase, norequest
-from twistedcaldav.sharing import SharedResourceMixin, WikiDirectoryService
+from twistedcaldav import sharing
+from twistedcaldav.sharing import WikiDirectoryService
 
 from twistedcaldav.resource import CalDAVResource
+
 from txdav.common.datastore.test.util import buildStore, StubNotifierFactory
-from zope.interface import implements
+from txdav.caldav.icalendarstore import BIND_DIRECT
+from twistedcaldav.test.test_cache import StubResponseCacheResource
 
 
 sharedOwnerType = davxml.ResourceType.sharedownercalendar #@UndefinedVariable
 regularCalendarType = davxml.ResourceType.calendar #@UndefinedVariable
 
-class SharingTests(HomeTestCase):
 
-    class FakePrincipal(object):
 
-        class FakeRecord(object):
+def normalize(x):
+    """
+    Normalize some XML by parsing it, collapsing whitespace, and
+    pretty-printing.
+    """
+    return WebDAVDocument.fromString(x).toxml()
 
-            def __init__(self, name, cuaddr):
-                self.fullName = name
-                self.guid = name
-                self.calendarUserAddresses = set((cuaddr,))
 
-        def __init__(self, cuaddr):
-            if cuaddr.startswith("mailto:"):
-                name = cuaddr[7:].split('@')[0]
-            elif cuaddr.startswith("urn:uuid:"):
-                name = cuaddr[9:]
-            else:
-                name = cuaddr
 
-            self.path = "/principals/__uids__/%s" % (name,)
-            self.homepath = "/calendars/__uids__/%s" % (name,)
-            self.displayname = name.upper()
-            self.record = self.FakeRecord(name, cuaddr)
+class FakeHome(object):
+    def removeShareByUID(self, request, uid):
+        pass
 
 
-        def calendarHome(self, request):
-            class FakeHome(object):
-                def removeShareByUID(self, request, uid):
-                    pass
-            return FakeHome()
 
-        def principalURL(self):
-            return self.path
+class FakeRecord(object):
 
-        def principalUID(self):
-            return self.record.guid
+    def __init__(self, name, cuaddr):
+        self.fullName = name
+        self.guid = name
+        self.calendarUserAddresses = set((cuaddr,))
+        if name.startswith("wiki-"):
+            recordType = WikiDirectoryService.recordType_wikis
+        else:
+            recordType = None
+        self.recordType = recordType
+        self.shortNames = [name]
 
-        def displayName(self):
-            return self.displayname
 
 
+class FakePrincipal(object):
+
+    def __init__(self, cuaddr, test):
+        if cuaddr.startswith("mailto:"):
+            name = cuaddr[7:].split('@')[0]
+        elif cuaddr.startswith("urn:uuid:"):
+            name = cuaddr[9:]
+        else:
+            name = cuaddr
+
+        self.path = "/principals/__uids__/%s" % (name,)
+        self.homepath = "/calendars/__uids__/%s" % (name,)
+        self.displayname = name.upper()
+        self.record = FakeRecord(name, cuaddr)
+        self._test = test
+        self._name = name
+
+
     @inlineCallbacks
+    def calendarHome(self, request):
+        a, seg = yield self._test.homeProvisioner.locateChild(request,
+                                                              ["__uids__"])
+        b, seg = yield a.locateChild(request, [self._name])
+        if b is None:
+            # XXX all tests except test_noWikiAccess currently rely on the
+            # fake thing here.
+            returnValue(FakeHome())
+        returnValue(b)
+
+
+    def principalURL(self):
+        return self.path
+
+
+    def principalUID(self):
+        return self.record.guid
+
+
+    def displayName(self):
+        return self.displayname
+
+
+
+class SharingTests(HomeTestCase):
+
+    def configure(self):
+        """
+        Override configuration hook to turn on sharing.
+        """
+        super(SharingTests, self).configure()
+        self.patch(config.Sharing, "Enabled", True)
+        self.patch(config.Sharing.Calendars, "Enabled", True)
+
+
+    @inlineCallbacks
     def setUp(self):
         self.calendarStore = yield buildStore(self, StubNotifierFactory())
 
         yield super(SharingTests, self).setUp()
 
-        self.patch(config.Sharing, "Enabled", True)
-        self.patch(config.Sharing.Calendars, "Enabled", True)
+        def patched(c):
+            """
+            The decorated method is patched on L{CalDAVResource} for the
+            duration of the test.
+            """
+            self.patch(CalDAVResource, c.__name__, c)
+            return c
 
-        CalDAVResource.sendInviteNotification = lambda self, record, request: succeed(True)
-        CalDAVResource.removeInviteNotification = lambda self, record, request: succeed(True)
+        @patched
+        def sendInviteNotification(resourceSelf, record, request):
+            """
+            For testing purposes, sending an invite notification succeeds
+            without doing anything.
+            """
+            return succeed(True)
 
-        self.patch(CalDAVResource, "validUserIDForShare", lambda self, userid, request: None if "bogus" in userid else SharingTests.FakePrincipal(userid).principalURL())
-        self.patch(CalDAVResource, "principalForCalendarUserAddress", lambda self, cuaddr: None if "bogus" in cuaddr else SharingTests.FakePrincipal(cuaddr))
-        self.patch(CalDAVResource, "principalForUID", lambda self, principalUID: SharingTests.FakePrincipal("urn:uuid:" + principalUID))
+        @patched
+        def removeInviteNotification(resourceSelf, record, request):
+            """
+            For testing purposes, removing an invite notification succeeds
+            without doing anything.
+            """
+            return succeed(True)
 
+        @patched
+        def principalForCalendarUserAddress(resourceSelf, cuaddr):
+            if "bogus" in cuaddr:
+                return None
+            else:
+                return FakePrincipal(cuaddr, self)
+
+        @patched
+        def validUserIDForShare(resourceSelf, userid, request):
+            """
+            Temporary replacement for L{CalDAVResource.validUserIDForShare}
+            that marks any principal without 'bogus' in its name.
+            """
+            result = principalForCalendarUserAddress(resourceSelf, userid)
+            if result is None:
+                return result
+            return result.principalURL()
+
+        @patched
+        def principalForUID(resourceSelf, principalUID):
+            return FakePrincipal("urn:uuid:" + principalUID, self)
+
+
     def createDataStore(self):
         return self.calendarStore
 
+
     @inlineCallbacks
     def _refreshRoot(self, request=None):
         if request is None:
@@ -103,20 +190,16 @@
         self.resource = (
             yield self.site.resource.locateChild(request, ["calendar"])
         )[0]
+        self.site.resource.responseCache = StubResponseCacheResource()
+        self.site.resource.putChild("calendars", self.homeProvisioner)
         returnValue(result)
 
 
-    @inlineCallbacks
     def _doPOST(self, body, resultcode=responsecode.OK):
-        request = SimpleRequest(self.site, "POST", "/calendar/")
-        request.headers.setHeader("content-type", MimeType("text", "xml"))
-        request.stream = MemoryStream(body)
+        return self.simpleSend("POST", "/calendar/", body,
+                               resultcode=resultcode)
 
-        response = (yield self.send(request, None))
-        self.assertEqual(response.code, resultcode)
-        returnValue(response)
 
-
     def _clearUIDElementValue(self, xml):
 
         for user in xml.children:
@@ -133,8 +216,8 @@
         self.assertEquals(rtype, regularCalendarType)
         isShared = (yield self.resource.isShared(None))
         self.assertFalse(isShared)
-        isShareeCollection = self.resource.isShareeCollection()
-        self.assertFalse(isShareeCollection)
+        isShareeResource = self.resource.isShareeResource()
+        self.assertFalse(isShareeResource)
 
         self.resource.upgradeToShare()
 
@@ -142,8 +225,8 @@
         self.assertEquals(rtype, sharedOwnerType)
         isShared = (yield self.resource.isShared(None))
         self.assertTrue(isShared)
-        isShareeCollection = self.resource.isShareeCollection()
-        self.assertFalse(isShareeCollection)
+        isShareeResource = self.resource.isShareeResource()
+        self.assertFalse(isShareeResource)
 
 
     @inlineCallbacks
@@ -155,8 +238,8 @@
         self.assertEquals(rtype, sharedOwnerType)
         isShared = (yield self.resource.isShared(None))
         self.assertTrue(isShared)
-        isShareeCollection = self.resource.isShareeCollection()
-        self.assertFalse(isShareeCollection)
+        isShareeResource = self.resource.isShareeResource()
+        self.assertFalse(isShareeResource)
 
         yield self.resource.downgradeFromShare(None)
 
@@ -164,8 +247,8 @@
         self.assertEquals(rtype, regularCalendarType)
         isShared = (yield self.resource.isShared(None))
         self.assertFalse(isShared)
-        isShareeCollection = self.resource.isShareeCollection()
-        self.assertFalse(isShareeCollection)
+        isShareeResource = self.resource.isShareeResource()
+        self.assertFalse(isShareeResource)
 
 
     @inlineCallbacks
@@ -174,14 +257,14 @@
         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>
-""")
+            <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))
         self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -196,23 +279,22 @@
 
         isShared = (yield self.resource.isShared(None))
         self.assertTrue(isShared)
-        isShareeCollection = self.resource.isShareeCollection()
-        self.assertFalse(isShareeCollection)
+        isShareeResource = self.resource.isShareeResource()
+        self.assertFalse(isShareeResource)
 
 
     @inlineCallbacks
     def test_POSTaddInviteeNotAlreadyShared(self):
 
         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>
-"""
-        )
+        <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))
         self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -227,8 +309,8 @@
 
         isShared = (yield self.resource.isShared(None))
         self.assertTrue(isShared)
-        isShareeCollection = (yield self.resource.isShareeCollection())
-        self.assertFalse(isShareeCollection)
+        isShareeResource = (yield self.resource.isShareeResource())
+        self.assertFalse(isShareeResource)
 
 
     @inlineCallbacks
@@ -238,27 +320,27 @@
         self.assertFalse(isShared)
 
         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>
-""")
+            <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>
+            """)
 
         isShared = (yield self.resource.isShared(None))
         self.assertTrue(isShared)
 
         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/>
-    </CS:set>
-</CS:share>
-""")
+            <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/>
+                </CS:set>
+            </CS:share>
+            """)
 
         isShared = (yield self.resource.isShared(None))
         self.assertTrue(isShared)
@@ -282,25 +364,25 @@
         self.assertFalse(isShared)
 
         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>
-""")
+            <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>
+            """)
 
         isShared = (yield self.resource.isShared(None))
         self.assertTrue(isShared)
 
         yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
-    <CS:remove>
-        <D:href>mailto:user02 at example.com</D:href>
-    </CS:remove>
-</CS:share>
-""")
+            <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+                <CS:remove>
+                    <D:href>mailto:user02 at example.com</D:href>
+                </CS:remove>
+            </CS:share>
+            """)
 
         isShared = (yield self.resource.isShared(None))
         self.assertFalse(isShared)
@@ -315,28 +397,28 @@
         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>
-""")
+            <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>
+            """)
         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:user03 at example.com</D:href>
-        <CS:summary>Your Shared Calendar</CS:summary>
-        <CS:read-write/>
-    </CS:set>
-    <CS:set>
-        <D:href>mailto:user04 at example.com</D:href>
-        <CS:summary>Your Shared Calendar</CS:summary>
-        <CS:read-write/>
-    </CS:set>
-</CS:share>
-""")
+            <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+                <CS:set>
+                    <D:href>mailto:user03 at example.com</D:href>
+                    <CS:summary>Your Shared Calendar</CS:summary>
+                    <CS:read-write/>
+                </CS:set>
+                <CS:set>
+                    <D:href>mailto:user04 at example.com</D:href>
+                    <CS:summary>Your Shared Calendar</CS:summary>
+                    <CS:read-write/>
+                </CS:set>
+            </CS:share>
+            """)
 
         propInvite = (yield self.resource.readProperty(customxml.Invite, None))
         self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -370,31 +452,31 @@
         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:set>
-        <D:href>mailto:user03 at example.com</D:href>
-        <CS:summary>My Shared Calendar</CS:summary>
-        <CS:read-write/>
-    </CS:set>
-</CS:share>
-""")
+            <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:set>
+                    <D:href>mailto:user03 at example.com</D:href>
+                    <CS:summary>My Shared Calendar</CS:summary>
+                    <CS:read-write/>
+                </CS:set>
+            </CS:share>
+            """)
         yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
-    <CS:remove>
-        <D:href>mailto:user03 at example.com</D:href>
-    </CS:remove>
-    <CS:set>
-        <D:href>mailto:user04 at example.com</D:href>
-        <CS:summary>Your Shared Calendar</CS:summary>
-        <CS:read-write/>
-    </CS:set>
-</CS:share>
-""")
+            <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+                <CS:remove>
+                    <D:href>mailto:user03 at example.com</D:href>
+                </CS:remove>
+                <CS:set>
+                    <D:href>mailto:user04 at example.com</D:href>
+                    <CS:summary>Your Shared Calendar</CS:summary>
+                    <CS:read-write/>
+                </CS:set>
+            </CS:share>
+            """)
 
         propInvite = (yield self.resource.readProperty(customxml.Invite, None))
         self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -420,31 +502,31 @@
         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:set>
-        <D:href>mailto:user03 at example.com</D:href>
-        <CS:summary>My Shared Calendar</CS:summary>
-        <CS:read-write/>
-    </CS:set>
-</CS:share>
-""")
+            <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:set>
+                    <D:href>mailto:user03 at example.com</D:href>
+                    <CS:summary>My Shared Calendar</CS:summary>
+                    <CS:read-write/>
+                </CS:set>
+            </CS:share>
+            """)
         yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
-    <CS:remove>
-        <D:href>mailto:user03 at example.com</D:href>
-    </CS:remove>
-    <CS:set>
-        <D:href>mailto:user03 at example.com</D:href>
-        <CS:summary>Your Shared Calendar</CS:summary>
-        <CS:read/>
-    </CS:set>
-</CS:share>
-""")
+            <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+                <CS:remove>
+                    <D:href>mailto:user03 at example.com</D:href>
+                </CS:remove>
+                <CS:set>
+                    <D:href>mailto:user03 at example.com</D:href>
+                    <CS:summary>Your Shared Calendar</CS:summary>
+                    <CS:read/>
+                </CS:set>
+            </CS:share>
+            """)
 
         propInvite = (yield self.resource.readProperty(customxml.Invite, None))
         self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -476,36 +558,36 @@
         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:set>
-        <D:href>mailto:user03 at example.com</D:href>
-        <CS:summary>My Shared Calendar</CS:summary>
-        <CS:read-write/>
-    </CS:set>
-</CS:share>
-""")
+            <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:set>
+                    <D:href>mailto:user03 at example.com</D:href>
+                    <CS:summary>My Shared Calendar</CS:summary>
+                    <CS:read-write/>
+                </CS:set>
+            </CS:share>
+            """)
         yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
-    <CS:remove>
-        <D:href>mailto:user03 at example.com</D:href>
-    </CS:remove>
-</CS:share>
-""")
+            <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+                <CS:remove>
+                    <D:href>mailto:user03 at example.com</D:href>
+                </CS:remove>
+            </CS:share>
+            """)
         yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
-    <CS:remove>
-        <D:href>mailto:user02 at example.com</D:href>
-    </CS:remove>
-    <CS:remove>
-        <D:href>mailto:user03 at example.com</D:href>
-    </CS:remove>
-</CS:share>
-""")
+            <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+                <CS:remove>
+                    <D:href>mailto:user02 at example.com</D:href>
+                </CS:remove>
+                <CS:remove>
+                    <D:href>mailto:user03 at example.com</D:href>
+                </CS:remove>
+            </CS:share>
+            """)
 
         propInvite = (yield self.resource.readProperty(customxml.Invite, None))
         self.assertEquals(propInvite, None)
@@ -515,46 +597,53 @@
     def test_POSTaddInvalidInvitee(self):
         self.resource.upgradeToShare()
 
-        response = (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:bogus at example.net</D:href>
-        <CS:summary>My Shared Calendar</CS:summary>
-        <CS:read-write/>
-    </CS:set>
-</CS:share>
-""",
+        data = (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:bogus at example.net</D:href>
+                    <CS:summary>My Shared Calendar</CS:summary>
+                    <CS:read-write/>
+                </CS:set>
+            </CS:share>
+            """,
             responsecode.MULTI_STATUS
         ))
-        self.assertEqual(
-            str(response.stream.read()).replace("\r\n", "\n"),
+        self.assertXMLEquals(
+            data,
             """<?xml version='1.0' encoding='UTF-8'?>
-<multistatus xmlns='DAV:'>
-  <response>
-    <href>mailto:bogus at example.net</href>
-    <status>HTTP/1.1 403 Forbidden</status>
-  </response>
-</multistatus>"""
+            <multistatus xmlns='DAV:'>
+              <response>
+                <href>mailto:bogus at example.net</href>
+                <status>HTTP/1.1 403 Forbidden</status>
+              </response>
+            </multistatus>"""
         )
         propInvite = (yield self.resource.readProperty(customxml.Invite, None))
-
         self.assertEquals(propInvite, None)
 
 
+    def assertXMLEquals(self, a, b):
+        """
+        Assert two strings are equivalent as XML.
+        """
+        self.assertEquals(normalize(a), normalize(b))
+
+
     @inlineCallbacks
     def test_POSTremoveInvalidInvitee(self):
 
         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:user01 at example.com</D:href>
-        <CS:summary>My Shared Calendar</CS:summary>
-        <CS:read-write/>
-    </CS:set>
-</CS:share>
-""")
+            <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+                <CS:set>
+                    <D:href>mailto:user01 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))
         self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -583,18 +672,39 @@
         ))
 
         yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
-    <CS:remove>
-        <D:href>mailto:user01 at example.com</D:href>
-    </CS:remove>
-</CS:share>
-""")
+            <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+                <CS:remove>
+                    <D:href>mailto:user01 at example.com</D:href>
+                </CS:remove>
+            </CS:share>
+            """)
 
         propInvite = (yield self.resource.readProperty(customxml.Invite, None))
         self.assertEquals(propInvite, None)
 
 
     @inlineCallbacks
+    def wikiSetup(self):
+        """
+        Create a wiki called C{wiki-testing}, and share it with the user whose
+        home is at /.  Return the name of the newly shared calendar in the
+        sharee's home.
+        """
+        wcreate = self.calendarStore.newTransaction("create wiki")
+        yield wcreate.calendarHomeWithUID("wiki-testing", create=True)
+        yield wcreate.commit()
+        self.directoryFixture.addDirectoryService(WikiDirectoryService())
+
+        txn = self.site.resource._associatedTransaction
+        sharee = self.site.resource._newStoreHome
+        sharer = yield txn.calendarHomeWithUID("wiki-testing")
+        cal = yield sharer.calendarWithName("calendar")
+        sharedName = yield cal.shareWith(sharee, BIND_DIRECT)
+        yield self._refreshRoot()
+        returnValue(sharedName)
+
+
+    @inlineCallbacks
     def test_wikiACL(self):
         """
         Ensure shareeAccessControlList( ) honors the access granted by the wiki
@@ -603,100 +713,54 @@
         """
 
         access = "read"
-
         def stubWikiAccessMethod(userID, wikiID):
             return access
+        self.patch(sharing, "getWikiAccess", stubWikiAccessMethod)
 
-        class StubCollection(object):
-            def __init__(self):
-                self._isShareeCollection = True
-                self._shareePrincipal = StubUserPrincipal()
-            def isCalendarCollection(self):
-                return True
+        sharedName = yield self.wikiSetup()
+        request = SimpleRequest(self.site, "GET", "/404")
+        collection = yield request.locateResource("/" + sharedName)
 
-        class StubShare(object):
-            def direct(self):
-                return True
-
-            def url(self):
-                return "/wikifoo"
-
-            def uid(self):
-                return "012345"
-
-            def shareeUID(self):
-                return StubUserPrincipal().record.guid
-
-        class TestCollection(SharedResourceMixin, StubCollection):
-            def principalForUID(self, uid):
-                principal = StubUserPrincipal()
-                return principal if principal.record.guid == uid else None
-
-        class StubRecord(object):
-            def __init__(self, recordType, name, guid):
-                self.recordType = recordType
-                self.shortNames = [name]
-                self.guid = guid
-
-        class StubUserPrincipal(object):
-            def __init__(self):
-                self.record = StubRecord(
-                    "users",
-                    "testuser",
-                    "4F364813-0415-45CB-9FD4-DBFEF7A0A8E0"
-                )
-            def principalURL(self):
-                return "/principals/__uids__/%s/" % (self.record.guid,)
-
-        class StubWikiPrincipal(object):
-            def __init__(self):
-                self.record = StubRecord(
-                    WikiDirectoryService.recordType_wikis,
-                    "wikifoo",
-                    "foo"
-                )
-
-        class StubWikiResource(object):
-            implements(IResource)
-
-            def locateChild(self, req, segments):
-                pass
-
-
-            def renderHTTP(self, req):
-                pass
-
-
-            def ownerPrincipal(self, req):
-                return succeed(StubWikiPrincipal())
-
-        collection = TestCollection()
-        collection._share = StubShare()
-        self.site.resource.putChild("wikifoo", StubWikiResource())
-        request = SimpleRequest(self.site, "GET", "/wikifoo")
-
         # Simulate the wiki server granting Read access
-        acl = (yield collection.shareeAccessControlList(request,
-            wikiAccessMethod=stubWikiAccessMethod))
+        acl = (yield collection.shareeAccessControlList(request))
         self.assertFalse("<write/>" in acl.toxml())
 
         # Simulate the wiki server granting Read-Write access
         access = "write"
-        acl = (yield collection.shareeAccessControlList(request,
-            wikiAccessMethod=stubWikiAccessMethod))
+        acl = (yield collection.shareeAccessControlList(request))
         self.assertTrue("<write/>" in acl.toxml())
 
 
-'''
-class DatabaseSharingTests(SharingTests):
-
     @inlineCallbacks
-    def setUp(self):
-        self.calendarStore = yield buildStore(self, StubNotifierFactory())
-        yield super(DatabaseSharingTests, self).setUp()
+    def test_noWikiAccess(self):
+        """
+        If L{SharedCollectionMixin.shareeAccessControlList} detects missing
+        access controls for a directly shared collection, it will automatically
+        un-share that collection.
+        """
+        sharedName = yield self.wikiSetup()
+        access = "write"
+        def stubWikiAccessMethod(userID, wikiID):
+            return access
+        self.patch(sharing, "getWikiAccess", stubWikiAccessMethod)
+        @inlineCallbacks
+        def listChildrenViaPropfind():
+            data = yield self.simpleSend(
+                "PROPFIND", "/", resultcode=responsecode.MULTI_STATUS,
+                headers=[('Depth', '1')]
+            )
+            tree = XML(data)
+            seq = [e.text for e in tree.findall("{DAV:}response/{DAV:}href")]
+            shortest = min(seq, key=len)
+            seq.remove(shortest)
+            filtered = [elem[len(shortest):].rstrip("/") for elem in seq]
+            returnValue(filtered)
+        childNames = yield listChildrenViaPropfind()
+        self.assertIn(sharedName, childNames)
+        access = "no-access"
+        childNames = yield listChildrenViaPropfind()
+        self.assertNotIn(sharedName, childNames)
 
 
-    def createDataStore(self):
-        return self.calendarStore
 
-'''
+

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_wrapping.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_wrapping.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_wrapping.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -32,7 +32,7 @@
 from twistedcaldav.ical import Component as VComponent
 from twistedcaldav.vcard import Component as VCComponent
 
-from twistedcaldav.storebridge import DropboxCollection,\
+from twistedcaldav.storebridge import DropboxCollection, \
     CalendarCollectionResource, AddressBookCollectionResource
 
 from twistedcaldav.test.util import TestCase
@@ -42,7 +42,7 @@
 
 from txdav.carddav.datastore.test.test_file import vcard4_text
 
-from txdav.common.datastore.test.util import buildStore, assertProvides,\
+from txdav.common.datastore.test.util import buildStore, assertProvides, \
     StubNotifierFactory
 
 
@@ -65,18 +65,32 @@
     def writeHeaders(self, code, headers):
         self.code = code
         self.headers = headers
+
+
     def registerProducer(self, producer, streaming):
         pass
+
+
     def write(self, data):
         pass
+
+
     def unregisterProducer(self):
         pass
+
+
     def abortConnection(self):
         pass
+
+
     def getHostInfo(self):
         return '127.0.0.1', False
+
+
     def getRemoteHost(self):
         return '127.0.0.1'
+
+
     def finish(self):
         pass
 
@@ -160,7 +174,6 @@
         )
         yield txn.commit()
 
-
     requestUnderTest = None
 
     @inlineCallbacks
@@ -232,10 +245,18 @@
         req.credentialFactories = {}
         return req
 
-
     pathTypes = ['calendar', 'addressbook']
 
 
+    def checkPrincipalCollections(self, resource):
+        """
+        Verify that the C{_principalCollections} attribute of the given
+        L{Resource} is accurately set.
+        """
+        self.assertEquals(resource._principalCollections,
+                          frozenset([self.directoryFixture.principalsResource]))
+
+
     @inlineCallbacks
     def test_autoRevertUnCommitted(self):
         """
@@ -392,8 +413,7 @@
             "calendars/users/wsanchez/calendar/1.ics"
         )
         yield self.commit()
-        self.assertEquals(calDavFileCalendar._principalCollections,
-                          frozenset([self.principalsResource]))
+        self.checkPrincipalCollections(calDavFileCalendar)
         self.assertEquals(calDavFileCalendar._associatedTransaction,
                           calendarHome._associatedTransaction)
 
@@ -404,6 +424,9 @@
         Exceeding quota on an attachment returns an HTTP error code.
         """
         self.patch(config, "EnableDropBox", True)
+        if not hasattr(self.calendarCollection._newStore, "_dropbox_ok"):
+            self.calendarCollection._newStore._dropbox_ok = False
+        self.patch(self.calendarCollection._newStore, "_dropbox_ok", True)
         self.patch(Calendar, "asShared", lambda self: [])
 
         yield self.populateOneObject("1.ics", test_event_text)
@@ -435,8 +458,7 @@
             "calendars/users/wsanchez/calendar/xyzzy.ics"
         )
         yield self.commit()
-        self.assertEquals(calDavFileCalendar._principalCollections,
-                          frozenset([self.principalsResource]))
+        self.checkPrincipalCollections(calDavFileCalendar)
 
 
     def test_createAddressBookStore(self):
@@ -468,8 +490,7 @@
         """
         calDavFile = yield self.getResource("addressbooks/users/wsanchez/addressbook")
         yield self.commit()
-        self.assertEquals(calDavFile._principalCollections,
-                          frozenset([self.principalsResource]))
+        self.checkPrincipalCollections(calDavFile)
 
 
     @inlineCallbacks
@@ -487,8 +508,7 @@
         yield calDavFile.createAddressBookCollection()
         self.assertTrue(calDavFile.exists())
         yield self.commit()
-        self.assertEquals(calDavFile._principalCollections,
-                          frozenset([self.principalsResource]))
+        self.checkPrincipalCollections(calDavFile)
 
 
     @inlineCallbacks
@@ -503,8 +523,7 @@
             "addressbooks/users/wsanchez/addressbook/1.vcf"
         )
         yield self.commit()
-        self.assertEquals(calDavFileAddressBook._principalCollections,
-                          frozenset([self.principalsResource]))
+        self.checkPrincipalCollections(calDavFileAddressBook)
 
 
     @inlineCallbacks
@@ -518,8 +537,7 @@
             "addressbooks/users/wsanchez/addressbook/xyzzy.ics"
         )
         yield self.commit()
-        self.assertEquals(calDavFileAddressBook._principalCollections,
-                          frozenset([self.principalsResource]))
+        self.checkPrincipalCollections(calDavFileAddressBook)
 
 
     @inlineCallbacks
@@ -568,7 +586,7 @@
         # see twistedcaldav/directory/test/accounts.xml
         wsanchez = '6423F94A-6B76-4A3A-815B-D52CFD77935D'
         cdaboo = '5A985493-EE2C-4665-94CF-4DFEA3A89500'
-        eventTemplate="""\
+        eventTemplate = """\
 BEGIN:VCALENDAR
 CALSCALE:GREGORIAN
 PRODID:-//Example Inc.//Example Calendar//EN
@@ -612,9 +630,7 @@
 END:VEVENT""".format(wsanchez=wsanchez, cdaboo=cdaboo)
         #txn = self.requestUnderTest._newStoreTransaction
         invalidEvent = eventTemplate.format(invalidInstance, wsanchez=wsanchez, cdaboo=cdaboo).replace(CR, CRLF)
-        resp2, rsrc2 = yield putEvt(invalidEvent)
+        yield putEvt(invalidEvent)
         self.requestUnderTest = None
         yield self.assertCalendarEmpty(wsanchez)
         yield self.assertCalendarEmpty(cdaboo)
-
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/util.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/util.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -37,9 +37,12 @@
 from twistedcaldav.config import config
 from twistedcaldav.directory import augment
 from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
-from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
+from twistedcaldav.directory.calendar import (
+    DirectoryCalendarHomeProvisioningResource
+)
 from twistedcaldav.directory.principal import (
     DirectoryPrincipalProvisioningResource)
+from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 
 from txdav.common.datastore.test.util import deriveQuota
@@ -74,33 +77,81 @@
 
 
 
-class TestCase(twext.web2.dav.test.util.TestCase):
-    resource_class = RootResource
+class DirectoryFixture(object):
+    """
+    Test fixture for creating various parts of the resource hierarchy related
+    to directories.
+    """
 
-    def createStockDirectoryService(self):
+    def __init__(self):
+        def _setUpPrincipals(ds):
+            # FIXME: see FIXME in
+            # DirectoryPrincipalProvisioningResource.__init__; this performs a
+            # necessary modification to any directory service object for it to
+            # be fully functional.
+            self.principalsResource = DirectoryPrincipalProvisioningResource(
+                "/principals/", ds
+            )
+        self._directoryChangeHooks = [_setUpPrincipals]
+
+
+    directoryService = None
+    principalsResource = None
+
+    def addDirectoryService(self, newService):
         """
-        Create a stock C{directoryService} attribute and assign it.
+        Add an L{IDirectoryService} to this test case.
+
+        If this test case does not have a directory service yet, create it and
+        assign C{directoryService} and C{principalsResource} attributes to this
+        test case.
+
+        If the test case already has a directory service, create an
+        L{AggregateDirectoryService} and re-assign the C{self.directoryService}
+        attribute to point at it instead, while setting the C{realmName} of the
+        new service to match the old one.
+
+        If the test already has an L{AggregateDirectoryService}, create a
+        I{new} L{AggregateDirectoryService} with the same list of services,
+        after adjusting the new service's realm to match the existing ones.
         """
-        self.xmlFile = FilePath(config.DataRoot).child("accounts.xml")
-        self.xmlFile.setContent(xmlFile.getContent())
 
+        if self.directoryService is None:
+            directoryService = newService
+        else:
+            newService.realmName = self.directoryService.realmName
+            if isinstance(self.directoryService, AggregateDirectoryService):
+                directories = set(self.directoryService._recordTypes.items())
+                directories.add(newService)
+            else:
+                directories = [newService, self.directoryService]
+            directoryService = AggregateDirectoryService(directories, None)
 
-        self.directoryService = XMLDirectoryService(
-            {
-                "xmlFile" : "accounts.xml",
-                "augmentService" :
-                    augment.AugmentXMLDB( xmlFiles=(augmentsFile.path,)),
-            }
-        )
-
+        self.directoryService = directoryService
         # FIXME: see FIXME in DirectoryPrincipalProvisioningResource.__init__;
-        # this performs a necessary modification to the directory service
-        # object for it to be fully functional.
-        self.principalsResource = DirectoryPrincipalProvisioningResource(
-            "/principals/", self.directoryService
-        )
+        # this performs a necessary modification to the directory service object
+        # for it to be fully functional.
+        for hook in self._directoryChangeHooks:
+            hook(directoryService)
 
 
+    def whenDirectoryServiceChanges(self, callback):
+        """
+        When the C{directoryService} attribute is changed by
+        L{TestCase.addDirectoryService}, call the given callback in order to
+        update any state which relies upon that service.
+
+        If there's already a directory, invoke the callback immediately.
+        """
+        self._directoryChangeHooks.append(callback)
+        if self.directoryService is not None:
+            callback(self.directoryService)
+
+
+
+class TestCase(twext.web2.dav.test.util.TestCase):
+    resource_class = RootResource
+
     def createDataStore(self):
         """
         Create an L{IDataStore} that can store calendars (but not
@@ -111,45 +162,88 @@
                                quota=deriveQuota(self))
 
 
+    def createStockDirectoryService(self):
+        """
+        Create a stock C{directoryService} attribute and assign it.
+        """
+        self.xmlFile = FilePath(config.DataRoot).child("accounts.xml")
+        self.xmlFile.setContent(xmlFile.getContent())
+        self.directoryFixture.addDirectoryService(XMLDirectoryService({
+            "xmlFile": "accounts.xml",
+            "augmentService":
+                augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
+        }))
+
+
     def setupCalendars(self):
         """
-        Set up the resource at /calendars (a
-        L{DirectoryCalendarHomeProvisioningResource}), and assign it as
-        C{self.calendarCollection}.
+        When a directory service exists, set up the resources at C{/calendars}
+        and C{/addressbooks} (a L{DirectoryCalendarHomeProvisioningResource}
+        and L{DirectoryAddressBookHomeProvisioningResource} respectively), and
+        assign them to the C{self.calendarCollection} and
+        C{self.addressbookCollection} attributes.
+
+        A directory service may be associated with this L{TestCase} with
+        L{TestCase.createStockDirectoryService} or
+        L{TestCase.directoryFixture.addDirectoryService}.
         """
+        newStore = self.createDataStore()
+        @self.directoryFixture.whenDirectoryServiceChanges
+        def putAllChildren(ds):
+            self.calendarCollection = (
+                DirectoryCalendarHomeProvisioningResource(
+                    ds, "/calendars/", newStore
+                ))
+            self.site.resource.putChild("calendars", self.calendarCollection)
+            self.addressbookCollection = (
+                DirectoryAddressBookHomeProvisioningResource(
+                    ds, "/addressbooks/", newStore
+                ))
+            self.site.resource.putChild("addressbooks",
+                                        self.addressbookCollection)
 
-        # Need a data store
-        self._newStore = self.createDataStore()
 
-        self.calendarCollection = DirectoryCalendarHomeProvisioningResource(
-            self.directoryService,
-            "/calendars/",
-            self._newStore
-        )
-        self.site.resource.putChild("calendars", self.calendarCollection)
+    def configure(self):
+        """
+        Adjust the global configuration for this test.
+        """
+        config.reset()
 
-        self.addressbookCollection = DirectoryAddressBookHomeProvisioningResource(
-            self.directoryService,
-            "/addressbooks/",
-            self._newStore
-        )
-        self.site.resource.putChild("addressbooks", self.addressbookCollection)
+        config.ServerRoot = os.path.abspath(self.serverRoot)
+        config.ConfigRoot = "config"
+        config.LogRoot = "logs"
+        config.RunRoot = "logs"
 
+        config.Memcached.Pools.Default.ClientEnabled = False
+        config.Memcached.Pools.Default.ServerEnabled = False
+        ClientFactory.allowTestCache = True
+        memcacher.Memcacher.allowTestCache = True
+        memcacher.Memcacher.memoryCacheInstance = None
+        config.DirectoryAddressBook.Enabled = False
 
+
+    @property
+    def directoryService(self):
+        """
+        Read-only alias for L{DirectoryFixture.directoryService} for
+        compatibility with older tests.  TODO: remove this.
+        """
+        return self.directoryFixture.directoryService
+
+
     def setUp(self):
         super(TestCase, self).setUp()
 
+        self.directoryFixture = DirectoryFixture()
+
         # FIXME: this is only here to workaround circular imports
         doBind()
 
-        config.reset()
-        serverroot = self.mktemp()
-        os.mkdir(serverroot)
-        config.ServerRoot = os.path.abspath(serverroot)
-        config.ConfigRoot = "config"
-        config.LogRoot = "logs"
-        config.RunRoot = "logs"
+        self.serverRoot = self.mktemp()
+        os.mkdir(self.serverRoot)
 
+        self.configure()
+
         if not os.path.exists(config.DataRoot):
             os.makedirs(config.DataRoot)
         if not os.path.exists(config.DocumentRoot):
@@ -159,15 +253,7 @@
         if not os.path.exists(config.LogRoot):
             os.makedirs(config.LogRoot)
 
-        config.Memcached.Pools.Default.ClientEnabled = False
-        config.Memcached.Pools.Default.ServerEnabled = False
-        ClientFactory.allowTestCache = True
-        memcacher.Memcacher.allowTestCache = True
-        memcacher.Memcacher.memoryCacheInstance = None
 
-        config.DirectoryAddressBook.Enabled = False
-
-
     def createHierarchy(self, structure, root=None):
         if root is None:
             root = os.path.abspath(self.mktemp())
@@ -184,8 +270,6 @@
                     # This is a file
                     with open(childPath, "w") as child:
                         child.write(childStructure["@contents"])
-
-
                 else:
                     # This is a directory
                     os.mkdir(childPath)
@@ -207,6 +291,7 @@
         createChildren(root, structure)
         return root
 
+
     def verifyHierarchy(self, root, structure):
 
         def verifyChildren(parent, subStructure):
@@ -341,21 +426,18 @@
     def setUp(self):
         """
         Replace self.site.resource with an appropriately provisioned
-        CalendarHomeResource, and replace self.docroot with a path pointing at that
-        file.
+        L{CalendarHomeResource}, and, if the data store backing this test is a
+        file store, replace C{self.docroot} with a path pointing at the path
+        that stores the data for that L{CalendarHomeResource}.
         """
         super(HomeTestCase, self).setUp()
-
         self.createStockDirectoryService()
+        @self.directoryFixture.whenDirectoryServiceChanges
+        def addHomeProvisioner(ds):
+            self.homeProvisioner = DirectoryCalendarHomeProvisioningResource(
+                ds, "/calendars/", self.createDataStore()
+            )
 
-        # Need a data store
-        _newStore = self.createDataStore()
-
-        self.homeProvisioner = DirectoryCalendarHomeProvisioningResource(
-            self.directoryService, "/calendars/",
-            _newStore
-        )
-        
         def _defer(user):
             # Commit the transaction
             self.addCleanup(self.noRenderCommit)
@@ -434,20 +516,13 @@
         file.
         """
         super(AddressBookHomeTestCase, self).setUp()
-
-        fp = FilePath(self.mktemp())
-        fp.createDirectory()
-
         self.createStockDirectoryService()
+        @self.directoryFixture.whenDirectoryServiceChanges
+        def addHomeProvisioner(ds):
+            self.homeProvisioner = DirectoryAddressBookHomeProvisioningResource(
+                ds, "/calendars/", self.createDataStore()
+            )
 
-        # Need a data store
-        _newStore = CommonDataStore(fp, None, True, False)
-
-        self.homeProvisioner = DirectoryAddressBookHomeProvisioningResource(
-            self.directoryService, "/addressbooks/",
-            _newStore
-        )
-
         @inlineCallbacks
         def _defer(user):
             # Commit the transaction
@@ -456,6 +531,7 @@
 
         return self._refreshRoot().addCallback(_defer)
 
+
     @inlineCallbacks
     def _refreshRoot(self):
         """

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/upgrade.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/upgrade.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/upgrade.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -66,7 +66,9 @@
 from calendarserver.tools.util import getDirectory
 
 from twext.python.parallel import Parallelizer
+from twistedcaldav.scheduling.imip.mailgateway import migrateTokensToStore
 
+
 deadPropertyXattrPrefix = namedAny(
     "txdav.base.propertystore.xattr.PropertyStore.deadPropertyXattrPrefix"
 )
@@ -1048,6 +1050,8 @@
         Start the service.
         """
 
+        directory = directoryFromConfig(self.config)
+
         # Load proxy assignments from XML if specified
         if self.config.ProxyLoadFromFile:
             proxydbClass = namedClass(self.config.ProxyDBService.type)
@@ -1063,7 +1067,6 @@
             if proxydb is None:
                 proxydbClass = namedClass(self.config.ProxyDBService.type)
                 proxydb = proxydbClass(**self.config.ProxyDBService.params)
-            directory = directoryFromConfig(self.config)
 
             updater = GroupMembershipCacheUpdater(proxydb,
                 directory, self.config.GroupCaching.ExpireSeconds,
@@ -1082,7 +1085,11 @@
         yield self.processInboxItems()
         self.store.setMigrating(False)
 
+        # Migrate mail tokens from sqlite to store
+        yield migrateTokensToStore(self.config.DataRoot, self.store)
 
+
+
     @inlineCallbacks
     def processInboxItems(self):
         """

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/base/propertystore/appledouble_xattr.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/base/propertystore/appledouble_xattr.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/base/propertystore/appledouble_xattr.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -25,32 +25,32 @@
 # http://www.opensource.apple.com/source/Libc/Libc-391/darwin/copyfile.c
 
 # File header format: magic, version, unused, number of entries
-AS_HEADER_FORMAT=">LL16sh"
-AS_HEADER_LENGTH=26
+AS_HEADER_FORMAT = ">LL16sh"
+AS_HEADER_LENGTH = 26
 
 # The flag words for AppleDouble
-AS_MAGIC=0x00051607
-AS_VERSION=0x00020000
+AS_MAGIC = 0x00051607
+AS_VERSION = 0x00020000
 
 # Entry header format: id, offset, length
-AS_ENTRY_FORMAT=">lll"
-AS_ENTRY_LENGTH=12
+AS_ENTRY_FORMAT = ">lll"
+AS_ENTRY_LENGTH = 12
 
 # The id values
-AS_DATAFORK=1
-AS_RESOURCEFORK=2
-AS_REALNAME=3
-AS_COMMENT=4
-AS_ICONBW=5
-AS_ICONCOLOR=6
-AS_DATESINFO=8
-AS_FINDERINFO=9
-AS_MACFILEINFO=10
-AS_PRODOSFILEINFO=11
-AS_MSDOSFILEINFO=12
-AS_SHORTNAME=13
-AS_AFPFILEINFO=14
-AS_DIECTORYID=15
+AS_DATAFORK = 1
+AS_RESOURCEFORK = 2
+AS_REALNAME = 3
+AS_COMMENT = 4
+AS_ICONBW = 5
+AS_ICONCOLOR = 6
+AS_DATESINFO = 8
+AS_FINDERINFO = 9
+AS_MACFILEINFO = 10
+AS_PRODOSFILEINFO = 11
+AS_MSDOSFILEINFO = 12
+AS_SHORTNAME = 13
+AS_AFPFILEINFO = 14
+AS_DIECTORYID = 15
 
 FINDER_INFO_LENGTH = 32
 XATTR_OFFSET = FINDER_INFO_LENGTH + 2
@@ -93,7 +93,7 @@
         raise ValueError("AppleDouble file contains no forks")
 
     # Get each entry
-    headers = [fileobj.read(AS_ENTRY_LENGTH) for _ignore in xrange(nentry)]
+    headers = [fileobj.read(AS_ENTRY_LENGTH) for _ignore_count in xrange(nentry)]
     for hdr in headers:
         try:
             restype, offset, length = struct.unpack(AS_ENTRY_FORMAT, hdr)
@@ -109,9 +109,9 @@
             # Get the xattr header
             fileobj.seek(offset + XATTR_OFFSET)
             data = fileobj.read(length - XATTR_OFFSET)
-            if len(data) != length-XATTR_OFFSET:
+            if len(data) != length - XATTR_OFFSET:
                 raise ValueError("Short read: expected %d bytes got %d" %
-                                 (length-XATTR_OFFSET, len(data)))
+                                 (length - XATTR_OFFSET, len(data)))
             magic, _ignore_tag, total_size, data_start, data_length, \
             _ignore_reserved1, _ignore_reserved2, _ignore_reserved3, \
             flags, num_attrs = struct.unpack(XATTR_HEADER,
@@ -138,7 +138,7 @@
                 xattr_name = data[
                     XATTR_ENTRY_LENGTH:
                     XATTR_ENTRY_LENGTH + xattr_name_len
-                    -1 # strip NULL terminator
+                    - 1 # strip NULL terminator
                 ]
                 fileobj.seek(xattr_offset)
                 xattr_value = fileobj.read(xattr_length)
@@ -184,9 +184,6 @@
     @property
     def attrs(self):
         try:
-            return attrsFromFile(self.path.sibling("._"+self.path.basename()).open())
+            return attrsFromFile(self.path.sibling("._" + self.path.basename()).open())
         except IOError:
             return {}
-
-
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/sql.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/sql.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -62,7 +62,7 @@
     dropboxIDFromCalendarObject
 from txdav.caldav.icalendarstore import ICalendarHome, ICalendar, ICalendarObject, \
     IAttachment, AttachmentStoreFailed, AttachmentStoreValidManagedID, \
-    AttachmentMigrationFailed
+    AttachmentMigrationFailed, AttachmentDropboxNotAllowed
 from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, ECALENDARTYPE
@@ -90,6 +90,7 @@
 import collections
 import os
 import tempfile
+import urllib
 import uuid
 
 log = Logger()
@@ -170,7 +171,8 @@
             total = rows[0][0]
             count = 0
             log.warn("%d dropbox ids to migrate" % (total,))
-        except RuntimeError:
+        except RuntimeError, e:
+            log.error("Dropbox migration failed when cleaning out dropbox ids: %s" % (e,))
             yield txn.abort()
             raise
         else:
@@ -390,7 +392,7 @@
             Otherwise, (if this is the string "calendar") we are checking for
             conflicts with a new unscheduled calendar object, which will
             conflict only with other scheduled objects.
-        @type type: C{str}
+        @type mode: C{str}
 
         @return: a L{Deferred} which fires with C{True} if there is a conflict
             and C{False} if not.
@@ -1568,6 +1570,8 @@
         for managed_id in added:
             changed[managed_id] = newattached[managed_id]
 
+        if self._dropboxID is None:
+            self._dropboxID = str(uuid.uuid4())
         changes = yield self._addingManagedIDs(self._txn, self._parentCollection, self._dropboxID, changed, component.resourceUID())
 
         # Make sure existing data is not changed
@@ -1643,7 +1647,7 @@
         new_attachment = (yield ManagedAttachment.load(txn, managed_id))
         if new_id:
             new_attachment._managedID = new_id
-            new_attachment._objectDropboxID = dropbox_id
+        new_attachment._objectDropboxID = dropbox_id
         for attachment in attachments:
             yield new_attachment.updateProperty(attachment)
 
@@ -1807,7 +1811,10 @@
             attachments = component.properties("ATTACH")
             removed = False
             for attachment in tuple(attachments):
-                if attachment.value().endswith("/dropbox/%s/%s" % (oldattachment.dropboxID(), oldattachment.name(),)):
+                if attachment.value().endswith("/dropbox/%s/%s" % (
+                    urllib.quote(oldattachment.dropboxID()),
+                    urllib.quote(oldattachment.name()),
+                )):
                     component.removeProperty(attachment)
                     removed = True
             if removed:
@@ -2157,6 +2164,8 @@
 
 
     def store(self, contentType, dispositionName=None):
+        if not self._name:
+            self._name = dispositionName
         return AttachmentStorageTransport(self, contentType, dispositionName, self._justCreated)
 
 
@@ -2301,6 +2310,11 @@
         @type ownerHomeID: C{int}
         """
 
+        # If store has already migrated to managed attachments we will prevent creation of dropbox attachments
+        dropbox = (yield txn.store().dropboxAllowed(txn))
+        if not dropbox:
+            raise AttachmentDropboxNotAllowed
+
         # Now create the DB entry
         att = schema.ATTACHMENT
         rows = (yield Insert({
@@ -2719,8 +2733,8 @@
         fname = self.lastSegmentOfUriPath(self._managedID, self._name)
         location = self._txn._store.attachmentsURIPattern % {
             "home": self._ownerName,
-            "dropbox_id": self._objectDropboxID,
-            "name": fname,
+            "dropbox_id": urllib.quote(self._objectDropboxID),
+            "name": urllib.quote(fname),
         }
         returnValue(location)
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/common.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/common.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -20,7 +20,6 @@
 """
 
 from StringIO import StringIO
-import os
 
 from twisted.internet.defer import Deferred, inlineCallbacks, returnValue, \
     maybeDeferred
@@ -31,7 +30,6 @@
 from twext.python.vcomponent import VComponent
 from twext.python.filepath import CachingFilePath as FilePath
 from twext.enterprise.ienterprise import AlreadyFinishedError
-from twext.web2.http_headers import MimeType
 
 from txdav.xml.element import WebDAVUnknownElement, ResourceType
 from txdav.idav import IPropertyStore, IDataStore
@@ -48,14 +46,10 @@
 
 from txdav.caldav.icalendarstore import (
     ICalendarObject, ICalendarHome,
-    ICalendar, IAttachment, ICalendarTransaction)
+    ICalendar, ICalendarTransaction)
 
 from twistedcaldav.customxml import InviteNotification, InviteSummary
-from txdav.caldav.icalendarstore import IAttachmentStorageTransport
-from txdav.caldav.icalendarstore import QuotaExceeded
-from txdav.common.datastore.test.util import (
-    deriveQuota, withSpecialQuota, transactionClean
-)
+from txdav.common.datastore.test.util import transactionClean
 from txdav.common.icommondatastore import ConcurrentModification
 from twistedcaldav.ical import Component
 from twistedcaldav.config import config
@@ -314,48 +308,6 @@
     }
 
 
-    def storeUnderTest(self):
-        """
-        Subclasses must override this to return an L{ICommonDataStore} provider
-        which adheres to the structure detailed by L{CommonTests.requirements}.
-        This attribute is a dict of dict of dicts; the outermost layer
-        representing UIDs mapping to calendar homes, then calendar names mapping
-        to calendar collections, and finally calendar object names mapping to
-        calendar object text.
-        """
-        raise NotImplementedError()
-
-
-    @inlineCallbacks
-    def homeUnderTest(self, txn=None, name="home1"):
-        """
-        Get the calendar home detailed by C{requirements['home1']}.
-        """
-        if txn is None:
-            txn = self.transactionUnderTest()
-        returnValue((yield txn.calendarHomeWithUID(name)))
-
-
-    @inlineCallbacks
-    def calendarUnderTest(self, txn=None, name="calendar_1", home="home1"):
-        """
-        Get the calendar detailed by C{requirements['home1']['calendar_1']}.
-        """
-        returnValue((yield
-            (yield self.homeUnderTest(txn, home)).calendarWithName(name))
-        )
-
-
-    @inlineCallbacks
-    def calendarObjectUnderTest(self, name="1.ics", txn=None):
-        """
-        Get the calendar detailed by
-        C{requirements['home1']['calendar_1'][name]}.
-        """
-        returnValue((yield (yield self.calendarUnderTest(txn))
-                     .calendarObjectWithName(name)))
-
-
     def test_calendarStoreProvides(self):
         """
         The calendar store provides L{IDataStore} and its required attributes.
@@ -493,8 +445,8 @@
         # Make sure notification fired after commit
         self.assertEquals(self.notifierFactory.history,
             [
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/notification"),
+                "CalDAV|home1",
+                "CalDAV|home1/notification",
             ]
         )
 
@@ -511,8 +463,8 @@
         # Make sure notification fired after commit
         self.assertEquals(self.notifierFactory.history,
             [
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/notification"),
+                "CalDAV|home1",
+                "CalDAV|home1/notification",
             ]
         )
 
@@ -584,16 +536,7 @@
         self.assertEquals(name, "/CalDAV/example.com/home1/")
 
 
-    @inlineCallbacks
-    def test_nodeNameFailure(self):
-        # The StubNodeCacher is set up to fail when the node name has the
-        # word "fail" in it, for testing the failure mode:
-        home = yield self.transactionUnderTest().calendarHomeWithUID("fail",
-            create=True)
-        name = yield home.nodeName()
-        self.assertEquals(name, None)
 
-
     @inlineCallbacks
     def test_displayNameNone(self):
         """
@@ -764,7 +707,7 @@
         yield self.commit()
 
         # Make sure notification fired after commit
-        self.assertTrue(("update", "CalDAV|home1") in self.notifierFactory.history)
+        self.assertTrue("CalDAV|home1" in self.notifierFactory.history)
 
         # Make sure it's available in a new transaction; i.e. test the commit.
         home = yield self.homeUnderTest()
@@ -813,12 +756,12 @@
         self.assertEquals(
             self.notifierFactory.history,
             [
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/calendar_1"),
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/calendar_2"),
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/calendar_empty"),
+                "CalDAV|home1",
+                "CalDAV|home1/calendar_1",
+                "CalDAV|home1",
+                "CalDAV|home1/calendar_2",
+                "CalDAV|home1",
+                "CalDAV|home1/calendar_empty",
             ]
         )
 
@@ -991,8 +934,8 @@
         self.assertEquals(
             self.notifierFactory.history,
             [
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/calendar_1"),
+                "CalDAV|home1",
+                "CalDAV|home1/calendar_1",
             ]
         )
 
@@ -1040,7 +983,7 @@
         """
         cal = yield self.calendarUnderTest()
         other = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        newCalName = (yield cal.shareWith(other, _BIND_MODE_WRITE)).name()
+        newCalName = yield cal.shareWith(other, _BIND_MODE_WRITE)
         self.sharedName = newCalName
         yield self.commit()
         normalCal = yield self.calendarUnderTest()
@@ -1065,7 +1008,7 @@
         # yield self.commit() # txn is none? why?
         cal = yield self.calendarUnderTest()
         other = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        newName = (yield cal.shareWith(other, _BIND_MODE_READ)).name()
+        newName = yield cal.shareWith(other, _BIND_MODE_READ)
         otherCal = yield other.childWithName(self.sharedName)
 
         # Name should not change just because we updated the mode.
@@ -1543,8 +1486,8 @@
         self.assertEquals(
             self.notifierFactory.history,
             [
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/calendar_1"),
+                "CalDAV|home1",
+                "CalDAV|home1/calendar_1",
             ]
         )
 
@@ -1660,8 +1603,8 @@
         self.assertEquals(
             self.notifierFactory.history,
             [
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/calendar_1"),
+                "CalDAV|home1",
+                "CalDAV|home1/calendar_1",
             ]
         )
 
@@ -1756,60 +1699,7 @@
                 propertyContent
             )
 
-    eventWithDropbox = "\r\n".join("""
-BEGIN:VCALENDAR
-CALSCALE:GREGORIAN
-PRODID:-//Example Inc.//Example Calendar//EN
-VERSION:2.0
-BEGIN:VTIMEZONE
-LAST-MODIFIED:20040110T032845Z
-TZID:US/Eastern
-BEGIN:DAYLIGHT
-DTSTART:20000404T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20001026T020000
-RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-DTSTAMP:20051222T205953Z
-CREATED:20060101T150000Z
-DTSTART;TZID=US/Eastern:20060101T100000
-DURATION:PT1H
-SUMMARY:event 1
-UID:event1 at ninevah.local
-ORGANIZER:user01
-ATTENDEE;PARTSTAT=ACCEPTED:user01
-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
-    """.strip().split("\n"))
 
-
-    @inlineCallbacks
-    def test_dropboxID(self):
-        """
-        L{ICalendarObject.dropboxID} should synthesize its dropbox from the X
-        -APPLE-DROPBOX property, if available.
-        """
-        cal = yield self.calendarUnderTest()
-        yield cal.createCalendarObjectWithName("drop.ics", VComponent.fromString(
-                self.eventWithDropbox
-            )
-        )
-        obj = yield cal.calendarObjectWithName("drop.ics")
-        self.assertEquals((yield obj.dropboxID()), "some-dropbox-id")
-
-
     def token2revision(self, token):
         """
         FIXME: the API names for L{syncToken}() and L{resourceNamesSinceToken}()
@@ -1835,7 +1725,7 @@
         cal = yield self.calendarUnderTest()
         st = yield home.syncToken()
         yield cal.createCalendarObjectWithName("new.ics", VComponent.fromString(
-                self.eventWithDropbox
+                test_event_text
             )
         )
 
@@ -1870,7 +1760,7 @@
         st = yield cal.syncToken()
         rev = self.token2revision(st)
         yield cal.createCalendarObjectWithName("new.ics", VComponent.fromString(
-                self.eventWithDropbox
+                test_event_text
             )
         )
         yield cal.removeCalendarObjectWithName("2.ics")
@@ -1885,354 +1775,6 @@
 
 
     @inlineCallbacks
-    def test_dropboxIDs(self):
-        """
-        L{ICalendarObject.getAllDropboxIDs} returns a L{Deferred} that fires
-        with a C{list} of all Dropbox IDs.
-        """
-        home = yield self.homeUnderTest()
-        # The only item in the home which has an ATTACH or X-APPLE-DROPBOX
-        # property.
-        allDropboxIDs = set([
-            u'FE5CDC6F-7776-4607-83A9-B90FF7ACC8D0.dropbox',
-        ])
-        self.assertEquals(set((yield home.getAllDropboxIDs())),
-                          allDropboxIDs)
-
-
-    @inlineCallbacks
-    def test_indexByDropboxProperty(self):
-        """
-        L{ICalendarHome.calendarObjectWithDropboxID} will return a calendar
-        object in the calendar home with the given final segment in its C{X
-        -APPLE-DROPBOX} property URI.
-        """
-        objName = "with-dropbox.ics"
-        cal = yield self.calendarUnderTest()
-        yield cal.createCalendarObjectWithName(
-            objName, VComponent.fromString(
-                self.eventWithDropbox
-            )
-        )
-        yield self.commit()
-        home = yield self.homeUnderTest()
-        cal = yield self.calendarUnderTest()
-        fromName = yield cal.calendarObjectWithName(objName)
-        fromDropbox = yield home.calendarObjectWithDropboxID("some-dropbox-id")
-        self.assertEquals(fromName, fromDropbox)
-
-
-    @inlineCallbacks
-    def createAttachmentTest(self, refresh):
-        """
-        Common logic for attachment-creation tests.
-        """
-        obj = yield self.calendarObjectUnderTest()
-        attachment = yield obj.createAttachmentWithName(
-            "new.attachment",
-        )
-        t = attachment.store(MimeType("text", "x-fixture"), "")
-        self.assertProvides(IAttachmentStorageTransport, t)
-        t.write("new attachment")
-        t.write(" text")
-        yield t.loseConnection()
-        obj = yield refresh(obj)
-        attachment = yield obj.attachmentWithName("new.attachment")
-        self.assertProvides(IAttachment, attachment)
-        data = yield self.attachmentToString(attachment)
-        self.assertEquals(data, "new attachment text")
-        contentType = attachment.contentType()
-        self.assertIsInstance(contentType, MimeType)
-        self.assertEquals(contentType, MimeType("text", "x-fixture"))
-        self.assertEquals(attachment.md5(), '50a9f27aeed9247a0833f30a631f1858')
-        self.assertEquals(
-            [attachment.name() for attachment in (yield obj.attachments())],
-            ['new.attachment']
-        )
-
-
-    @inlineCallbacks
-    def test_twoAttachmentsWithTheSameName(self):
-        """
-        Attachments are uniquely identified by their associated object and path;
-        two attachments with the same name won't overwrite each other.
-        """
-        obj = yield self.calendarObjectUnderTest()
-        obj2 = yield self.calendarObjectUnderTest("2.ics")
-        att1 = yield self.stringToAttachment(obj, "sample.attachment",
-                                             "test data 1")
-        att2 = yield self.stringToAttachment(obj2, "sample.attachment",
-                                             "test data 2")
-        data1 = yield self.attachmentToString(att1)
-        data2 = yield self.attachmentToString(att2)
-        self.assertEquals(data1, "test data 1")
-        self.assertEquals(data2, "test data 2")
-
-
-    @inlineCallbacks
-    def stringToAttachment(self, obj, name, contents,
-                           mimeType=MimeType("text", "x-fixture")):
-        """
-        Convenience for producing an attachment from a calendar object.
-
-        @param obj: the calendar object which owns the dropbox associated with
-            the to-be-created attachment.
-
-        @param name: the (utf-8 encoded) name to create the attachment with.
-
-        @type name: C{bytes}
-
-        @param contents: the desired contents of the new attachment.
-
-        @type contents: C{bytes}
-
-        @param mimeType: the mime type of the incoming bytes.
-
-        @return: a L{Deferred} that fires with the L{IAttachment} that is
-            created, once all the bytes have been stored.
-        """
-        att = yield obj.createAttachmentWithName(name)
-        t = att.store(mimeType, "")
-        t.write(contents)
-        yield t.loseConnection()
-        returnValue(att)
-
-
-    def attachmentToString(self, attachment):
-        """
-        Convenience to convert an L{IAttachment} to a string.
-
-        @param attachment: an L{IAttachment} provider to convert into a string.
-
-        @return: a L{Deferred} that fires with the contents of the attachment.
-
-        @rtype: L{Deferred} firing C{bytes}
-        """
-        capture = CaptureProtocol()
-        attachment.retrieve(capture)
-        return capture.deferred
-
-
-    def test_createAttachment(self):
-        """
-        L{ICalendarObject.createAttachmentWithName} will store an
-        L{IAttachment} object that can be retrieved by
-        L{ICalendarObject.attachmentWithName}.
-        """
-        return self.createAttachmentTest(lambda x: x)
-
-
-    def test_createAttachmentCommit(self):
-        """
-        L{ICalendarObject.createAttachmentWithName} will store an
-        L{IAttachment} object that can be retrieved by
-        L{ICalendarObject.attachmentWithName} in subsequent transactions.
-        """
-        @inlineCallbacks
-        def refresh(obj):
-            yield self.commit()
-            result = yield self.calendarObjectUnderTest()
-            returnValue(result)
-        return self.createAttachmentTest(refresh)
-
-
-    @inlineCallbacks
-    def test_attachmentTemporaryFileCleanup(self):
-        """
-        L{IAttachmentStream} object cleans-up its temporary file on txn abort.
-        """
-        obj = yield self.calendarObjectUnderTest()
-        attachment = yield obj.createAttachmentWithName(
-            "new.attachment",
-        )
-        t = attachment.store(MimeType("text", "x-fixture"))
-
-        temp = t._path.path
-
-        yield self.abort()
-
-        self.assertFalse(os.path.exists(temp))
-
-        obj = yield self.calendarObjectUnderTest()
-        attachment = yield obj.createAttachmentWithName(
-            "new.attachment",
-        )
-        t = attachment.store(MimeType("text", "x-fixture"))
-
-        temp = t._path.path
-        os.remove(temp)
-
-        yield self.abort()
-
-        self.assertFalse(os.path.exists(temp))
-
-
-    @inlineCallbacks
-    def test_quotaAllowedBytes(self):
-        """
-        L{ICalendarHome.quotaAllowedBytes} should return the configuration value
-        passed to the calendar store's constructor.
-        """
-        expected = deriveQuota(self)
-        home = yield self.homeUnderTest()
-        actual = home.quotaAllowedBytes()
-        self.assertEquals(expected, actual)
-
-
-    @withSpecialQuota(None)
-    @inlineCallbacks
-    def test_quotaUnlimited(self):
-        """
-        When L{ICalendarHome.quotaAllowedBytes} returns C{None}, quota is
-        unlimited; any sized attachment can be stored.
-        """
-        home = yield self.homeUnderTest()
-        allowed = home.quotaAllowedBytes()
-        self.assertIdentical(allowed, None)
-        yield self.test_createAttachment()
-
-
-    @inlineCallbacks
-    def test_quotaTransportAddress(self):
-        """
-        Since L{IAttachmentStorageTransport} is a subinterface of L{ITransport},
-        it must provide peer and host addresses.
-        """
-        obj = yield self.calendarObjectUnderTest()
-        name = 'a-fun-attachment'
-        attachment = yield obj.createAttachmentWithName(name)
-        transport = attachment.store(MimeType("test", "x-something"), "")
-        peer = transport.getPeer()
-        host = transport.getHost()
-        self.assertIdentical(peer.attachment, attachment)
-        self.assertIdentical(host.attachment, attachment)
-        self.assertIn(name, repr(peer))
-        self.assertIn(name, repr(host))
-
-
-    @inlineCallbacks
-    def exceedQuotaTest(self, getit):
-        """
-        If too many bytes are passed to the transport returned by
-        L{ICalendarObject.createAttachmentWithName},
-        L{IAttachmentStorageTransport.loseConnection} will return a L{Deferred}
-        that fails with L{QuotaExceeded}.
-        """
-        home = yield self.homeUnderTest()
-        attachment = yield getit()
-        t = attachment.store(MimeType("text", "x-fixture"), "")
-        sample = "all work and no play makes jack a dull boy"
-        chunk = (sample * (home.quotaAllowedBytes() / len(sample)))
-
-        t.write(chunk)
-        t.writeSequence([chunk, chunk])
-
-        d = t.loseConnection()
-        yield self.failUnlessFailure(d, QuotaExceeded)
-
-
-    @inlineCallbacks
-    def test_exceedQuotaNew(self):
-        """
-        When quota is exceeded on a new attachment, that attachment will no
-        longer exist.
-        """
-        obj = yield self.calendarObjectUnderTest()
-        yield self.exceedQuotaTest(
-            lambda: obj.createAttachmentWithName("too-big.attachment")
-        )
-        self.assertEquals((yield obj.attachments()), [])
-        yield self.commit()
-        obj = yield self.calendarObjectUnderTest()
-        self.assertEquals((yield obj.attachments()), [])
-
-
-    @inlineCallbacks
-    def test_exceedQuotaReplace(self):
-        """
-        When quota is exceeded while replacing an attachment, that attachment's
-        contents will not be replaced.
-        """
-        obj = yield self.calendarObjectUnderTest()
-        create = lambda: obj.createAttachmentWithName("exists.attachment")
-        get = lambda: obj.attachmentWithName("exists.attachment")
-        attachment = yield create()
-        t = attachment.store(MimeType("text", "x-fixture"), "")
-        sampleData = "a reasonably sized attachment"
-        t.write(sampleData)
-        yield t.loseConnection()
-        yield self.exceedQuotaTest(get)
-        @inlineCallbacks
-        def checkOriginal():
-            actual = yield self.attachmentToString(attachment)
-            expected = sampleData
-            # note: 60 is less than len(expected); trimming is just to make
-            # the error message look sane when the test fails.
-            actual = actual[:60]
-            self.assertEquals(actual, expected)
-        yield checkOriginal()
-        yield self.commit()
-        # Make sure that things go back to normal after a commit of that
-        # transaction.
-        obj = yield self.calendarObjectUnderTest()
-        attachment = yield get()
-        yield checkOriginal()
-
-
-    def test_removeAttachmentWithName(self, refresh=lambda x: x):
-        """
-        L{ICalendarObject.removeAttachmentWithName} will remove the calendar
-        object with the given name.
-        """
-        @inlineCallbacks
-        def deleteIt(ignored):
-            obj = yield self.calendarObjectUnderTest()
-            yield obj.removeAttachmentWithName("new.attachment")
-            obj = yield refresh(obj)
-            self.assertIdentical(
-                None, (yield obj.attachmentWithName("new.attachment"))
-            )
-            self.assertEquals(list((yield obj.attachments())), [])
-        return self.test_createAttachmentCommit().addCallback(deleteIt)
-
-
-    def test_removeAttachmentWithNameCommit(self):
-        """
-        L{ICalendarObject.removeAttachmentWithName} will remove the calendar
-        object with the given name.  (After commit, it will still be gone.)
-        """
-        @inlineCallbacks
-        def refresh(obj):
-            yield self.commit()
-            result = yield self.calendarObjectUnderTest()
-            returnValue(result)
-        return self.test_removeAttachmentWithName(refresh)
-
-
-    @inlineCallbacks
-    def test_noDropboxCalendar(self):
-        """
-        L{ICalendarObject.createAttachmentWithName} may create a directory
-        named 'dropbox', but this should not be seen as a calendar by
-        L{ICalendarHome.calendarWithName} or L{ICalendarHome.calendars}.
-        """
-        obj = yield self.calendarObjectUnderTest()
-        attachment = yield obj.createAttachmentWithName(
-            "new.attachment",
-        )
-        t = attachment.store(MimeType("text", "plain"), "")
-        t.write("new attachment text")
-        yield t.loseConnection()
-        yield self.commit()
-        home = (yield self.homeUnderTest())
-        calendars = (yield home.calendars())
-        self.assertEquals((yield home.calendarWithName("dropbox")), None)
-        self.assertEquals(
-            set([n.name() for n in calendars]),
-            set(home1_calendarNames))
-
-
-    @inlineCallbacks
     def test_finishedOnCommit(self):
         """
         Calling L{ITransaction.abort} or L{ITransaction.commit} after

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_attachments.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_attachments.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_attachments.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -14,26 +14,1245 @@
 # limitations under the License.
 ##
 
+from calendarserver.tap.util import getRootResource
+
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.value import PyCalendarValue
+
+from twext.enterprise.dal.syntax import Delete
+from twext.python.clsprop import classproperty
+from twext.web2.http_headers import MimeType
+from twext.web2.stream import MemoryStream
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.filepath import FilePath
 from twisted.trial import unittest
-from txdav.common.datastore.test.util import CommonCommonTests, buildStore, \
-    populateCalendarsFrom
-from twisted.internet.defer import inlineCallbacks, returnValue
+
 from twistedcaldav.config import config
-import os
-from calendarserver.tap.util import getRootResource
-from twext.enterprise.dal.syntax import Delete
-from txdav.common.datastore.sql_tables import schema
-from pycalendar.datetime import PyCalendarDateTime
+from twistedcaldav.ical import Property, Component
+
 from txdav.caldav.datastore.sql import CalendarStoreFeatures, DropBoxAttachment, \
     ManagedAttachment
-from twext.web2.http_headers import MimeType
-from twistedcaldav.ical import Property
-from pycalendar.value import PyCalendarValue
+from txdav.caldav.datastore.test.common import CaptureProtocol
+from txdav.caldav.icalendarstore import IAttachmentStorageTransport, IAttachment, \
+    QuotaExceeded
+from txdav.common.datastore.sql_tables import schema
+from txdav.common.datastore.test.util import CommonCommonTests, buildStore, \
+    populateCalendarsFrom, deriveQuota, withSpecialQuota
 
+import hashlib
+import os
+import uuid
+
 """
 Tests for txdav.caldav.datastore.sql attachment handling.
 """
 
+storePath = FilePath(__file__).parent().child("calendar_store")
+homeRoot = storePath.child("ho").child("me").child("home1")
+cal1Root = homeRoot.child("calendar_1")
+
+calendar1_objectNames = [
+    "1.ics",
+    "2.ics",
+    "3.ics",
+    "4.ics",
+]
+
+home1_calendarNames = [
+    "calendar_1",
+]
+
+
+class AttachmentTests(CommonCommonTests, unittest.TestCase):
+
+    metadata1 = {
+        "accessMode": "PUBLIC",
+        "isScheduleObject": True,
+        "scheduleTag": "abc",
+        "scheduleEtags": (),
+        "hasPrivateComment": False,
+    }
+    metadata2 = {
+        "accessMode": "PRIVATE",
+        "isScheduleObject": False,
+        "scheduleTag": "",
+        "scheduleEtags": (),
+        "hasPrivateComment": False,
+    }
+    metadata3 = {
+        "accessMode": "PUBLIC",
+        "isScheduleObject": None,
+        "scheduleTag": "abc",
+        "scheduleEtags": (),
+        "hasPrivateComment": True,
+    }
+    metadata4 = {
+        "accessMode": "PUBLIC",
+        "isScheduleObject": True,
+        "scheduleTag": "abc4",
+        "scheduleEtags": (),
+        "hasPrivateComment": False,
+    }
+
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(AttachmentTests, self).setUp()
+        self._sqlCalendarStore = yield buildStore(self, self.notifierFactory)
+        yield self.populate()
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    @classproperty(cache=False)
+    def requirements(cls): #@NoSelf
+        metadata1 = cls.metadata1.copy()
+        metadata2 = cls.metadata2.copy()
+        metadata3 = cls.metadata3.copy()
+        metadata4 = cls.metadata4.copy()
+        return {
+        "home1": {
+            "calendar_1": {
+                "1.ics": (cal1Root.child("1.ics").getContent(), metadata1),
+                "2.ics": (cal1Root.child("2.ics").getContent(), metadata2),
+                "3.ics": (cal1Root.child("3.ics").getContent(), metadata3),
+                "4.ics": (cal1Root.child("4.ics").getContent(), metadata4),
+            },
+        },
+    }
+
+
+    def storeUnderTest(self):
+        """
+        Create and return a L{CalendarStore} for testing.
+        """
+        return self._sqlCalendarStore
+
+
+
+class DropBoxAttachmentTests(AttachmentTests):
+
+    eventWithDropbox = "\r\n".join("""
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART;TZID=US/Eastern:20060101T100000
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:user01
+ATTENDEE;PARTSTAT=ACCEPTED:user01
+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
+    """.strip().split("\n"))
+
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(DropBoxAttachmentTests, self).setUp()
+
+        # Need to tweak config and settings to setup dropbox to work
+        self.patch(config, "EnableDropBox", True)
+        self.patch(config, "EnableManagedAttachments", False)
+        self._sqlCalendarStore.enableManagedAttachments = False
+
+        txn = self._sqlCalendarStore.newTransaction()
+        cs = schema.CALENDARSERVER
+        yield Delete(
+            From=cs,
+            Where=cs.NAME == "MANAGED-ATTACHMENTS"
+        ).on(txn)
+        yield txn.commit()
+
+
+    @inlineCallbacks
+    def createAttachmentTest(self, refresh):
+        """
+        Common logic for attachment-creation tests.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"), "")
+        self.assertProvides(IAttachmentStorageTransport, t)
+        t.write("new attachment")
+        t.write(" text")
+        yield t.loseConnection()
+        obj = yield refresh(obj)
+        attachment = yield obj.attachmentWithName("new.attachment")
+        self.assertProvides(IAttachment, attachment)
+        data = yield self.attachmentToString(attachment)
+        self.assertEquals(data, "new attachment text")
+        contentType = attachment.contentType()
+        self.assertIsInstance(contentType, MimeType)
+        self.assertEquals(contentType, MimeType("text", "x-fixture"))
+        self.assertEquals(attachment.md5(), '50a9f27aeed9247a0833f30a631f1858')
+        self.assertEquals(
+            [_attachment.name() for _attachment in (yield obj.attachments())],
+            ['new.attachment']
+        )
+
+
+    @inlineCallbacks
+    def stringToAttachment(self, obj, name, contents,
+                           mimeType=MimeType("text", "x-fixture")):
+        """
+        Convenience for producing an attachment from a calendar object.
+
+        @param obj: the calendar object which owns the dropbox associated with
+            the to-be-created attachment.
+
+        @param name: the (utf-8 encoded) name to create the attachment with.
+
+        @type name: C{bytes}
+
+        @param contents: the desired contents of the new attachment.
+
+        @type contents: C{bytes}
+
+        @param mimeType: the mime type of the incoming bytes.
+
+        @return: a L{Deferred} that fires with the L{IAttachment} that is
+            created, once all the bytes have been stored.
+        """
+        att = yield obj.createAttachmentWithName(name)
+        t = att.store(mimeType, "")
+        t.write(contents)
+        yield t.loseConnection()
+        returnValue(att)
+
+
+    def attachmentToString(self, attachment):
+        """
+        Convenience to convert an L{IAttachment} to a string.
+
+        @param attachment: an L{IAttachment} provider to convert into a string.
+
+        @return: a L{Deferred} that fires with the contents of the attachment.
+
+        @rtype: L{Deferred} firing C{bytes}
+        """
+        capture = CaptureProtocol()
+        attachment.retrieve(capture)
+        return capture.deferred
+
+
+    @inlineCallbacks
+    def test_attachmentPath(self):
+        """
+        L{ICalendarObject.createAttachmentWithName} will store an
+        L{IAttachment} object that can be retrieved by
+        L{ICalendarObject.attachmentWithName}.
+        """
+        yield self.createAttachmentTest(lambda x: x)
+        attachmentRoot = (
+            yield self.calendarObjectUnderTest()
+        )._txn._store.attachmentsPath
+        obj = yield self.calendarObjectUnderTest()
+        hasheduid = hashlib.md5(obj._dropboxID).hexdigest()
+        attachmentPath = attachmentRoot.child(
+            hasheduid[0:2]).child(hasheduid[2:4]).child(hasheduid).child(
+                "new.attachment")
+        self.assertTrue(attachmentPath.isfile())
+
+
+    @inlineCallbacks
+    def test_dropboxID(self):
+        """
+        L{ICalendarObject.dropboxID} should synthesize its dropbox from the X
+        -APPLE-DROPBOX property, if available.
+        """
+        cal = yield self.calendarUnderTest()
+        yield cal.createCalendarObjectWithName("drop.ics", Component.fromString(
+                self.eventWithDropbox
+            )
+        )
+        obj = yield cal.calendarObjectWithName("drop.ics")
+        self.assertEquals((yield obj.dropboxID()), "some-dropbox-id")
+
+
+    @inlineCallbacks
+    def test_dropboxIDs(self):
+        """
+        L{ICalendarObject.getAllDropboxIDs} returns a L{Deferred} that fires
+        with a C{list} of all Dropbox IDs.
+        """
+        home = yield self.homeUnderTest()
+        # The only item in the home which has an ATTACH or X-APPLE-DROPBOX
+        # property.
+        allDropboxIDs = set([
+            u'FE5CDC6F-7776-4607-83A9-B90FF7ACC8D0.dropbox',
+        ])
+        self.assertEquals(set((yield home.getAllDropboxIDs())),
+                          allDropboxIDs)
+
+
+    @inlineCallbacks
+    def test_indexByDropboxProperty(self):
+        """
+        L{ICalendarHome.calendarObjectWithDropboxID} will return a calendar
+        object in the calendar home with the given final segment in its C{X
+        -APPLE-DROPBOX} property URI.
+        """
+        objName = "with-dropbox.ics"
+        cal = yield self.calendarUnderTest()
+        yield cal.createCalendarObjectWithName(
+            objName, Component.fromString(
+                self.eventWithDropbox
+            )
+        )
+        yield self.commit()
+        home = yield self.homeUnderTest()
+        cal = yield self.calendarUnderTest()
+        fromName = yield cal.calendarObjectWithName(objName)
+        fromDropbox = yield home.calendarObjectWithDropboxID("some-dropbox-id")
+        self.assertEquals(fromName, fromDropbox)
+
+
+    @inlineCallbacks
+    def test_twoAttachmentsWithTheSameName(self):
+        """
+        Attachments are uniquely identified by their associated object and path;
+        two attachments with the same name won't overwrite each other.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        obj2 = yield self.calendarObjectUnderTest("2.ics")
+        att1 = yield self.stringToAttachment(obj, "sample.attachment",
+                                             "test data 1")
+        att2 = yield self.stringToAttachment(obj2, "sample.attachment",
+                                             "test data 2")
+        data1 = yield self.attachmentToString(att1)
+        data2 = yield self.attachmentToString(att2)
+        self.assertEquals(data1, "test data 1")
+        self.assertEquals(data2, "test data 2")
+
+
+    def test_createAttachment(self):
+        """
+        L{ICalendarObject.createAttachmentWithName} will store an
+        L{IAttachment} object that can be retrieved by
+        L{ICalendarObject.attachmentWithName}.
+        """
+        return self.createAttachmentTest(lambda x: x)
+
+
+    def test_createAttachmentCommit(self):
+        """
+        L{ICalendarObject.createAttachmentWithName} will store an
+        L{IAttachment} object that can be retrieved by
+        L{ICalendarObject.attachmentWithName} in subsequent transactions.
+        """
+        @inlineCallbacks
+        def refresh(obj):
+            yield self.commit()
+            result = yield self.calendarObjectUnderTest()
+            returnValue(result)
+        return self.createAttachmentTest(refresh)
+
+
+    @inlineCallbacks
+    def test_attachmentTemporaryFileCleanup(self):
+        """
+        L{IAttachmentStream} object cleans-up its temporary file on txn abort.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"))
+
+        temp = t._path.path
+
+        yield self.abort()
+
+        self.assertFalse(os.path.exists(temp))
+
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"))
+
+        temp = t._path.path
+        os.remove(temp)
+
+        yield self.abort()
+
+        self.assertFalse(os.path.exists(temp))
+
+
+    @inlineCallbacks
+    def test_quotaAllowedBytes(self):
+        """
+        L{ICalendarHome.quotaAllowedBytes} should return the configuration value
+        passed to the calendar store's constructor.
+        """
+        expected = deriveQuota(self)
+        home = yield self.homeUnderTest()
+        actual = home.quotaAllowedBytes()
+        self.assertEquals(expected, actual)
+
+
+    @withSpecialQuota(None)
+    @inlineCallbacks
+    def test_quotaUnlimited(self):
+        """
+        When L{ICalendarHome.quotaAllowedBytes} returns C{None}, quota is
+        unlimited; any sized attachment can be stored.
+        """
+        home = yield self.homeUnderTest()
+        allowed = home.quotaAllowedBytes()
+        self.assertIdentical(allowed, None)
+        yield self.test_createAttachment()
+
+
+    @inlineCallbacks
+    def test_quotaTransportAddress(self):
+        """
+        Since L{IAttachmentStorageTransport} is a subinterface of L{ITransport},
+        it must provide peer and host addresses.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        name = 'a-fun-attachment'
+        attachment = yield obj.createAttachmentWithName(name)
+        transport = attachment.store(MimeType("test", "x-something"), "")
+        peer = transport.getPeer()
+        host = transport.getHost()
+        self.assertIdentical(peer.attachment, attachment)
+        self.assertIdentical(host.attachment, attachment)
+        self.assertIn(name, repr(peer))
+        self.assertIn(name, repr(host))
+
+
+    @inlineCallbacks
+    def exceedQuotaTest(self, getit):
+        """
+        If too many bytes are passed to the transport returned by
+        L{ICalendarObject.createAttachmentWithName},
+        L{IAttachmentStorageTransport.loseConnection} will return a L{Deferred}
+        that fails with L{QuotaExceeded}.
+        """
+        home = yield self.homeUnderTest()
+        attachment = yield getit()
+        t = attachment.store(MimeType("text", "x-fixture"), "")
+        sample = "all work and no play makes jack a dull boy"
+        chunk = (sample * (home.quotaAllowedBytes() / len(sample)))
+
+        t.write(chunk)
+        t.writeSequence([chunk, chunk])
+
+        d = t.loseConnection()
+        yield self.failUnlessFailure(d, QuotaExceeded)
+
+
+    @inlineCallbacks
+    def test_exceedQuotaNew(self):
+        """
+        When quota is exceeded on a new attachment, that attachment will no
+        longer exist.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        yield self.exceedQuotaTest(
+            lambda: obj.createAttachmentWithName("too-big.attachment")
+        )
+        self.assertEquals((yield obj.attachments()), [])
+        yield self.commit()
+        obj = yield self.calendarObjectUnderTest()
+        self.assertEquals((yield obj.attachments()), [])
+
+
+    @inlineCallbacks
+    def test_exceedQuotaReplace(self):
+        """
+        When quota is exceeded while replacing an attachment, that attachment's
+        contents will not be replaced.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        create = lambda: obj.createAttachmentWithName("exists.attachment")
+        get = lambda: obj.attachmentWithName("exists.attachment")
+        attachment = yield create()
+        t = attachment.store(MimeType("text", "x-fixture"), "")
+        sampleData = "a reasonably sized attachment"
+        t.write(sampleData)
+        yield t.loseConnection()
+        yield self.exceedQuotaTest(get)
+        @inlineCallbacks
+        def checkOriginal():
+            actual = yield self.attachmentToString(attachment)
+            expected = sampleData
+            # note: 60 is less than len(expected); trimming is just to make
+            # the error message look sane when the test fails.
+            actual = actual[:60]
+            self.assertEquals(actual, expected)
+        yield checkOriginal()
+        yield self.commit()
+        # Make sure that things go back to normal after a commit of that
+        # transaction.
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield get()
+        yield checkOriginal()
+
+
+    def test_removeAttachmentWithName(self, refresh=lambda x: x):
+        """
+        L{ICalendarObject.removeAttachmentWithName} will remove the calendar
+        object with the given name.
+        """
+        @inlineCallbacks
+        def deleteIt(ignored):
+            obj = yield self.calendarObjectUnderTest()
+            yield obj.removeAttachmentWithName("new.attachment")
+            obj = yield refresh(obj)
+            self.assertIdentical(
+                None, (yield obj.attachmentWithName("new.attachment"))
+            )
+            self.assertEquals(list((yield obj.attachments())), [])
+        return self.test_createAttachmentCommit().addCallback(deleteIt)
+
+
+    def test_removeAttachmentWithNameCommit(self):
+        """
+        L{ICalendarObject.removeAttachmentWithName} will remove the calendar
+        object with the given name.  (After commit, it will still be gone.)
+        """
+        @inlineCallbacks
+        def refresh(obj):
+            yield self.commit()
+            result = yield self.calendarObjectUnderTest()
+            returnValue(result)
+        return self.test_removeAttachmentWithName(refresh)
+
+
+    @inlineCallbacks
+    def test_noDropboxCalendar(self):
+        """
+        L{ICalendarObject.createAttachmentWithName} may create a directory
+        named 'dropbox', but this should not be seen as a calendar by
+        L{ICalendarHome.calendarWithName} or L{ICalendarHome.calendars}.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment",
+        )
+        t = attachment.store(MimeType("text", "plain"), "")
+        t.write("new attachment text")
+        yield t.loseConnection()
+        yield self.commit()
+        home = (yield self.homeUnderTest())
+        calendars = (yield home.calendars())
+        self.assertEquals((yield home.calendarWithName("dropbox")), None)
+        self.assertEquals(
+            set([n.name() for n in calendars]),
+            set(home1_calendarNames))
+
+
+    @inlineCallbacks
+    def test_cleanupAttachments(self):
+        """
+        L{ICalendarObject.remove} will remove an associated calendar
+        attachment.
+        """
+
+        # Create attachment
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"))
+        t.write("new attachment")
+        t.write(" text")
+        yield t.loseConnection()
+        apath = attachment._path.path
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest()
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertFalse(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertEqual(quota, 0)
+
+
+    @inlineCallbacks
+    def test_cleanupMultipleAttachments(self):
+        """
+        L{ICalendarObject.remove} will remove all associated calendar
+        attachments.
+        """
+
+        # Create attachment
+        obj = yield self.calendarObjectUnderTest()
+
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"))
+        t.write("new attachment")
+        t.write(" text")
+        yield t.loseConnection()
+        apath1 = attachment._path.path
+
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment2",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"))
+        t.write("new attachment 2")
+        t.write(" text")
+        yield t.loseConnection()
+        apath2 = attachment._path.path
+
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath1))
+        self.assertTrue(os.path.exists(apath2))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest()
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertFalse(os.path.exists(apath1))
+        self.assertFalse(os.path.exists(apath2))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertEqual(quota, 0)
+
+
+    @inlineCallbacks
+    def test_cleanupAttachmentsOnMultipleResources(self):
+        """
+        L{ICalendarObject.remove} will remove all associated calendar
+        attachments unless used in another resource.
+        """
+
+        # Create attachment
+        obj = yield self.calendarObjectUnderTest()
+
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"))
+        t.write("new attachment")
+        t.write(" text")
+        yield t.loseConnection()
+        apath = attachment._path.path
+
+        new_component = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+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
+BEGIN:VEVENT
+ATTENDEE;CN="Wilfredo Sanchez";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailt
+ o:wsanchez at example.com
+ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cda
+ boo at example.com
+DTEND;TZID=US/Pacific:%(now)s0324T124500
+TRANSP:OPAQUE
+ORGANIZER;CN="Wilfredo Sanchez":mailto:wsanchez at example.com
+UID:uid1-attachmenttest
+DTSTAMP:20090326T145447Z
+LOCATION:Wilfredo's Office
+SEQUENCE:2
+X-APPLE-EWS-BUSYSTATUS:BUSY
+X-APPLE-DROPBOX:/calendars/__uids__/user01/dropbox/FE5CDC6F-7776-4607-83
+ A9-B90FF7ACC8D0.dropbox
+SUMMARY:CalDAV protocol updates
+DTSTART;TZID=US/Pacific:%(now)s0324T121500
+CREATED:20090326T145440Z
+BEGIN:VALARM
+X-WR-ALARMUID:DB39AB67-449C-441C-89D2-D740B5F41A73
+TRIGGER;VALUE=DATE-TIME:%(now)s0324T180009Z
+ACTION:AUDIO
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % {"now": 2012}
+
+        calendar = yield self.calendarUnderTest()
+        yield calendar.createCalendarObjectWithName(
+            "test.ics", Component.fromString(new_component)
+        )
+
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest()
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest("test.ics")
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertFalse(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertEqual(quota, 0)
+
+
+
+class ManagedAttachmentTests(AttachmentTests):
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(ManagedAttachmentTests, self).setUp()
+
+        # Need to tweak config and settings to setup dropbox to work
+        self.patch(config, "EnableDropBox", False)
+        self.patch(config, "EnableManagedAttachments", True)
+        self._sqlCalendarStore.enableManagedAttachments = True
+
+
+    @inlineCallbacks
+    def createAttachmentTest(self, refresh):
+        """
+        Common logic for attachment-creation tests.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createManagedAttachment()
+        mid = attachment.managedID()
+        t = attachment.store(MimeType("text", "x-fixture"), "new.attachment")
+        self.assertProvides(IAttachmentStorageTransport, t)
+        t.write("new attachment")
+        t.write(" text")
+        yield t.loseConnection()
+        obj = yield refresh(obj)
+        attachment = yield obj.attachmentWithManagedID(mid)
+        self.assertProvides(IAttachment, attachment)
+        data = yield self.attachmentToString(attachment)
+        self.assertEquals(data, "new attachment text")
+        contentType = attachment.contentType()
+        self.assertIsInstance(contentType, MimeType)
+        self.assertEquals(contentType, MimeType("text", "x-fixture"))
+        self.assertEquals(attachment.md5(), '50a9f27aeed9247a0833f30a631f1858')
+        self.assertEquals(
+            (yield obj.managedAttachmentList()),
+            ['new-%s.attachment' % (mid[:8],)]
+        )
+
+        returnValue(mid)
+
+
+    @inlineCallbacks
+    def stringToAttachment(self, obj, name, contents,
+                           mimeType=MimeType("text", "x-fixture")):
+        """
+        Convenience for producing an attachment from a calendar object.
+
+        @param obj: the calendar object which owns the dropbox associated with
+            the to-be-created attachment.
+
+        @param name: the (utf-8 encoded) name to create the attachment with.
+
+        @type name: C{bytes}
+
+        @param contents: the desired contents of the new attachment.
+
+        @type contents: C{bytes}
+
+        @param mimeType: the mime type of the incoming bytes.
+
+        @return: a L{Deferred} that fires with the L{IAttachment} that is
+            created, once all the bytes have been stored.
+        """
+        att = yield obj.createManagedAttachment()
+        t = att.store(mimeType, name)
+        t.write(contents)
+        yield t.loseConnection()
+        returnValue(att)
+
+
+    def attachmentToString(self, attachment):
+        """
+        Convenience to convert an L{IAttachment} to a string.
+
+        @param attachment: an L{IAttachment} provider to convert into a string.
+
+        @return: a L{Deferred} that fires with the contents of the attachment.
+
+        @rtype: L{Deferred} firing C{bytes}
+        """
+        capture = CaptureProtocol()
+        attachment.retrieve(capture)
+        return capture.deferred
+
+
+    @inlineCallbacks
+    def test_attachmentPath(self):
+        """
+        L{ICalendarObject.createManagedAttachment} will store an
+        L{IAttachment} object that can be retrieved by
+        L{ICalendarObject.attachmentWithManagedID}.
+        """
+
+        mid = yield self.createAttachmentTest(lambda x: x)
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.attachmentWithManagedID(mid)
+        hasheduid = hashlib.md5(str(attachment._attachmentID)).hexdigest()
+
+        attachmentRoot = (
+            yield self.calendarObjectUnderTest()
+        )._txn._store.attachmentsPath
+        attachmentPath = attachmentRoot.child(
+            hasheduid[0:2]).child(hasheduid[2:4]).child(hasheduid)
+        self.assertTrue(attachmentPath.isfile())
+
+
+    @inlineCallbacks
+    def test_twoAttachmentsWithTheSameName(self):
+        """
+        Attachments are uniquely identified by their associated object and path;
+        two attachments with the same name won't overwrite each other.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        obj2 = yield self.calendarObjectUnderTest("2.ics")
+        att1 = yield self.stringToAttachment(obj, "sample.attachment",
+                                             "test data 1")
+        att2 = yield self.stringToAttachment(obj2, "sample.attachment",
+                                             "test data 2")
+        data1 = yield self.attachmentToString(att1)
+        data2 = yield self.attachmentToString(att2)
+        self.assertEquals(data1, "test data 1")
+        self.assertEquals(data2, "test data 2")
+
+
+    def test_createAttachment(self):
+        """
+        L{ICalendarObject.createManagedAttachment} will store an
+        L{IAttachment} object that can be retrieved by
+        L{ICalendarObject.attachmentWithManagedID}.
+        """
+        return self.createAttachmentTest(lambda x: x)
+
+
+    def test_createAttachmentCommit(self):
+        """
+        L{ICalendarObject.createManagedAttachment} will store an
+        L{IAttachment} object that can be retrieved by
+        L{ICalendarObject.attachmentWithManagedID} in subsequent transactions.
+        """
+        @inlineCallbacks
+        def refresh(obj):
+            yield self.commit()
+            result = yield self.calendarObjectUnderTest()
+            returnValue(result)
+        return self.createAttachmentTest(refresh)
+
+
+    @inlineCallbacks
+    def test_attachmentTemporaryFileCleanup(self):
+        """
+        L{IAttachmentStream} object cleans-up its temporary file on txn abort.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createManagedAttachment()
+        t = attachment.store(MimeType("text", "x-fixture"), "new.attachment")
+
+        temp = t._path.path
+
+        yield self.abort()
+
+        self.assertFalse(os.path.exists(temp))
+
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createManagedAttachment()
+        t = attachment.store(MimeType("text", "x-fixture"), "new.attachment")
+
+        temp = t._path.path
+        os.remove(temp)
+
+        yield self.abort()
+
+        self.assertFalse(os.path.exists(temp))
+
+
+    @inlineCallbacks
+    def test_quotaAllowedBytes(self):
+        """
+        L{ICalendarHome.quotaAllowedBytes} should return the configuration value
+        passed to the calendar store's constructor.
+        """
+        expected = deriveQuota(self)
+        home = yield self.homeUnderTest()
+        actual = home.quotaAllowedBytes()
+        self.assertEquals(expected, actual)
+
+
+    @withSpecialQuota(None)
+    @inlineCallbacks
+    def test_quotaUnlimited(self):
+        """
+        When L{ICalendarHome.quotaAllowedBytes} returns C{None}, quota is
+        unlimited; any sized attachment can be stored.
+        """
+        home = yield self.homeUnderTest()
+        allowed = home.quotaAllowedBytes()
+        self.assertIdentical(allowed, None)
+        yield self.test_createAttachment()
+
+
+    @inlineCallbacks
+    def test_quotaTransportAddress(self):
+        """
+        Since L{IAttachmentStorageTransport} is a subinterface of L{ITransport},
+        it must provide peer and host addresses.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        name = 'a-fun-attachment'
+        attachment = yield obj.createManagedAttachment()
+        transport = attachment.store(MimeType("test", "x-something"), name)
+        peer = transport.getPeer()
+        host = transport.getHost()
+        self.assertIdentical(peer.attachment, attachment)
+        self.assertIdentical(host.attachment, attachment)
+        self.assertIn(name, repr(peer))
+        self.assertIn(name, repr(host))
+
+
+    @inlineCallbacks
+    def exceedQuotaTest(self, getit, name):
+        """
+        If too many bytes are passed to the transport returned by
+        L{ICalendarObject.createManagedAttachment},
+        L{IAttachmentStorageTransport.loseConnection} will return a L{Deferred}
+        that fails with L{QuotaExceeded}.
+        """
+        home = yield self.homeUnderTest()
+        attachment = yield getit()
+        t = attachment.store(MimeType("text", "x-fixture"), name)
+        sample = "all work and no play makes jack a dull boy"
+        chunk = (sample * (home.quotaAllowedBytes() / len(sample)))
+
+        t.write(chunk)
+        t.writeSequence([chunk, chunk])
+
+        d = t.loseConnection()
+        yield self.failUnlessFailure(d, QuotaExceeded)
+
+
+    @inlineCallbacks
+    def test_exceedQuotaNew(self):
+        """
+        When quota is exceeded on a new attachment, that attachment will no
+        longer exist.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        yield self.exceedQuotaTest(
+            lambda: obj.createManagedAttachment(), "too-big.attachment"
+        )
+        self.assertEquals((yield obj.managedAttachmentList()), [])
+        yield self.commit()
+        obj = yield self.calendarObjectUnderTest()
+        self.assertEquals((yield obj.managedAttachmentList()), [])
+
+
+    @inlineCallbacks
+    def test_exceedQuotaReplace(self):
+        """
+        When quota is exceeded while replacing an attachment, that attachment's
+        contents will not be replaced.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        create = lambda: obj.createManagedAttachment()
+        attachment = yield create()
+        get = lambda: obj.attachmentWithManagedID(attachment.managedID())
+        t = attachment.store(MimeType("text", "x-fixture"), "new.attachment")
+        sampleData = "a reasonably sized attachment"
+        t.write(sampleData)
+        yield t.loseConnection()
+        yield self.exceedQuotaTest(get, "exists.attachment")
+        @inlineCallbacks
+        def checkOriginal():
+            actual = yield self.attachmentToString(attachment)
+            expected = sampleData
+            # note: 60 is less than len(expected); trimming is just to make
+            # the error message look sane when the test fails.
+            actual = actual[:60]
+            self.assertEquals(actual, expected)
+        yield checkOriginal()
+        yield self.commit()
+        # Make sure that things go back to normal after a commit of that
+        # transaction.
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield get()
+        yield checkOriginal()
+
+
+    def test_removeManagedAttachmentWithID(self, refresh=lambda x: x):
+        """
+        L{ICalendarObject.removeManagedAttachmentWithID} will remove the calendar
+        object with the given managed-id.
+        """
+        @inlineCallbacks
+        def deleteIt(mid):
+            obj = yield self.calendarObjectUnderTest()
+            yield obj.removeManagedAttachmentWithID(mid)
+            obj = yield refresh(obj)
+            self.assertIdentical(
+                None, (yield obj.attachmentWithManagedID(mid))
+            )
+            self.assertEquals(list((yield obj.managedAttachmentList())), [])
+        return self.test_createAttachmentCommit().addCallback(deleteIt)
+
+
+    def test_removeManagedAttachmentWithIDCommit(self):
+        """
+        L{ICalendarObject.removeManagedAttachmentWithID} will remove the calendar
+        object with the given managed-id.  (After commit, it will still be gone.)
+        """
+        @inlineCallbacks
+        def refresh(obj):
+            yield self.commit()
+            result = yield self.calendarObjectUnderTest()
+            returnValue(result)
+        return self.test_removeManagedAttachmentWithID(refresh)
+
+
+    @inlineCallbacks
+    def test_noDropboxCalendar(self):
+        """
+        L{ICalendarObject.createManagedAttachment} may create a directory
+        named 'dropbox', but this should not be seen as a calendar by
+        L{ICalendarHome.calendarWithName} or L{ICalendarHome.calendars}.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createManagedAttachment()
+        t = attachment.store(MimeType("text", "plain"), "new.attachment")
+        t.write("new attachment text")
+        yield t.loseConnection()
+        yield self.commit()
+        home = (yield self.homeUnderTest())
+        calendars = (yield home.calendars())
+        self.assertEquals((yield home.calendarWithName("dropbox")), None)
+        self.assertEquals(
+            set([n.name() for n in calendars]),
+            set(home1_calendarNames))
+
+
+    @inlineCallbacks
+    def test_cleanupAttachments(self):
+        """
+        L{ICalendarObject.remove} will remove an associated calendar
+        attachment.
+        """
+
+        # Create attachment
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createManagedAttachment()
+        t = attachment.store(MimeType("text", "x-fixture"), "new.attachment")
+        t.write("new attachment")
+        t.write(" text")
+        yield t.loseConnection()
+        apath = attachment._path.path
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest()
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertFalse(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertEqual(quota, 0)
+
+
+    @inlineCallbacks
+    def test_cleanupMultipleAttachments(self):
+        """
+        L{ICalendarObject.remove} will remove all associated calendar
+        attachments.
+        """
+
+        # Create attachment
+        obj = yield self.calendarObjectUnderTest()
+
+        attachment = yield obj.createManagedAttachment()
+        t = attachment.store(MimeType("text", "x-fixture"), "new.attachment")
+        t.write("new attachment")
+        t.write(" text")
+        yield t.loseConnection()
+        apath1 = attachment._path.path
+
+        attachment = yield obj.createManagedAttachment()
+        t = attachment.store(MimeType("text", "x-fixture"), "new.attachment2")
+        t.write("new attachment 2")
+        t.write(" text")
+        yield t.loseConnection()
+        apath2 = attachment._path.path
+
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath1))
+        self.assertTrue(os.path.exists(apath2))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest()
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertFalse(os.path.exists(apath1))
+        self.assertFalse(os.path.exists(apath2))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertEqual(quota, 0)
+
+
+    @inlineCallbacks
+    def test_cleanupAttachmentsOnMultipleResources(self):
+        """
+        L{ICalendarObject.remove} will remove all associated calendar
+        attachments unless used in another resource.
+        """
+
+        # Create attachment
+        obj = yield self.calendarObjectUnderTest()
+        cdata = yield obj.component()
+
+        attachment, _ignore_location = yield obj.addAttachment(None, MimeType("text", "x-fixture"), "new.attachment", MemoryStream("new attachment text"), cdata)
+        mid = attachment.managedID()
+        apath = attachment._path.path
+
+        newcdata = Component.fromString(str(cdata).replace("uid1", "uid1-attached"))
+        calendar = yield self.calendarUnderTest()
+        cobj = yield calendar.createCalendarObjectWithName(
+            "test.ics", newcdata
+        )
+        yield cobj.copyResourceAttachments(((mid, str(uuid.uuid4()),),))
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest()
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest("test.ics")
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertFalse(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertEqual(quota, 0)
+
+
+
 now = PyCalendarDateTime.getToday().getYear()
 
 PLAIN_ICS = """BEGIN:VCALENDAR
@@ -197,11 +1416,11 @@
         self.notifierFactory.reset()
 
         txn = self._sqlCalendarStore.newTransaction()
-        Delete(
+        yield Delete(
             From=schema.ATTACHMENT,
             Where=None
         ).on(txn)
-        Delete(
+        yield Delete(
             From=schema.ATTACHMENT_CALENDAR_OBJECT,
             Where=None
         ).on(txn)
@@ -219,6 +1438,7 @@
     @inlineCallbacks
     def _addAttachment(self, home, calendar, event, dropboxid, name):
 
+        self._sqlCalendarStore._dropbox_ok = True
         txn = self._sqlCalendarStore.newTransaction()
 
         # Create an event with an attachment
@@ -239,6 +1459,7 @@
         ))
         yield event.setComponent(cal)
         yield txn.commit()
+        self._sqlCalendarStore._dropbox_ok = False
 
         returnValue(attachment)
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_sql.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_sql.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -22,14 +22,12 @@
 from pycalendar.datetime import PyCalendarDateTime
 from pycalendar.timezone import PyCalendarTimezone
 
-from twext.enterprise.dal.syntax import Select, Parameter, Insert
+from twext.enterprise.dal.syntax import Select, Parameter, Insert, Delete
 from twext.python.vcomponent import VComponent
-from twext.web2.http_headers import MimeType
 
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.task import deferLater
-from twisted.python import hashlib
 from twisted.trial import unittest
 
 from twistedcaldav import caldavxml
@@ -53,7 +51,6 @@
 from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
 
 import datetime
-import os
 
 class CalendarSQLStorageTests(CalendarCommonTests, unittest.TestCase):
     """
@@ -130,25 +127,6 @@
 
 
     @inlineCallbacks
-    def test_attachmentPath(self):
-        """
-        L{ICalendarObject.createAttachmentWithName} will store an
-        L{IAttachment} object that can be retrieved by
-        L{ICalendarObject.attachmentWithName}.
-        """
-        yield self.createAttachmentTest(lambda x: x)
-        attachmentRoot = (
-            yield self.calendarObjectUnderTest()
-        )._txn._store.attachmentsPath
-        obj = yield self.calendarObjectUnderTest()
-        hasheduid = hashlib.md5(obj._dropboxID).hexdigest()
-        attachmentPath = attachmentRoot.child(
-            hasheduid[0:2]).child(hasheduid[2:4]).child(hasheduid).child(
-                "new.attachment")
-        self.assertTrue(attachmentPath.isfile())
-
-
-    @inlineCallbacks
     def test_migrateCalendarFromFile(self):
         """
         C{_migrateCalendar()} can migrate a file-backed calendar to a database-
@@ -366,6 +344,19 @@
         backed calendar. Test that migrating a calendar containing duplicate attachments
         will de-duplicate those attachments and proceed without error.
         """
+        # Need to tweak config and settings to setup dropbox to work
+        self.patch(config, "EnableDropBox", True)
+        self.patch(config, "EnableManagedAttachments", False)
+        self._sqlCalendarStore.enableManagedAttachments = False
+
+        txn = self._sqlCalendarStore.newTransaction()
+        cs = schema.CALENDARSERVER
+        yield Delete(
+            From=cs,
+            Where=cs.NAME == "MANAGED-ATTACHMENTS"
+        ).on(txn)
+        yield txn.commit()
+
         fromCalendar = yield (yield self.fileTransaction().calendarHomeWithUID(
             "home_attachments")).calendarWithName("calendar_1")
         toHome = yield self.transactionUnderTest().calendarHomeWithUID(
@@ -1436,200 +1427,3 @@
         obj = (yield self.calendarObjectUnderTest())
         calendarObject = (yield home.objectResourceWithID(obj._resourceID))
         self.assertNotEquals(calendarObject, None)
-
-
-    @inlineCallbacks
-    def test_cleanupAttachments(self):
-        """
-        L{ICalendarObject.remove} will remove an associated calendar
-        attachment.
-        """
-
-        # Create attachment
-        obj = yield self.calendarObjectUnderTest()
-        attachment = yield obj.createAttachmentWithName(
-            "new.attachment",
-        )
-        t = attachment.store(MimeType("text", "x-fixture"))
-        t.write("new attachment")
-        t.write(" text")
-        yield t.loseConnection()
-        apath = attachment._path.path
-        yield self.commit()
-
-        self.assertTrue(os.path.exists(apath))
-
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
-        quota = (yield home.quotaUsedBytes())
-        yield self.commit()
-        self.assertNotEqual(quota, 0)
-
-        # Remove resource
-        obj = yield self.calendarObjectUnderTest()
-        yield obj.remove()
-        yield self.commit()
-
-        self.assertFalse(os.path.exists(apath))
-
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
-        quota = (yield home.quotaUsedBytes())
-        yield self.commit()
-        self.assertEqual(quota, 0)
-
-
-    @inlineCallbacks
-    def test_cleanupMultipleAttachments(self):
-        """
-        L{ICalendarObject.remove} will remove all associated calendar
-        attachments.
-        """
-
-        # Create attachment
-        obj = yield self.calendarObjectUnderTest()
-
-        attachment = yield obj.createAttachmentWithName(
-            "new.attachment",
-        )
-        t = attachment.store(MimeType("text", "x-fixture"))
-        t.write("new attachment")
-        t.write(" text")
-        yield t.loseConnection()
-        apath1 = attachment._path.path
-
-        attachment = yield obj.createAttachmentWithName(
-            "new.attachment2",
-        )
-        t = attachment.store(MimeType("text", "x-fixture"))
-        t.write("new attachment 2")
-        t.write(" text")
-        yield t.loseConnection()
-        apath2 = attachment._path.path
-
-        yield self.commit()
-
-        self.assertTrue(os.path.exists(apath1))
-        self.assertTrue(os.path.exists(apath2))
-
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
-        quota = (yield home.quotaUsedBytes())
-        yield self.commit()
-        self.assertNotEqual(quota, 0)
-
-        # Remove resource
-        obj = yield self.calendarObjectUnderTest()
-        yield obj.remove()
-        yield self.commit()
-
-        self.assertFalse(os.path.exists(apath1))
-        self.assertFalse(os.path.exists(apath2))
-
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
-        quota = (yield home.quotaUsedBytes())
-        yield self.commit()
-        self.assertEqual(quota, 0)
-
-
-    @inlineCallbacks
-    def test_cleanupAttachmentsOnMultipleResources(self):
-        """
-        L{ICalendarObject.remove} will remove all associated calendar
-        attachments unless used in another resource.
-        """
-
-        # Create attachment
-        obj = yield self.calendarObjectUnderTest()
-
-        attachment = yield obj.createAttachmentWithName(
-            "new.attachment",
-        )
-        t = attachment.store(MimeType("text", "x-fixture"))
-        t.write("new attachment")
-        t.write(" text")
-        yield t.loseConnection()
-        apath = attachment._path.path
-
-        new_component = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Apple Inc.//iCal 4.0.1//EN
-CALSCALE:GREGORIAN
-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
-BEGIN:VEVENT
-ATTENDEE;CN="Wilfredo Sanchez";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailt
- o:wsanchez at example.com
-ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cda
- boo at example.com
-DTEND;TZID=US/Pacific:%(now)s0324T124500
-TRANSP:OPAQUE
-ORGANIZER;CN="Wilfredo Sanchez":mailto:wsanchez at example.com
-UID:uid1-attachmenttest
-DTSTAMP:20090326T145447Z
-LOCATION:Wilfredo's Office
-SEQUENCE:2
-X-APPLE-EWS-BUSYSTATUS:BUSY
-X-APPLE-DROPBOX:/calendars/__uids__/user01/dropbox/FE5CDC6F-7776-4607-83
- A9-B90FF7ACC8D0.dropbox
-SUMMARY:CalDAV protocol updates
-DTSTART;TZID=US/Pacific:%(now)s0324T121500
-CREATED:20090326T145440Z
-BEGIN:VALARM
-X-WR-ALARMUID:DB39AB67-449C-441C-89D2-D740B5F41A73
-TRIGGER;VALUE=DATE-TIME:%(now)s0324T180009Z
-ACTION:AUDIO
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n") % {"now": 2012}
-
-        calendar = yield self.calendarUnderTest()
-        yield calendar.createCalendarObjectWithName(
-            "test.ics", VComponent.fromString(new_component)
-        )
-
-        yield self.commit()
-
-        self.assertTrue(os.path.exists(apath))
-
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
-        quota = (yield home.quotaUsedBytes())
-        yield self.commit()
-        self.assertNotEqual(quota, 0)
-
-        # Remove resource
-        obj = yield self.calendarObjectUnderTest()
-        yield obj.remove()
-        yield self.commit()
-
-        self.assertTrue(os.path.exists(apath))
-
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
-        quota = (yield home.quotaUsedBytes())
-        yield self.commit()
-        self.assertNotEqual(quota, 0)
-
-        # Remove resource
-        obj = yield self.calendarObjectUnderTest("test.ics")
-        yield obj.remove()
-        yield self.commit()
-
-        self.assertFalse(os.path.exists(apath))
-
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
-        quota = (yield home.quotaUsedBytes())
-        yield self.commit()
-        self.assertEqual(quota, 0)

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/icalendarstore.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/icalendarstore.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -84,6 +84,13 @@
 
 
 
+class AttachmentDropboxNotAllowed(Exception):
+    """
+    Dropbox attachments no longer allowed.
+    """
+
+
+
 class QuotaExceeded(Exception):
     """
     The quota for a particular user has been exceeded.

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/sql.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/sql.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -613,27 +613,31 @@
         @return: an L{CommonHomeChild} or C{None} if no such child
             exists.
         """
-        bindRows = yield cls._acceptedBindForNameAndHomeID.on(home._txn, name=shareUID, homeID=home._resourceID)
+        bindRows = yield cls._bindForNameAndHomeID.on(home._txn, name=shareUID, homeID=home._resourceID)
         if bindRows:
             bindMode, homeID, resourceID, bindName, bindStatus, bindMessage = bindRows[0] #@UnusedVariable
             # use childWithName, since it is cached by querycacher
-            # returnValue((yield home.childWithID(resourceID)))
-            ownerHomeID = yield cls.ownerHomeID(home._txn, resourceID)
-            ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerHomeID)
-            ownerAddressBook = yield ownerHome.addressbook()
-            returnValue((yield home.childWithName(ownerAddressBook.shareeABName())))
+            if bindStatus == _BIND_STATUS_ACCEPTED:
+                ownerHomeID = yield cls.ownerHomeID(home._txn, resourceID)
+                ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerHomeID)
+                ownerAddressBook = yield ownerHome.addressbook()
+                returnValue((yield home.childWithName(ownerAddressBook.shareeABName())))
+            else:
+                returnValue((yield home.childWithID(resourceID)))
 
 
-        groupBindRows = yield AddressBookObject._acceptedBindForNameAndHomeID.on(home._txn, name=shareUID, homeID=home._resourceID)
+        groupBindRows = yield AddressBookObject._bindForNameAndHomeID.on(home._txn, name=shareUID, homeID=home._resourceID)
         if groupBindRows:
             bindMode, homeID, resourceID, bindName, bindStatus, bindMessage = groupBindRows[0] #@UnusedVariable
             ownerAddressBookID = yield AddressBookObject.ownerAddressBookID(home._txn, resourceID)
             # use childWithName, since it is cached by querycacher
-            # addressbook = yield cls.objectWithID(home, ownerAddressBookIDRows[0][0])
-            ownerHomeID = yield cls.ownerHomeID(home._txn, ownerAddressBookID)
-            ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerHomeID)
-            ownerAddressBook = yield ownerHome.addressbook()
-            addressbook = yield home.childWithName(ownerAddressBook.shareeABName())
+            if bindStatus == _BIND_STATUS_ACCEPTED:
+                ownerHomeID = yield cls.ownerHomeID(home._txn, ownerAddressBookID)
+                ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerHomeID)
+                ownerAddressBook = yield ownerHome.addressbook()
+                addressbook = yield home.childWithName(ownerAddressBook.shareeABName())
+            else:
+                addressbook = yield cls.objectWithID(home, ownerAddressBookID)
             returnValue((yield addressbook.objectResourceWithID(resourceID)))
 
         returnValue(None)
@@ -794,7 +798,7 @@
         remainingIDs = set(groupIDs)
         while remainingIDs:
             memberRows = yield cls._memberIDsWithGroupIDsQuery(remainingIDs).on(txn, groupIDs=remainingIDs)
-            objectIDs |= set([memberRow[0] for memberRow in memberRows])
+            objectIDs |= set(memberRow[0] for memberRow in memberRows)
             examinedIDs |= remainingIDs
             remainingIDs = objectIDs - examinedIDs
 
@@ -1802,37 +1806,10 @@
 
 
     @inlineCallbacks
-    def shareWith(self, shareeHome, mode, status=None, message=None):
-        """
-        Share this (owned) L{CommonHomeChild} with another home.
+    def notifyChanged(self):
+        returnValue((yield self._addressbook.notifyChanged()))
 
-        @param shareeHome: The home of the sharee.
-        @type shareeHome: L{CommonHome}
 
-        @param mode: The sharing mode; L{_BIND_MODE_READ} or
-            L{_BIND_MODE_WRITE} or L{_BIND_MODE_DIRECT}
-        @type mode: L{str}
-
-        @param status: The sharing status; L{_BIND_STATUS_INVITED} or
-            L{_BIND_STATUS_ACCEPTED}
-        @type mode: L{str}
-
-        @param message: The proposed message to go along with the share, which
-            will be used as the default display name.
-        @type mode: L{str}
-
-        @rtype: a L{Deferred} which fires with a L{ICalendar} if the sharee item
-        """
-
-        yield self._shareWith(shareeHome, mode, status=status, message=message)
-        addressbook = yield self.addressbook()
-        shareeAddressBook = yield shareeHome.childWithID(addressbook._resourceID)
-
-        sharedGroup = yield shareeAddressBook.objectResourceWithID(self._resourceID)
-        yield self._addressbook.notifyChanged()
-        returnValue(sharedGroup)
-
-
     @inlineCallbacks
     def asShared(self):
         """

Added: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/3765A955-1B96-41EA-994D-335192BEDCCD.vcf
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/3765A955-1B96-41EA-994D-335192BEDCCD.vcf	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/3765A955-1B96-41EA-994D-335192BEDCCD.vcf	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,18 @@
+BEGIN:VCARD
+VERSION:3.0
+N:InfoIn;All;;;
+FN:All InfoIn
+ORG:allinfo Company;
+EMAIL;type=INTERNET;type=WORK;type=pref:allinfomationin at example.com
+TEL;type=WORK;type=pref:777-777-7777
+TEL;type=CELL:8888888888
+item1.ADR;type=WORK;type=pref:;;1 Gally Street Apt #2;Mountain Top;CA;99999;USA
+item1.X-ABADR:us
+X-YAHOO;type=WORK;type=pref:saeko.where at example.com
+X-YAHOO-ID;type=WORK;type=pref:saeko.where at example.com
+item2.X-ABRELATEDNAMES;type=pref:Mayumi Yan
+item2.X-ABLabel:_$!<Friend>!$_
+item3.X-ABRELATEDNAMES:Shane
+item3.X-ABLabel:_$!<Assistant>!$_
+UID:3765A955-1B96-41EA-994D-335192BEDCCD
+END:VCARD

Added: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/44745975-AE6D-4FB0-80A6-A298427E047A.vcf
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/44745975-AE6D-4FB0-80A6-A298427E047A.vcf	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/44745975-AE6D-4FB0-80A6-A298427E047A.vcf	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,12 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Mariotte;WithNote;;;
+FN:WithNote Mariotte
+EMAIL;type=INTERNET;type=WORK;type=pref:withnmariotte at example.com
+TEL;type=WORK;type=pref:1-777-777-7777
+TEL;type=CELL:1-8888888888
+item1.ADR;type=WORK;type=pref:;;1 North Blvd;Cupertino;CA;99999;United States
+item1.X-ABADR:us
+NOTE: Address book server test contact that hsa note field filled in.
+UID:44745975-AE6D-4FB0-80A6-A298427E047A
+END:VCARD

Added: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/44EE78BF-8814-4471-899C-92280CEFB098.vcf
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/44EE78BF-8814-4471-899C-92280CEFB098.vcf	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/44EE78BF-8814-4471-899C-92280CEFB098.vcf	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,11 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Śuterry;HiAscii;;;
+FN:HiAscii Śuterry
+EMAIL;type=INTERNET;type=WORK;type=pref:hiascii at example.com
+TEL;type=WORK;type=pref:777-777-7777
+TEL;type=CELL:8888888888
+item1.ADR;type=WORK;type=pref:;;1 ïlena;Paris;Paris;77777;France
+item1.X-ABADR:us
+UID:44EE78BF-8814-4471-899C-92280CEFB098
+END:VCARD

Added: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/8424B7F0-C878-4722-B522-EBB07CF48AD7.vcf
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/8424B7F0-C878-4722-B522-EBB07CF48AD7.vcf	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/8424B7F0-C878-4722-B522-EBB07CF48AD7.vcf	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,12 @@
+BEGIN:VCARD
+VERSION:3.0
+N:バイト;ダブル;;;
+FN:ダブル バイト
+EMAIL;type=INTERNET;type=WORK;type=pref:doublebytes at example.com
+TEL;type=WORK;type=pref:777-777-7777
+TEL;type=CELL:8888888888
+item1.ADR;type=WORK;type=pref:;;1-23-4 Irohacho #2;Tokyo;Japan;33-3333;Japan
+item1.X-ABADR:us
+NOTE:日本ですよ。
+UID:8424B7F0-C878-4722-B522-EBB07CF48AD7
+END:VCARD

Added: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/934731C6-1C95-4C40-BE1F-FA4215B2307B.vcf
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/934731C6-1C95-4C40-BE1F-FA4215B2307B.vcf	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/934731C6-1C95-4C40-BE1F-FA4215B2307B.vcf	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,993 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Picture;With;;;
+FN:With Picture
+EMAIL;type=INTERNET;type=WORK;type=pref:withpicture at example.com
+TEL;type=WORK;type=pref:777-777-7777
+TEL;type=CELL:8888888888
+item1.ADR;type=WORK;type=pref:;;1234 Golly Street;Sunnyside;CA;99999;USA
+item1.X-ABADR:us
+PHOTO;BASE64:
+  /9j/4AAQSkZJRgABAQAAAQABAAD/7QA8UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAB8cAVoAAx
+  sl RxwCAAACAAIcAhkAC1Bob3RvIEJvb3RoAP/iG6hJQ0NfUFJPRklMRQABAQAAG5hhcHBsAgA
+  AAG1u dHJSR0IgWFlaIAfaAAEAEwAJADEABGFjc3BBUFBMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+  AAAAAAD2 1gABAAAAANMtYXBwbFYcEOZVYuhIRg5LwLIi62wAAAAAAAAAAAAAAAAAAAAAAAAAA
+  AAAAAAAAAAA AAAAEXJYWVoAAAFQAAAAFGdYWVoAAAFkAAAAFGJYWVoAAAF4AAAAFHd0cHQAAA
+  GMAAAAFGNoYWQA AAGgAAAALHJUUkMAAAHMAAAIDGdUUkMAAAnYAAAIDGJUUkMAABHkAAAIDGF
+  hcmcAABnwAAAAIGFh Z2cAABoQAAAAIGFhYmcAABowAAAAIHZjZ3QAABpQAAAAMG5kaW4AABqA
+  AAAAOGRlc2MAABq4AAAA ZGRzY20AABscAAAALm1tb2QAABtMAAAAKGNwcnQAABt0AAAAJFhZW
+  iAAAAAAAAB7vQAAQXsAAAJL WFlaIAAAAAAAAFYqAACp0AAAFF9YWVogAAAAAAAAJO8AABS1AA
+  C8glhZWiAAAAAAAADz2AABAAAA ARYIc2YzMgAAAAAAAQu3AAAFlv//81cAAAcpAAD91///+7f
+  ///2mAAAD2gAAwPZjdXJ2AAAAAAAA BAAAAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUA
+  SgBPAFQAWQBeAGMAaABtAHIAdwB8AIEA hgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0
+  ADVANoA4ADlAOoA8AD1APsBAQEHAQwBEgEY AR4BJQErATEBOAE+AUUBSwFSAVkBYAFmAW0BdQ
+  F8AYMBigGSAZkBoQGoAbABuAHAAcgB0AHYAeAB 6QHxAfoCAgILAhQCHAIlAi4CNwJAAkoCUwJ
+  cAmYCcAJ5AoMCjQKXAqECqwK1Ar8CygLUAt8C6gL0 Av8DCgMVAyADKwM3A0IDTQNZA2UDcAN8
+  A4gDlAOgA6wDuQPFA9ID3gPrA/gEBAQRBB4ELAQ5BEYE VARhBG8EfASKBJgEpgS0BMIE0QTfB
+  O4E/AULBRoFKAU3BUcFVgVlBXQFhAWTBaMFswXDBdMF4wXz BgMGFAYkBjUGRQZWBmcGeAaJBp
+  oGqwa9Bs4G4AbyBwMHFQcnBzkHTAdeB3AHgweWB6gHuwfOB+EH 9AgICBsILwhCCFYIagh+CJI
+  Ipgi6CM4I4wj3CQwJIQk2CUsJYAl1CYoJoAm1CcsJ4An2CgwKIgo5 Ck8KZQp8CpIKqQrACtcK
+  7gsFCx0LNAtLC2MLewuTC6sLwwvbC/MMDAwkDD0MVgxuDIcMoQy6DNMM 7Q0GDSANOg1UDW4Ni
+  A2iDbwN1w3xDgwOJw5CDl0OeA6TDq8Oyg7mDwIPHg86D1YPcg+OD6sPyA/k EAEQHhA7EFgQdh
+  CTELEQzhDsEQoRKBFGEWQRgxGhEcAR3xH+Eh0SPBJbEnoSmhK5EtkS+RMZEzkT WRN6E5oTuxP
+  bE/wUHRQ+FF8UgRSiFMQU5RUHFSkVSxVtFZAVshXVFfcWGhY9FmAWgxanFsoW7hcS FzUXWRd9
+  F6IXxhfqGA8YNBhZGH0YoxjIGO0ZExk4GV4ZhBmqGdAZ9hodGkMaahqQGrca3hsGGy0b VBt8G
+  6MbyxvzHBscQxxsHJQcvRzmHQ4dNx1gHYodsx3dHgYeMB5aHoQerh7YHwMfLR9YH4Mfrh/Z IA
+  QgMCBbIIcgsyDeIQohNyFjIY8hvCHpIhUiQiJwIp0iyiL4IyUjUyOBI68j3SQMJDokaSSXJMYk
+  9SUkJVQlgyWzJeImEiZCJnImoybTJwMnNCdlJ5Ynxyf4KCooWyiNKL4o8CkiKVUphym5KewqHy
+  pS KoUquCrrKx4rUiuGK7or7iwiLFYsiiy/LPQtKS1eLZMtyC39LjMuaS6eLtQvCy9BL3cvri/
+  kMBsw UjCJMMEw+DEwMWcxnzHXMg8ySDKAMrgy8TMqM2MznDPVNA80SDSCNLw09jUwNWo1pTXf
+  Nho2VTaQ Nss3BjdCN343uTf1ODE4bTiqOOY5IzlgOZ052joXOlQ6kjrPOw07SzuJO8c8BjxEP
+  IM8wj0BPUA9 fz2/Pf4+Pj5+Pr4+/j8/P38/wEAAQEFAgkDEQQVBR0GIQcpCDEJOQpFC00MWQ1
+  hDm0PeRCFEZUSo ROxFMEV0RbhF/EZARoVGykcOR1NHmUfeSCNIaUivSPVJO0mBScdKDkpVSpt
+  K4ksqS3FLuEwATEhM kEzYTSBNaE2xTfpOQk6MTtVPHk9nT7FP+1BFUI9Q2VEkUW5RuVIEUk9S
+  mlLlUzFTfFPIVBRUYFSt VPlVRlWSVd9WLFZ6VsdXFFdiV7BX/lhMWJpY6Vk4WYZZ1VokWnRaw
+  1sTW2NbslwDXFNco1z0XURd lV3mXjdeiV7aXyxffl/QYCJgdGDHYRlhbGG/YhJiZWK5YwxjYG
+  O0ZAhkXGSxZQVlWmWvZgRmWWav ZwRnWmewaAZoXGiyaQlpX2m2ag1qZGq8axNra2vDbBtsc2z
+  LbSNtfG3Vbi5uh27gbzpvk2/tcEdw oXD7cVZxsHILcmZywXMcc3hz03QvdIt053VDdaB1/HZZ
+  drZ3E3dwd854K3iJeOd5RXmjegJ6YHq/ ex57fXvcfDx8m3z7fVt9u34bfnx+3H89f55//4Bgg
+  MKBI4GFgeeCSYKrgw6DcIPThDaEmYT8hWCF w4YnhouG74dUh7iIHYiBiOaJTImxihaKfIrii0
+  iLrowUjHuM4o1Ija+OF45+juWPTY+1kB2QhZDu kVaRv5IokpGS+pNkk82UN5ShlQuVdZXglkq
+  WtZcgl4uX95himM6ZOpmmmhKafprrm1ebxJwxnJ+d DJ15neeeVZ7DnzGfoKAPoH2g7KFbocui
+  OqKqoxqjiqP6pGqk26VMpbymLqafpxCngqf0qGWo2KlK qbyqL6qiqxWriKv7rG+s461WrcuuP
+  66zryivnbARsIew/LFxseeyXbLTs0mzv7Q2tK21JLWbthK2 ibcBt3m38bhpuOG5WrnSuku6xL
+  s+u7e8MLyqvSS9nr4ZvpO/Dr+JwATAf8D6wXbB8cJtwunDZsPi xF/E3MVZxdbGU8bRx07HzMh
+  KyMnJR8nGykXKxMtDy8LMQszBzUHNwc5CzsLPQ8/D0ETQxtFH0cjS StLM007T0NRT1NbVWNXb
+  1l7W4tdl1+nYbdjx2XXZ+tp/2wPbiNwO3JPdGd2e3iTeqt8x37fgPuDF 4Uzh0+Ja4uLjauPy5
+  HrlAuWL5hPmnOcl56/oOOjC6Uzp1upg6urrdev/7IrtFu2h7izuuO9E79Dw XPDp8XXyAvKP8x
+  zzqvQ39MX1U/Xh9m/2/veM+Bv4qvk5+cn6Wfro+3j8CPyZ/Sn9uv5L/tz/bmN1 cnYAAAAAAAA
+  EAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0A cgB3AHwA
+  gQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2gDgAOUA6gDwAPUA+wEB AQcBD
+  AESARgBHgElASsBMQE4AT4BRQFLAVIBWQFgAWYBbQF1AXwBgwGKAZIBmQGhAagBsAG4AcAB yA
+  HQAdgB4AHpAfEB+gICAgsCFAIcAiUCLgI3AkACSgJTAlwCZgJwAnkCgwKNApcCoQKrArUCvwLK
+  AtQC3wLqAvQC/wMKAxUDIAMrAzcDQgNNA1kDZQNwA3wDiAOUA6ADrAO5A8UD0gPeA+sD+AQEBB
+  EE HgQsBDkERgRUBGEEbwR8BIoEmASmBLQEwgTRBN8E7gT8BQsFGgUoBTcFRwVWBWUFdAWEBZM
+  FowWz BcMF0wXjBfMGAwYUBiQGNQZFBlYGZwZ4BokGmgarBr0GzgbgBvIHAwcVBycHOQdMB14H
+  cAeDB5YH qAe7B84H4Qf0CAgIGwgvCEIIVghqCH4IkgimCLoIzgjjCPcJDAkhCTYJSwlgCXUJi
+  gmgCbUJywng CfYKDAoiCjkKTwplCnwKkgqpCsAK1wruCwULHQs0C0sLYwt7C5MLqwvDC9sL8w
+  wMDCQMPQxWDG4M hwyhDLoM0wztDQYNIA06DVQNbg2IDaINvA3XDfEODA4nDkIOXQ54DpMOrw7
+  KDuYPAg8eDzoPVg9y D44Pqw/ID+QQARAeEDsQWBB2EJMQsRDOEOwRChEoEUYRZBGDEaERwBHf
+  Ef4SHRI8ElsSehKaErkS 2RL5ExkTORNZE3oTmhO7E9sT/BQdFD4UXxSBFKIUxBTlFQcVKRVLF
+  W0VkBWyFdUV9xYaFj0WYBaD FqcWyhbuFxIXNRdZF30XohfGF+oYDxg0GFkYfRijGMgY7RkTGT
+  gZXhmEGaoZ0Bn2Gh0aQxpqGpAa txreGwYbLRtUG3wboxvLG/McGxxDHGwclBy9HOYdDh03HWA
+  dih2zHd0eBh4wHloehB6uHtgfAx8t H1gfgx+uH9kgBCAwIFsghyCzIN4hCiE3IWMhjyG8Ieki
+  FSJCInAinSLKIvgjJSNTI4EjryPdJAwk OiRpJJckxiT1JSQlVCWDJbMl4iYSJkImciajJtMnA
+  yc0J2UnlifHJ/goKihbKI0ovijwKSIpVSmH Kbkp7CofKlIqhSq4KusrHitSK4YruivuLCIsVi
+  yKLL8s9C0pLV4tky3ILf0uMy5pLp4u1C8LL0Ev dy+uL+QwGzBSMIkwwTD4MTAxZzGfMdcyDzJ
+  IMoAyuDLxMyozYzOcM9U0DzRINII0vDT2NTA1ajWl Nd82GjZVNpA2yzcGN0I3fje5N/U4MTht
+  OKo45jkjOWA5nTnaOhc6VDqSOs87DTtLO4k7xzwGPEQ8 gzzCPQE9QD1/Pb89/j4+Pn4+vj7+P
+  z8/fz/AQABAQUCCQMRBBUFHQYhBykIMQk5CkULTQxZDWEOb Q95EIURlRKhE7EUwRXRFuEX8Rk
+  BGhUbKRw5HU0eZR95II0hpSK9I9Uk7SYFJx0oOSlVKm0riSypL cUu4TABMSEyQTNhNIE1oTbF
+  N+k5CToxO1U8eT2dPsU/7UEVQj1DZUSRRblG5UgRST1KaUuVTMVN8 U8hUFFRgVK1U+VVGVZJV
+  31YsVnpWx1cUV2JXsFf+WExYmljpWThZhlnVWiRadFrDWxNbY1uyXANc U1yjXPRdRF2VXeZeN
+  16JXtpfLF9+X9BgImB0YMdhGWFsYb9iEmJlYrljDGNgY7RkCGRcZLFlBWVa Za9mBGZZZq9nBG
+  daZ7BoBmhcaLJpCWlfabZqDWpkarxrE2tra8NsG2xzbMttI218bdVuLm6HbuBv Om+Tb+1wR3C
+  hcPtxVnGwcgtyZnLBcxxzeHPTdC90i3TndUN1oHX8dll2tncTd3B3zngreIl453lF eaN6Anpg
+  er97Hnt9e9x8PHybfPt9W327fht+fH7cfz1/nn//gGCAwoEjgYWB54JJgquDDoNwg9OE NoSZh
+  PyFYIXDhieGi4bvh1SHuIgdiIGI5olMibGKFop8iuKLSIuujBSMe4zijUiNr44Xjn6O5Y9N j7
+  WQHZCFkO6RVpG/kiiSkZL6k2STzZQ3lKGVC5V1leCWSpa1lyCXi5f3mGKYzpk6maaaEpp+muub
+  V5vEnDGcn50MnXmd555VnsOfMZ+goA+gfaDsoVuhy6I6oqqjGqOKo/qkaqTbpUylvKYupp+nEK
+  eC p/SoZajYqUqpvKovqqKrFauIq/usb6zjrVaty64/rrOvKK+dsBGwh7D8sXGx57JdstOzSbO
+  /tDa0 rbUktZu2EraJtwG3ebfxuGm44blaudK6S7rEuz67t7wwvKq9JL2evhm+k78Ov4nABMB/
+  wPrBdsHx wm3C6cNmw+LEX8TcxVnF1sZTxtHHTsfMyErIyclHycbKRcrEy0PLwsxCzMHNQc3Bz
+  kLOws9Dz8PQ RNDG0UfRyNJK0szTTtPQ1FPU1tVY1dvWXtbi12XX6dht2PHZddn62n/bA9uI3A
+  7ck90Z3Z7eJN6q 3zHft+A+4MXhTOHT4lri4uNq4/LkeuUC5YvmE+ac5yXnr+g46MLpTOnW6mD
+  q6ut16//siu0W7aHu LO6470Tv0PBc8OnxdfIC8o/zHPOq9Df0xfVT9eH2b/b+94z4G/iq+Tn5
+  yfpZ+uj7ePwI/Jn9Kf26 /kv+3P9uY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AM
+  gA3ADsAQABFAEoATwBUAFkA XgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtw
+  C8AMEAxgDLANAA1QDaAOAA5QDq APAA9QD7AQEBBwEMARIBGAEeASUBKwExATgBPgFFAUsBUgF
+  ZAWABZgFtAXUBfAGDAYoBkgGZAaEB qAGwAbgBwAHIAdAB2AHgAekB8QH6AgICCwIUAhwCJQIu
+  AjcCQAJKAlMCXAJmAnACeQKDAo0ClwKh AqsCtQK/AsoC1ALfAuoC9AL/AwoDFQMgAysDNwNCA
+  00DWQNlA3ADfAOIA5QDoAOsA7kDxQPSA94D 6wP4BAQEEQQeBCwEOQRGBFQEYQRvBHwEigSYBK
+  YEtATCBNEE3wTuBPwFCwUaBSgFNwVHBVYFZQV0 BYQFkwWjBbMFwwXTBeMF8wYDBhQGJAY1BkU
+  GVgZnBngGiQaaBqsGvQbOBuAG8gcDBxUHJwc5B0wH XgdwB4MHlgeoB7sHzgfhB/QICAgbCC8I
+  QghWCGoIfgiSCKYIugjOCOMI9wkMCSEJNglLCWAJdQmK CaAJtQnLCeAJ9goMCiIKOQpPCmUKf
+  AqSCqkKwArXCu4LBQsdCzQLSwtjC3sLkwurC8ML2wvzDAwM JAw9DFYMbgyHDKEMugzTDO0NBg
+  0gDToNVA1uDYgNog28DdcN8Q4MDicOQg5dDngOkw6vDsoO5g8C Dx4POg9WD3IPjg+rD8gP5BA
+  BEB4QOxBYEHYQkxCxEM4Q7BEKESgRRhFkEYMRoRHAEd8R/hIdEjwS WxJ6EpoSuRLZEvkTGRM5
+  E1kTehOaE7sT2xP8FB0UPhRfFIEUohTEFOUVBxUpFUsVbRWQFbIV1RX3 FhoWPRZgFoMWpxbKF
+  u4XEhc1F1kXfReiF8YX6hgPGDQYWRh9GKMYyBjtGRMZOBleGYQZqhnQGfYa HRpDGmoakBq3Gt
+  4bBhstG1QbfBujG8sb8xwbHEMcbByUHL0c5h0OHTcdYB2KHbMd3R4GHjAeWh6E Hq4e2B8DHy0
+  fWB+DH64f2SAEIDAgWyCHILMg3iEKITchYyGPIbwh6SIVIkIicCKdIsoi+CMlI1Mj gSOvI90k
+  DCQ6JGkklyTGJPUlJCVUJYMlsyXiJhImQiZyJqMm0ycDJzQnZSeWJ8cn+CgqKFsojSi+ KPApI
+  ilVKYcpuSnsKh8qUiqFKrgq6yseK1Irhiu6K+4sIixWLIosvyz0LSktXi2TLcgt/S4zLmku ni
+  7ULwsvQS93L64v5DAbMFIwiTDBMPgxMDFnMZ8x1zIPMkgygDK4MvEzKjNjM5wz1TQPNEg0gjS8
+  NPY1MDVqNaU13zYaNlU2kDbLNwY3Qjd+N7k39TgxOG04qjjmOSM5YDmdOdo6FzpUOpI6zzsNO0
+  s7 iTvHPAY8RDyDPMI9AT1APX89vz3+Pj4+fj6+Pv4/Pz9/P8BAAEBBQIJAxEEFQUdBiEHKQgx
+  CTkKR QtNDFkNYQ5tD3kQhRGVEqETsRTBFdEW4RfxGQEaFRspHDkdTR5lH3kgjSGlIr0j1STtJ
+  gUnHSg5K VUqbSuJLKktxS7hMAExITJBM2E0gTWhNsU36TkJOjE7VTx5PZ0+xT/tQRVCPUNlRJ
+  FFuUblSBFJP UppS5VMxU3xTyFQUVGBUrVT5VUZVklXfVixWelbHVxRXYlewV/5YTFiaWOlZOF
+  mGWdVaJFp0WsNb E1tjW7JcA1xTXKNc9F1EXZVd5l43Xole2l8sX35f0GAiYHRgx2EZYWxhv2I
+  SYmViuWMMY2BjtGQI ZFxksWUFZVplr2YEZllmr2cEZ1pnsGgGaFxosmkJaV9ptmoNamRqvGsT
+  a2trw2wbbHNsy20jbXxt 1W4ubodu4G86b5Nv7XBHcKFw+3FWcbByC3JmcsFzHHN4c9N0L3SLd
+  Od1Q3Wgdfx2WXa2dxN3cHfO eCt4iXjneUV5o3oCemB6v3see3173Hw8fJt8+31bfbt+G358ft
+  x/PX+ef/+AYIDCgSOBhYHngkmC q4MOg3CD04Q2hJmE/IVghcOGJ4aLhu+HVIe4iB2IgYjmiUy
+  JsYoWinyK4otIi66MFIx7jOKNSI2v jheOfo7lj02PtZAdkIWQ7pFWkb+SKJKRkvqTZJPNlDeU
+  oZULlXWV4JZKlrWXIJeLl/eYYpjOmTqZ ppoSmn6a65tXm8ScMZyfnQydeZ3nnlWew58xn6CgD
+  6B9oOyhW6HLojqiqqMao4qj+qRqpNulTKW8 pi6mn6cQp4Kn9KhlqNipSqm8qi+qoqsVq4ir+6
+  xvrOOtVq3Lrj+us68or52wEbCHsPyxcbHnsl2y 07NJs7+0NrSttSS1m7YStom3Abd5t/G4abj
+  huVq50rpLusS7Pru3vDC8qr0kvZ6+Gb6Tvw6/icAE wH/A+sF2wfHCbcLpw2bD4sRfxNzFWcXW
+  xlPG0cdOx8zISsjJyUfJxspFysTLQ8vCzELMwc1BzcHO Qs7Cz0PPw9BE0MbRR9HI0krSzNNO0
+  9DUU9TW1VjV29Ze1uLXZdfp2G3Y8dl12fraf9sD24jcDtyT 3Rndnt4k3qrfMd+34D7gxeFM4d
+  PiWuLi42rj8uR65QLli+YT5pznJeev6DjowulM6dbqYOrq63Xr /+yK7Rbtoe4s7rjvRO/Q8Fz
+  w6fF18gLyj/Mc86r0N/TF9VP14fZv9v73jPgb+Kr5OfnJ+ln66Pt4 /Aj8mf0p/br+S/7c/25w
+  YXJhAAAAAAADAAAAAmZmAADypwAADVkAABPQAAALA3BhcmEAAAAAAAMA AAACZmYAAPKnAAANW
+  QAAE9AAAAsDcGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACwN2Y2d0 AAAAAAAAAAEAAQ
+  AAAAAAAAABAAAAAQAAAAAAAAABAAAAAQAAAAAAAAABAABuZGluAAAAAAAAADAA AKPAAABXwAA
+  ASsAAAJ5AAAAlQAAAEwAAAFBAAABUQAACMzMAAjMzAAIzM2Rlc2MAAAAAAAAACkNp bmVtYSBI
+  RAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAA
+  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAABIAAAAc AE
+  MAaQBuAGUAbQBhACAASABEAABtbW9kAAAAAAAABhAAAJIjAgAqqcBCT4AAAAAAAAAAAAAAAAAA
+  AAAAdGV4dAAAAABDb3B5cmlnaHQgQXBwbGUsIEluYy4sIDIwMTAA/+EAQEV4aWYAAE1NACoAAA
+  AI AAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAAoCgAwAEAAAAAQAAAeAAAAAA/9sAQwA
+  CAgIC AgECAgICAgICAwMGBAMDAwMHBQUEBggHCAgIBwgICQoNCwkJDAoICAsPCwwNDg4ODgkL
+  EBEPDhEN Dg4O/9sAQwECAgIDAwMGBAQGDgkICQ4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4OD
+  g4ODg4ODg4O Dg4ODg4ODg4ODg4ODg4O/8AAEQgB4AKAAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQ
+  EBAAAAAAAAAAAB AgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhN
+  RYQcicRQygZGhCCNC scEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RV
+  VldYWVpjZGVmZ2hpanN0 dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4u
+  brCw8TFxsfIycrS09TV1tfY 2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQ
+  EAAAAAAAABAgMEBQYHCAkKC//E ALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXE
+  TIjKBCBRCkaGxwQkjM1LwFWJy0QoW JDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZX
+  WFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWG h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5u
+  sLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp 6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A+To438
+  n5HVipGCR1q/Em6JiwOc9PrUEKbY8KCd3SrsUZ UfNk7sZA9ayje2jCbjHboWoh5gIJCsp71PH
+  ErQltrDcfXrTQvz4I4zwAeasLE8bcA4HTNNU07u+o oKLd0yaJFEWQCGwCD6CtJBFsOCSx6nNV
+  wuxE+Q5HC59KsxxuACNufUjpWjp33ZLm0TwpsdgnO3HG fWrSjEnG7nr+VQRIWYqQ3J4q7GMNn
+  OCTilaNxzaT1Y+NWSJQw6Lipo13RM3pgfjUa5RzuO5asxKi kEkYA6VqtI+pLaWhZZRJFGDlyM
+  kY71MgUIecgjn61HbguCjoyg9+lSxoVbDHbnkkjpWaj0fQldri g52+4yaFj2y8AqvbmlCls4O
+  DnAJ9Ksqylxxz0Fa81mrIt3jHTqKkW/DZ5PJxUgQhmByXBz+lAAUA k9D2qcbAFZTuPcntUOUm
+  7Iq17SYigNGoYbiF7UhGJfQAZz2zUiliduRwdtTkYQhSFXd1NDk927lR Scit85kG3gnHFLhwA
+  FHOc/Wp4wNuVHHc1IACxB7D16VE2+a6QX3uiEJlD8pyTkj0p2394QQSQMYF SnOSMhe3Sm7Cjq
+  mecck1fNeOpMoprRgFOCOetOcMSrHg9elPAxIN2MHkcdqcRltuDg85rG0m9EFu zI/LRWwAScZ
+  Jz1qdYQ6EkYZe1KiFnG0qPXNPwSQpJI7Y705yfRltSe5XdHLbu2enpT1HzorHOBjH tUmxhkkP
+  j+VTJGwYFgNp6k9qXncIzTViAxgAjsPU0Kn4euas+WcOx4GaBHk7sL1GM96q65Xd3KjF W0ZFj
+  aSQQ425NSrGSARwTkA9s0qRqCSenQVZCrsO0MOelRKSTSSM3q9Csy5Us3LnH51IYyYs5zg8 U4
+  JyTtJwanCsEwMHDZxjkD3q4JNle8rscgySOM/xe9O2sMdTjJx9KmSJt7FgASevtT1j3MCucg9/
+  SpcnfVGcVGz1K5A3K5545+tChshh1PUmrYjUxlQCy57elBSQPwowDjmm9VuNP3WV+EYEhsZwD7
+  1J 5J8hPmBPU+9WFBEhLAcgHkd6MFQvTI45PQVV9dUOzbuiqy7QY14wc59qV41LYHJz61Oqqck
+  5Ck55 70BAH3N+B9aXMloWm4+RD5fBU8Z70/CGIIRkgCpH+UZYZUd6REClRz64PUVUWum5Ls3e
+  40KOWPJJ ySO1O2ARk7SecjHrUx4G7aCuc1IIt5G3PqwpXfVA1FSRXXPkruO3PApArlxhgFAw1
+  WOA4UgLzyT/ ACppUuu3B2selWvd3RDTTd1Yr/OZM54PU+1QqDu5OwDocdauldockfTNQAttBl
+  AKdiB1pdW2i3Hm V0tCA9HwDgNjFVHAM24Da2CRn0q80eWPGFIz9DVdo+u75h0yKiN4PRgrRV7
+  3KgDbl24B5xnvUbB9 x2jAH86uBBgsM5XvnpUBDEE7lJ7itLoEkiIKwO5Dgn1HWlbywxDk5HQ+
+  9TE7odqDaucjPUVGw+Ut wxPYCsVBbBy8xG65jcHJycDHeq4j3LuHAA+6fX1q9jeMgDJ6HFQbS
+  YsZ6frXRBNJ3JbtuisBiTK8 8jj3pXZyjsOCCOoqfaDKCo+975pxVhHtwAMYJNZ631FzK+xS2h
+  gxYcqvr1qIYDbiCMj86uGPDBWH TuO9Q+UCgC/fz1NaXhs2a+0SVkQBR5me5/Wo2CkMGyvHU/W
+  rRTM23DZzyewprKwKbgSMdMVlzLWx GiaZWZjgncMdAKh2MZPu4wME5qw6cL8v3RSNteQqnUD5
+  qSduhUndaFJ0zDg9R1xUBjDJtwdpXIIq 4yb3L7H9wTTHXbBtTHzdCaq/K77icVoomeYiybHO0
+  d6rSJt6EEdiexrTKtxsXLY5Paq7RMJgSAV9 AO9aRloxwhzbsoup2LzkHjFV5EJCg4Cgg5J71o
+  vHvZQT8oyenJqJok3spy2RkUmna19yNFKyZmkF Qp77TkfWmYAUKwK854rQ8tGiBLLnuPU01kX
+  G/cCc8nHFRey1KbVrWM4oPvAhWB4J7etV2RfkUckN lauunm4JAHJOMVXlj2gEHoM5qKcYq/mV
+  Dkdmys20naecZGRVcqQAduATyPStJAB8q7Xz6dxULsm0 gRk49T1pTcl0M5rXRGaYwqAschhgj
+  pz2pohVSH5DAYPPerk4Aj2k8KPkJqMqhVsjA+8aykoyV1qy KrbdmcPbIxtyuVDk8fnU6rIGwd
+  ofkdO3rTIInVztB4HrV6AMW3Pznnnr9K0Ti/e6lxS2JI0Cpuxu I6Cre1WXBIyM8jtUaBWcFM4
+  PcnirO35gvOSMgY61cZrZjcHGWwi7iuCcj+Ee1X9hXaW4GOcnvUSq DtAIYDgEVbRCBubJwcVS
+  kk77EX5unyFQnapVjwfWrigjkHDcHJ5qFVIjD4G0jgkd6srGGm5IU4ye a05+idg93axKCN6kr
+  vBPIFWU2iR8grn7opIIwTtDAgc4qYRESj5DhelJVOgRkh6K5X5uFHT3NWM5 D56ds0kSkCMEdu
+  hqVEEm4ZAb+lNpLV7DhK716CAgAZIXb6ipCSUYfx54I9KcEGdzfMO3vUwUsQdp 9zUOSvciDs7
+  iRhfJAI3AEDOeasEqVZQhB3YzTFXksFOMAjPQ1KuWl5GOetOUrlNSXoBQHaOjcfnU ka5U8YHb
+  PenlOeDnjipowcruCliMAAVkqivoTKWtyNFwrZHy+3rTRlnYpww9RVlk25yehAxTdv8A dGQD0
+  qlO+xoprls9xrMCB8wyf0pCuWHfHenCPMuzGOeuKmSM7kBIZTyfX6VF9GPVOxCvLZPOD27V KE
+  bIJypB4PtTyquWCgkcdOtSohG35WbB4FKLk7hLyIwrGU7umck461OqnIBABxxgUoV15QBhyRxU
+  6eYeoyMfLxRe1rpBza3bsQZQjcfu9TUygsrkKewB7U7CBcbcZOOaeyB9wG4ZYdDilLlRLV43aI
+  TG uPmJ6cD0p21sBODjgcdqnxmPIU8cD6UuTtw2D6cYzQ46FySk1cjCfu8MMgHjFAQlNoGVz1q
+  4qghS VyT2oC4QYVjk5JHalC9rijOMVpoVkUhyQC4x0FWQB5bYG3d1Y/yp+z+JAct/KpnX5Azj
+  bz0NNTaY k4vcr7ZNoyQwHT61NtO44OM/p7U5doVdx5IPSpgCEAGCTxWmluxUdFqyMAhcHO3GK
+  cSfJORkAjp6 U9cFgMH/AGs96fszGV2n1xUzXSwKdyIE7GUjjqPWmMMjoTk8+9TjG7ZyzY5FLt
+  3KNwwR29KXw9Ac bNELITCVOGbsQKaAxkI6AdzVkgliFU7R3pdqm4YlSoOMZp82gcyk7EGNyjc
+  QwxggU0A+aXCYY+tW lTO3b3PXFKEKJwCcVSfvWsVztIgbe23cPy70/Y2CBnPXr0qVfmBVl4Jy
+  MU9IwAR0HbJ6Ucz6IzTa 1kyLYMDncWHTHU0NkAKFxjjOOlP2Ethn4HpSAbnz144BPNNy1CMdd
+  dSsWy43jdj26UhH948jsBUp QtIwxtIPT0pWX94GUHO386huI3Ho0UXjV3ZS5Uk5AzTRGFkJOc
+  dvarGBvxgk7eD6VHs3RKTuDZwc 96J3ve5bTatcpMSrPgD73PFJJHnHG47cjjvVh4w82GO3NN2
+  lmbkbRwvrRGpZak8qbK4RdoPc56U1 Yssc8Lj8an2FWA2kDrmjaSJF7Acgd6qE2nZAoroVzkRh
+  MHryR601lLK/RecdKsbVQhs5BOMZppUD LDoWyM/zpuUn5lUuVN6FFDhsgcKecUOQVODhifu1b
+  KjcSBt9TUJQqRlcE8Z60m1fUUpq2xAQxG4Y yM5yKiY8jcV6YOBirexgvIyvUkVXKhotuNp7E+
+  9J27ExlddyJkzGqA8g4+opgQ+YTuyoOfc1MQm/ axIIXtSIqHDchgp4zS5ujJcXazKnl43NnIL
+  fjTCWBJ+UZzxire0OMgYXnPNRMpZQNpx9KtSuh3Kp QH0BA4Gar/NuJVlK5wAfSr5HAJwQOw6i
+  oiBsJI2nODx1pqL6FOS27lRlj3ZUnntmogqtuyCBwTV4 qBDwPusB7moggxgMApOSKz5o3sDir
+  XKJiGAD/A3B9cVG4zCCAADkZx61oFi2F2NtyeSKg6wfKowD 0I6+9a03fWRn71ym0YEW3AyOp9
+  aq7Au4ckdh6f8A160Xj/dDdnPbNQeSCDuJIz69TVWuro0VmUHC 7AuGG3pVfyQ1xt3YBB4NaDQ
+  7Mt1LHHNRybWK7mULjhhwBUPmbuiZtRdkzLwF2quA5GR9KrvG29lY ckdfStJkJj3BVYLxkVCV
+  dUBxuycnik5a3BN82hQ2c/N8317VAY2JZuMY45q+6b0Jyc+npVRl2Abu F6hu2ayjPmlpv6BNy
+  scWkR3jy2LAnB9quLjesZZX+nGDTY1jLkYZT6E9asDHnMccqeKelryZTjL1 FjYKFB7cYq2u1S
+  u70xn29KYqLlTtPIz06VONznDHCYHUd61cby00RKcXezJgQASANuMgDrU6FimE yVwBmmIMbVy
+  B2JI4qwmVOM454GOoqXZIlOUVoTJhjtY8A4/GrCqC+FPze/NV0CEMNxGfSrKAL8oy W65PaiKT
+  WzE27WluWgxVtnG4kc46+1Wgrn7o4zwT2qqh+QO3UNjOPWrhP7kBWUELzSu001oRy20S J0VVG
+  7JYHoKkjAAdz8pNNjKsFVVZiF69qVmDBAM5x2rSMeZe87lXT0JFXmMs4GB/KnLkRhgxyR2/ Wl
+  2guoxyeeakj27UGQp64PaovJO6Qk3HbUWP7/Xv8ue9TkOzPtA4PpQEBnDg8Dr6GpSPkLA8Yxij
+  nTZq7qwJnYA+SSOwxzUwVlkG44OM/SowMptTjB6mpwoZSTnI7Z7VTb6iWj2H5wDlctjIPpTRyw
+  zn 3xT1RSRklcdc1IFCsGzjI9O9ZxUNbAnbWxXCKThSR3GT0zU5XaVXOWPp3pwX93uCk/NyalR
+  RyWyp BOM9jROS66E8zvoNU4I+Vl55JqULnc/IYniiMeZHuHQHk9jT1x5mARzzjHIp8umxq3ro
+  PXBVQMgj v2qQLu3jBUfWpPLOVw67SvI9akXaRxlm74PWnLTUjpdkIiHnsDkgHIqdFYxFmVcjv
+  ilCsMSLxz1N SspDc4xjJFQotyuLV69CGMkPyOnftineWwkIC5FPKDY69SDSKjFzjKgf3jRbRt
+  sbgl5Cbdy4YEYA BqTaCjIOdvB9alVG8xsLlcYA9akCYIAOecDNPmu97FJx7kYwD042kcdqVc7
+  djfMAOB3p3zbgOPfj rUobLDK9tpx2pRi7aohNX1GgKAoIB9Tj9Kbty6gZHGf/AK1WNoQhscA0
+  9VHILA5746VVkXGViEr8 4VO1WAFADt0xg+1TBQCCRnjOaDGrx/KOSOM9KhN3DmVyodyvlRkg5
+  6cmnsN8gYqykjBx61N5W5lH LEccdTStGQQAhD9GJqrpyDroQfO0ez15/AUpOTt2HINSiLPAzu
+  we9PIIBADZI4/xofI1sNMhKYkB /g6/So1VPMU/MCw/vVcyWhIBDY4IqIgAqMHjP+RUwV2xvfc
+  RP9Y3QkYwaXG+ZW3Acf8A6qGUlAFw CD940Z287d+ey0cvYjldrpjH+/k/dJzSIcHd/CR8pqZQ
+  Cxdl3Hdzjpmoir8jrk4A9PWtFcE00MIA BLN8xP6VEdwZW4wPxqc4Uj5fl7Z7VEyjcSu7BPU9A
+  aUtgcrtXehCQxcE460wuRKqEdRgVOR84Bx0 /P3prYAUYDY6kVCk7lOGlyFolCbm5btUBBVs42
+  qvT6mpMNwwbryBUwAywyCSfyrR3jZkxauVAoK4 bOM8HNC4yybfmPUipNuUAYZOfzoCsZAOAev
+  /ANamo33L+FXW5WZFEhHX0BpxXzHcgDdx19BUzEbi WXBJPHeo0UtJvCsgHBU+lTL4dTOfe5Xd
+  B5pZuMHIUd6iYZbvgkVbkG1TznHf0qFkzI46D+92qeZ7 iSRWcZn2nIbODzTCpGSMHB61YZf3b
+  ZGAO+eTVd4yA2MnPvQk927DVlKxG6jBJwcnk+lQmMFTjJA7 9KlkBWLnO3pinEfIQpHT8qpNtI
+  NU79Cru2odvA6NSMjcBTkY61JtJhDNjlhkAdKeU+UgOFbPJP8A KrejuOL10RVK7IyGI64B9Kj
+  2Ah9wPTKk1O0bFgjnbu6d6btAdhtLc4P+NLRa9yb62bKm3EQDA4xk ketMaM43Y/i6Y5FW2UHG
+  Bmo34PAy9KU7i1asirIGKY7kjgVEQSp3HbjjPqKsSKQTj0BNMPzbsYLd 89Kq+qsVfuQk5YFun
+  QAjioyp2KQDyc1aZThkG3JHSo/4FTBOD8xH6UlG6egnLQolN2dvQDnNVGBC 4AUg9citd1YLjK
+  nJxgcVUkiUQMG4YccU4ya06Cg0tWZ3lLu+X7uM4BqrInzttcH6VpHcG/2T271W aMoF55Kk+9S
+  k3rsaK6bszPkiITczAFuQPQVUMSlPmOQvH1rSePMALZLH37VWkiXJ2HaOvNVzWRPN b1OQ4YLt
+  VTjrxU8cQMiylcDrnHenIvzljjcTgAd6nXIjYN0B5XuKxTd9URHR3S3ARE5wAARmpo1Q hiQ2R
+  j5T1BpQF25DfLwQfapFXDuFBwTz64rW11ZsfM7bCoMHOCTjn61OpUv8yjJ6EdqbyoI5JPPW pl
+  GG46EdamMW90Wo6PUkCAN/CBnJOKsKp2fKC7dGqNYgmCT1NXIwPIC9VPP4+lVTcWmupM5K2jJI
+  1O1QBwTnj6VZjj2jcWyccj3qJUwqYJXnvVlVUBSCBk9D2qV7quZubY5RkEDIAHGKlVeMbSpzzU
+  aq AQc4GOBVhAH4JwelP3+UpxaW+g+NldSDw+MEU5QWlBAx2xjrTEVftDY5I61Nyr5IJPt0pcj
+  TLj7z 0JkY45IBI6YqQr8gHUDpxUaIABtB5PP1q7tGPccYNS0oscFyvQYnzH5cZPTFTKORwRxw
+  PWkC5BOM g8DFSrncFPUcim1bUUnfW4g+VMhSc9c1IY8y7ucA9DQY9/GTz3HSplAEOQDknvT3W
+  gruyY1FyhLn AHIx61LtBjDf7XNNCtySMZ6gU5cnae/92okubqNJLYkCbVIGMY605OeSAPl60r
+  DYM9MYxzTwMvgH APX0ojZ7scZN6MlVCyKXGAv4VKFUSsSQBnK+4qJA7Bg2Qo6L6fWrqrGdxUb
+  l449KblyqzYWdvIjV RvwPnDcn8KmweODgccinEYlAwMdqUoTIoLdOabWt2Jt3syHbmTcOucED
+  uaVtoVQ3BPGTUxjIz6hs 5qTblG+Xjd+lU5K1xrTV7FYJljlzgHOe1TbeSVVs49e9O4JcsMKT0
+  FSKQrjB4789awblbRAoys2Q KSrD5Tk8gnpVhUAKkLuGCTShN69yw6Uqn5ioBU9+atJuzsN2+H
+  uC5VyzggehqVETaBtzz69RTkiM krPj5SvQ84qRlOQxI2jsKeknbqNWTsmO2AH3HamfMpXcBnG
+  Tx0qTkwED0zn0pu3OCRnjinFa6kNN qzDsc8DI5pwB35Jy3b3p3BTIBA6nNAQl+CM96hrW76By
+  WtYjA+YKpw3XB600LIsvIOO4P6VM0Yxu AO49TT8HA7GqUU3ce22hCMkDPDdzSYUjef8AgIqXy
+  y2Rnae9DR7EHoOopJR72BRs1Z7kecJxt56i onGEOB8zDtUyoAvBOKQ7BKvXJAzzTh1sCa2K4f
+  BVf4R0A/nTF5OC+M9qtMIyWO3B6A1CF3bcA8Lx xThNrQJLaxExHmE9RxgetM25cAErg8ZqfGE
+  IYYbFRH5SgK5yOPXNTzeYN3diEjAOBnLcYprIRIVY YGOtT7S67sYYjcaZ5TYJIOc4BpJWepLT
+  tbYYRmIKFI3dKhIw6jnuM9quKH/dgjgc5xUG3Mm/rk/d x0q0+bQtVJR0RFyGVSQcDgUMAXkJU
+  gk5OP5VPtDOcckHtUcyEoSAVJPA9Kq8X6kc3NIr7cyAMDgj jHtRkrICSAT7VMuPkJyG757Gkb
+  PJK5J6YpO70YSd20VHUvHu3AnPboajbCOFOArHBq2w27VAHTp6 UEApjAPf6VU2r2HotEUnU7m
+  Kjfnt6VGwcnbt2kDnjp71aIDMVOdxOQe2Ka6kZ+916VLp3KXLzLuZ +Nr84Y+9H7sx4XhzyRVn
+  y9z/ACglsY3f1qLYBDjGCT1o91oLK+4xwTGGjAbPUCoMZi+YgOOntUxy nVie4x2pzKu7jJDHG
+  aHZLREu97FSSNmdVLDdnAIFMbchCEAsBzVqRS0eSOfT+tRTZMTv6kA4pqVk KMddSmR8wDZwR8
+  xqPgP0zxx71ZCKpyQcBe571AB1yRuHFVKSauXBLUjI5BK4Lc4PVaj2neVcbV6H AqVVLMcfNxy
+  TTSDuBJ68GpgrPUma7kZCMDjIG7hqgWT5dp5OCOP51aZVdODx6VF5ahSxI3H5sY7V cWr+YrNL
+  QiK8DeOT0AqHY2/Zt4znJqwSx9MY4JFRYYHrhRyfrR7NpPQUm7FKdRvHXKjk+tVH+b7o IbsfQ
+  VpMv7sBgOhzkVAy/IW4AwO1EZpoqLcUrGbJBjb1YdiOlVJUGG2Ho2AOta7BQW6nB5qjKWZA Rh
+  cHJOOvvSjNteRK35n+Jx2zLkFgB1x3qwvlvCSDknuPT3pkKctv+ZmH5VbWFfKP8K+1S3Hlu0VT
+  UktBiKcxqMHaMfWpxuMhJGMcYNBVdyYyScZ9qlCjJOfl3Zya0m9UiHT1fMJ/GyhSnOSakRQck5
+  JH 8IPSnCIhiSmSGxmpYgysWABOemKlRvKyKUktiVUVlIySOuM81NHGpmj+96gg96EXeylDjHH
+  Tp9au Ip3cYP0p8zQRu9ESAHY2SBhh19KeFbaVHzKCOlNwjxkMD8w5we9Tqu3Z8hPfg011E2kt
+  h3lg4JyA Dz7VKuQcfLkHPPYUKMyZJ2jqM96kCq4BAZiTyajTVMvVR1YKpbBHIz27VaWMKvJye
+  3vUIiKybRke v+NWgCFBPJxzTlJP4SVGSSdxIwjOWyRnselWE+Y8fNkcUxAxVTtB+g6Va8sYIT
+  txnNRdKSTKbYCJ ieMrgjgVKsZYNjIG7IzzRGDu4BG08GpVXMiu+evABpzk2LXdkUauHOCMHqK
+  mUbZguCR0JPalYEKw PUdcCnxgNEzEYI9TWU00hWd7oQJ8mCc4HBqQIqKpzu29/Q0oBOFwATzU
+  xBCMduOeRUtONrlNydk7 EcQDIykEtnj29qmaPJXAK4680KOFbBJI7elPCbo1wduFJwfSq91dC
+  rPo7Eq/eUBflxxUwABOcgf1 pqACFN5UsecipkAMg3Hg88UoyVr2JVm7Dgm9t3UEZFP2hFBJxu
+  55pFB3gKrFgOlPKMV3N3PB7VTv a7GoJvYRow+5lYDP5mlXIZOp4pdjBFUtlRnGKkERBBIbnt3
+  FO91ua83LFJ7BtT5jwBnFO2jPsB19 KkCkgLtO3GfrSgEvgkAEdMcmkrcy0FGzW+gxQCoKgtzj
+  ilwROARySefapgm04UfNnnFKfvqSvXpU 3voTe70/EWNZd3P44qRwfOAAHPUYpEVmkYDpnjH6i
+  pwrB9nU4x9KmWjJu1Ig8vLAFTg9DninKCV2 gd+CalUk8A/MTmpFIbKt1rbm01RTjK9yAAyOfu
+  qRwB604ghAuMMcGpU6kDGD3I5FIqfMWJwVOMno aj3t0hTjeRCsZXJbGPrSYPmYP3SMZqcbiow
+  uQvf8aUhCVycEH7o61oovqGlyAwhFzye3JpPvNjnb jg1Ic/eLA59+lIMeaG2nG39ahxa3KbaV
+  2RqpPzbhjBycVEUxGoI5PB561YOzgdCOvvTijMSy8qDk AGs3U5CFzXuykw7DnHQe1NHCHjBzz
+  U+MlQwwCOPemMm98DGM9a1TuhtQZAdzEuudpOee1K8ZIjA4 GcZqVdzPtBG0j06CkMTAbVy2Dk
+  k0ot37F1IrS7IGDrnHQjFOKtt4BJH8qs+WPLKnOexzTFVlQKCc k9x0FJz0uZOloVlGZc5xx0P
+  WmlNrLkH1B9qteWeD0JGaULx1DN2HpVRWliZR5VdbFXauQAOvJwaJ NocgDdnpk/pVp4gTkgjn
+  gd6Ro2YLhQB6nnmnyor3U1YoFPn5ZeuAaYYzgAjLDg81om3KgMSOpxgV EFBfKc84PPSn7ttAU
+  le5RcEsdq/IMc1AEZpGypxnj3960XSQEkqcZwABTGDIuSuG7E96b006im2m upRMYDE49vrTcE
+  5bBzjvVzbwCwJpCpCBQMjHQ1jJNNXHvdGeRj5lznOPlqNVUMhUqX27SDVpk53Y YAVDJEzSDkK
+  DVJSW5KtaxTkwBgEE9+KjI4AwTkVZljjyfm+YDGPemmPKKB/31VruXzqBUGCp2ggA bagkTajD
+  kAt69/SrTnAcFW4P51WkHAbady9vWkpMKl3a5G2DHs7tzioZFbzNoAHPYfrUrhjhlO1Q e9MZR
+  5rLuJY9MdqqTWmolFpX6EGMZAYDuD60MCVBf0z9aTB3KpHzECjDAEMQCtJQ1LuyMhsZUcdO el
+  JsUplWAYDGPUVKcHpjgcj1NLjMK7gFbkkdOKpS+8ht21Kzrh/7xxx6YpOAuOhz0xUuR5YGRkcH
+  im7kGSfWhQbQlq7MpFSZNxOBjvUbkBdxGQTzV0puk44P9KqyREbSMYBPXvWnkJK7V9ikwwrDac
+  no KoyRjA7FupNarRsAxIJdT+BqnIoHJwxNYqd37o2r2OSVAH4GTjC+9SiIlP4hgYPPWiNd77O
+  hJzU6 AeYcnkeveiEWndijKXzGoCHGep5H0qdAAEfHBHBqRFH3mU4fJ4pUA2hdp5Hr0q/djqhR
+  ldXGkjyj jOSct7VYgwfmZl4OD6Gh1Uop2MrVaSNBGcDHPU9DRNqMdQ91LUBh5o3UjgnIq0oKH
+  LL8nG0+ntUK Bvk3LtYrk8dCKuqSQC/3OpBpJ6aIbTi7t7iCE+XkcEHOB6VNGgx0ZVbqSc0Ko8
+  xtpLcYODVlVxDw 2OOQR0qm77j1T1ZGisFYZQgcDAqzHt2lMYx/OnIMrx2qxGAJMbcEc896xem
+  w48sVdoYqc8DIIx15 pQgXDEcHg896kMbs8gzhQePpUmzKkcsMnJPrVQlpZDjo9BYlcxsqgZHT
+  PcetWUXy4+oJzSxx4hQn IYggCp0T5cBSXHOaiSbIkuXYYiK7cH5icnBqQja4KA4VtuMVKNq4b
+  GWyMYqRkPmAg9fmxVySSByf qVwuGJbI78+lPXaTtwMZ/KnbSzgYYnOfrTRw6sw+XoR3xWerRV
+  lF3e5MFKjOw4HJz1p+A8oySccq PX60IPlDBWwTzk9KaquFJXnBwPU1Lbej0Jv5jWB8racx5xg
+  +gqeMkZx82FI3evtTQvmJgttbpzTx Gylg2CMYAFOyvqbuCfUkiX92rZGO6+lW1TIwQduetQxD
+  5WVvbOfSreAm0cgYzyaOfo2TNxi9NRMv t447fWpduIvusefu55FQ5YMDj3z1Bq0qsTubPsBVq
+  KWz0Ks762EjjBwwPc5zUvJcAggAZ3HtQPvk 8BfSpQrllHXNS7bszlve43nb6+9TKp2EgBsDOA
+  OaRYz9OM4zV+9vZb65SaZY1YRqg8pNowPp396i bk9LaepajzPRmfg5BKnlue1TMmZcAHJ5APp
+  T+NoCdz370pG1x/Hx2pqLbuK1t9xgRkXeCCCecVN5 b+Q/UH1oQMEVWUk54o2s0yqrMuBg5PU1
+  pZ6CteWoKo3biMBTyaVTmMhyFOPSgxs0PzffHWpApxyM Y4FTy8q3KlFNakQiJYdQQeT/ACqRB
+  hQuQMg4BGakAyoyOB3po5OSmMjr7VTTb1Rnz31vcRQNmRyp 600xqJ+clgORnmpAMoB1Q8ijaD
+  8zHH+16099UNR1uV2QbuhHp/jQFXaC/BwenFWFUFJG5PHY1Hsd WBbgEdetZ7p3ZUrIiCEZC9y
+  MsfaoWOZCMhc9fSrbgEsGVjxke9QDbnLAL8vfvRG27VxKz2IlTOD0 471GUUkHIABq0QGTABPP
+  H+FIsW4LkHimhQ5lqyExlV29s5yKcFcENwDg9qlGPPK5G0E4HrQYyeit nPXNXSWqvsZ2VyFjn
+  eo4bIIzT1jTbvY4PYetTGIh8hRtH51IqEE5TaeM0pRXQ1bKeHIPy5xx0p6R F2Bx1HDdqvqik5
+  cbDg/Q0rRhIDg4GOtQ5JWJk/spbmf5Q3Hb3PGaaqDewPYY/Grvl7lDZAyMA4pp UGfnhh7VUUK
+  a7lVe7MM7fWqzrlchcAHJxWg0YGdoJWhYSVK4ySMnPtWqilqNNJ7FBl+fAzzVdoix GQSoHb1r
+  U8k5Axs5PXtUHlPg8fMvUe9C0d0yYyd7GcIct3z6VE8e5dwyW9PSr5+7u2kjdULJzuCk HJyPW
+  p13Zo7bGcVLSsM/MePpUEifeHBx1NX3Tjbzu/vYqCQARAEEsfQ1knF7MUU07FCRG3AhR24x z7
+  1BJGBvYZX6mtArlivO4HNUpPvsCOR3q1JPboHNLmKkoUx7sbiRyAagkGYgeSw4xVvZhRgZbHT2
+  qsy7mIH8XQ0Jx7g+V9SEgDkc+tRBSCHHHPGe9Tsq5ywO7HrUHP8AF8wWko3ZCiloiJgGdjkfSg
+  nI baARxjingMYCP4euMc0mEIB3gY+8K1lBRHzJ6kIUs2NuHz29aCrdcZI4/OnHGVfJPGBj+dO
+  KEqVA IyecnvTSW9ynOzTSGPGViAwCOnHWoXTbg5Hr9atlGVCHXPriq7cI2MZDDnqKE3bUUOaW
+  xHtJTBHI NV5CMgEgHkirjcy/eBJ64qPYm8qCG5y2am9tQta6KbqcbsZz1xWdKFXLMpJA4wa0Z
+  G/dfM2FJzmq L7TMSGAX0PUirjFv3rCglbU5gI4j4252nGBT448oqgdfzpVJVmABGehNTopfGC
+  MjP41Ki0op7E87 WiSBFILqc8HP41Oqh2OGAOMn61AgKguMlu/vVqMAptcYz07YFU6cbrW4NRX
+  QkWPFxgkMAD+NTeSS BlwOfTpTWXaVyCX3Yx7VaUOzZUcZwKlT00exMVZjfKZ12hup/MVaUMAd
+  zKADjBFLs4I6joAvUmpo 1fG35ST1z0xVJprc0cm9LhkqAFAzj045qZIzu5OSq80KAQC4LKRzg
+  4xU6D98Aepx1pN8q0H7vKHl k7XBwAatJy24gknoaiWMMw56enSrMarGuQ3bvzUWctLi0+YBVz
+  xn73Q9c1LHDlgTnGfyqeJN5DYA 559qkCncBjAPT3odrWW5MXK46OPERJIJAyfzqXy2EkpJxkZ
+  XHepY9oX58cHp6VJtVwMkjBznPWiD aRUpcysiDYcAYIPoamcr5XzIyYbrUwUljkg5GcntTfvb
+  gAGX3qGtSoK2+xERgccseSSOBTNi7Mry QclR71Y45DEc+1Q+VsACFsk561UU+4rx6gpIifeQA
+  TwKQ7vOKFW2Zxu9Klk+cfLjrxxSkseA3XkV KvLYPdWohUxlVGNucZqXbk5GB3570iYYDJyOuK
+  lUZJwQDnafapd47sFG7HwuGcHIIJqYrtUEjLHr UIj+f3x0FWAm2QJnIA7mhRb1iNQSJljUgFs
+  57c1Y2ttByB/eqCIh5VHXvVlQFOfU80Wa3KkmtEOw fLCLHjA9OvNSiP7rbjkjkUcHAwSMcVYV
+  Ay7gu0k8Amna7uhX8hnlqNp5zjBz0zT/AC+eF6DvUywn eRn5QevvTzFzuAI7EUdRwik73KqJw
+  OwBxzUm3EXylQcd6mCOFYcD5snPenhVVtxIJJ5HoablO+hT ty2GBGf72MZyGFBDbUyoGfQVaA
+  O3gAY60hVyCSFDdv8AGjfcmMYy1IgvGADnPfvUvlZbHY5PFAUF l3blyM9elLsbJJyOMGs5pqV
+  0LlTVrkBQNnnDdMZpWGCB9457DrUoQE4OOBTdjbeBuOeNv86pK8bi Sa0uR45XgAE8CkIBf7wH
+  J4x0q0wIAUqOnJxTAozyRhTx70lrrsDepDtG0IWGc9cU0g+bjBbH86nO 3eSRnB4pAp39MN/Oh
+  WauKz2ZAc4XbjJ5INQFQxOEJIHGKuH74yVB9cVGw67APbFXDUFHl1KoG+P7 rEg9qeiHYeMHPe
+  pwhRctyp7U8gKv3TkcDjrmhtDjdvQhaLMZKDcQc5pyIVZOOCMkGpEVs9CFxyB6 1KxJcFQMdia
+  SvYSp3umRffBKr3wMVIsMjElmGSMkY6elShQsauDkk8gdKlCKWGCSV6gd6Iu2liL8 qsVvLDqF
+  79/rR5DNK2WBAGDnpVsR8YwQcdaapYAZIPvjrT1GpaalIoVwB0757U7ZldxXqcVa2jyv xxz3p
+  AoVsO6sMZzVSiujLeq0KjRfMQAVPoaaAWHVSPbg1awAz91PWo9i7cdT1wKztzego766FYqd 2W
+  bPrxUZRSd24sOTnNWNowzFSOw5poT5cDA7E1qlrvYTSKDoVL7cbcA59feqzLuwWyfpxmr5DZIT
+  BUdPpUUiZB2kYbr7VMnfdjaa3RmvtbHynHtVSRFwDtJwOMelakiquFIKkjqaqSg+Ruxls4z2xU
+  cz a0KppddTKnD4QqOW6VVmQguA3zZwR71pN0wc5zwKqug2sfmOTkVV7eQNqKM6XIjBHBIxVdg
+  Rkg5I 4Aq267gcsCR0A71Ds3SMWXaCRn61T1WoorS7Km1uMjGBTTwhJXBPXjrUroVbuU3du9I+
+  Fl6qeMjN Du3ZA4EDDahYnAI6U0xlWOFGcDPvT8HY2c7u2akWMeXnlznrmqlLRGclGG/UrBAXY
+  N6jgdqD8vQE 89TUmAXYchup96UHjdkYA446U1d9DW15e9sNyfNAxkE/pUPAOBjGcgYqdchieK
+  YynfkjOWolG8bh F2TRXIJyAueM8VAAAzNnDn9assA4OzcSc57Ux1VoY/UDqO1EWuVXFdJGfKo
+  LfN2GMVVdQI/ubsdV xzV+VSCwAPzEduaqyZIZsFfqaqN09xt30RzGAJMLjG3gVLGEEMW/qD1F
+  RjcAHI/H1qYHLsevoPT1 p662MG4t2JT8+CFIUHjijZnag5GOfpUwLBlxjaVwvFCEHIzyp61nG
+  pNPRF35rpIlJbJPbjFWowRG Cx6dPU1WBLI2Msc1OoHkDJI9vSnyXVy46R2LCKwk4X5BjBqym4
+  Lk/Xiod6MwyG4461OAw5ByB2NK K0ZEo/zDtuUKg7geRg1ZjU+WeMgcD1piA+YdwGVHHFTRjbt
+  ZeMjBz70NN2Vxuy2JwihcNkEdcd81 OBGG+ZgMdSKjjyVxtYe5HWrJVMJ0PsOtTfQbtzWHqFWJ
+  eCDmrCK0sakcbT0oVB5TKcbhknNWYubf BOCcEKOopw1iDVtLD0QmIsANucY71Kik4yuec0kSF
+  pHboB2p20E919M1K6hyXYIvzcZYHoe30pyx gBU+73P0qZFORtIyD0ppVmlbAANKTb6g1LoyB+
+  5Tpu7imMjYJzyeRgdqtDhcFc4HT0ppHHHzZ6Yq nIlS0sQlSqk8YA/Ko/KbKMCPKUZNWCreWpz
+  kEcj3pVLbSqr94c8dKhKUXdMcFf0GgDfgsPy6VIqj YAx9/rS7dibQCQeckU5fvBQBnPU1HNdm
+  junoPCrvycBck4FTKrFchhgnqeabG2VZTjgYB9aspGdg A9OnaiLktBx3HJGFB44z1xViNCbgF
+  VO0DAz2poBOAAcdqtqTvUMM5AB7Uc0ktyLvcUJ83TcAOcVM qAzEnK46Aninhf3hO3p39akZN8
+  vPyj1p01Lc0i9fUaqkZ6lieBUyBgpyj+1SxodqrjBweT3qVIiQ OowepqrX1sZyiQ4byk2jnHc
+  U5YlLtvGT2IqYxsYiFOSDg0oRliA4PPPrTXky3tqRIMlh2xT3Rtqc YGOM09FDOF6Lg80/6AkA
+  49aOV7ia1uV9u9tv3mI5pAjE/N0I5qwvysTlVOeQe9GwBiOpxnFG2iJc tdNCHywGIJGDxkd6Z
+  tZQWC7WHBH1qyCAjAcjOeabyMbs5I5JrJxaditdSEqdoUDH071GinkjDDIz kdasBVY/KrA4OM
+  96Ty+VxnC9MGr5rKxMo2SZAIyMgDqeSe1OdSBwuQDwKslQfYAdD1pgjxyTwOAK FZopR01Kyod
+  58wbe3P8AOoWAVioBGF5Iq+yhYMN14yfemsmC3HOaIvleocz3K+C2FI4zmnYJjAIz /M1KEIVe
+  Rzxn0p6ALJubOBkCqnZvTUGVwu1WDKQM+vNIFO8hmG32FW3BHJwf6UhwULdeelLmsiYu T6EeB
+  nIDA+hFCBstg/NiplT5iMb365BqTO2MkDDg5YU4zsu4KWpCBzywGCM5qUL83IBUDjFOdV3D 5h
+  83r2qMKu9s8jtg0KSlqx8rlqgKl5QUX5TyM1HtGWPAOec1OMoMZIA6AdqaQoRjz9aLvoK1tyP5
+  JeGwvFQuqqgIP3jx7VPgHAPOB2ppTn5iMKck4qnZE31K7Rgjbn5aidh5RIyGLVaPKjjqQTgVA4
+  GT gYPai19Ane5WkwUO3GSvPvVUjEOHDZP3jVzcykEKHGeRUEjP8oIzlc5os27Ar7dDPlUBDyT
+  gd6qh Q4DE5GOlaLKpBBbnghu2KpOGbG3AwcYx1pabLc1im42RVZQMAfMQccVRdWBPB4/WtHcC
+  zYwMYxkV UcNhgSFPv1NRJyi1clU0tDPkj+QsoxjoRVKUtvywAbPKitBowoIfIBHFQSKu7JUlg
+  vBFVfTVaj52 ml0KZA8rHUkVWdNx4OCSO1XnRwm47QPYVHngYUgjgn6UczW24Nte8iu0P7/ZnO
+  By3bNI5bdtVtuD gD2qVl3KnUk54B5qEI7H5R8o4IPWq1t7zH5takYBUMAQ3PIApuTsG5SOMmp
+  2iCsSScDrTCcp9OmK 0g7aolct9iONgQoZSM8g4psgCjG8D0OamyA2DgAdD601gwTGwnjk1Upa
+  3JUnFpWKyjM2Mjj0HFQs NpUNkGrMqb4s459qrEMgALDnnkVKSfW7E3e8iGQ5Dbhxk45qkcM+5
+  RkYzzVxtxJBxVSRvLj27eB2 xzTUWjVL3fM5bdu5cnGOmasjBb5Qc/xH0qoqhQyuQ4H3CKsJIq
+  hVJwT61PNFrTc5X8PmTqHDFj8w xxjvUy/eUkblzVbcywkEgNnj3qdJGdQw2/THH1ocnbUtO9r
+  lsMoBKgc9DntU0aYLfOOeTxVdFVnC qdwPX2q2VUZ/hI6AnNSk0xNJK1ydAqbSRxyOKmjznAO5
+  umPSqy8sMg7Satp/ruy55zitG0tLjUep MnDHnBJwc1aViCxwuMDjFVwvPykEnnpVmI/MOMA9c
+  9jUTit3qhuS5kmizHubO4AAHGMVJ5W6bI4x 1zTVA8zkkDA6dc1PuYBduGDdRilG0dEaJuOqWp
+  biIZUyMnGAR3FWlCrKMc5HNQxJt5bk9QasY3Ho cHr60Kd2Qvi2FRMLnc3XkZqwi55JyAM9OlM
+  wwwCNzZxVpcKi5BII5NJspxaIwGCAhSvzCkO4NIcY GR19anZQ0eAxGeQKiAwzNznPSmttUSo6
+  7jDhkI54HNR5CDBU4PerI3BMHBweOKMA4LY9MGlJ8ysy tnsVuSS33jjj0pVA8oZBJ6ilB2DC4
+  INOB/eqTzgHJ7VlPRWG4ybsNwwXJztqVI1eQnngc+9NJY8h SwUZwKmjBUHaDu6Gm9tB8slG1h
+  8cYAVlIPHI9KsqpUsRjnqPSkAywBG0Hmp40Y4YDA60r31Y7skT HIPHNWSmTnBOaIugAUk44Pr
+  VgKCRnuOT2FF1zaISs2EasEAYc/xfWrKAZGcHmnImX5yVx2qcLjrg 8gCtZVL6Gb3uMjAMmATk
+  dasAMFORwMjNKoDBmA4AxkCngcKAwx1JxUX1vsauOl0AjcL1JJ9KFG0Y bIXvkVJltz5I+Xoe2
+  KeD8/zY2gdxQrtXHPoQhQWODz2o34l+bgjqcdak48wgKeRUY5yx6elVGCl8 RUrdxMKzNn74pp
+  B5ZSCdv+RUu1g7BsEY5I44p4iw5wGOzGMd6TcY7shuKGbRtweh6cUzZuBxk461 OVBUNyAOcUg
+  VA+TwGHOegqVKz0E3G2pA0Z8xSMgEYBpmwGIBQwYnAFWvLyowR14B707YWkDKBn17 VV0yeezv
+  cg8vJUhcr60YViQcepx2qTBE2F+YL05qdkUpv4B6ips2y29SiYmfaoOR700pubGCSPvE VcyNm
+  Rgtn8hQyhWyMHjPHen72w9ehRUMp4AGBzTtpJZccfSrZ2luAWyM8DrUQD4JOeveq3VxWSaZ Bz
+  zuXk80pUByGyAeKsnHA3AknnjimOiqAyEhQO/NRe+yFomRB9qrtBye9OC7lc8nJAFLwpC53ev1
+  qVscgpgZ7etVDcmWm5HgkHLKoPHIpiqozk5Iz0p2wZY88nIBpM7fw4OKculgcLIUlQvUMM8Uxs
+  bQ B0J6UhHXdxj7tHCt8x68jntUR0eg9JdAfaCSDjB/OmMOuDkdzUoG5GJI44qDJDY4xyTQr7d
+  Qcbaj PmKEH2PSmO373joD1p5kyDzk9uKiOdhGCxYZAHatNiZJW0KsknynYnUHr2qEhfKcsSTg
+  Cp8DhQck dRUDhxk4BBIzipUo27AovRFaR1wV2NtJ49qruy4wePWrxUkglehPJPBqiVLMpdG+b
+  7px6U5W0sSr NWuVSueR8ueSTVaVSJDjGR2PXFWn+8wY8fyqq4ypK5OTyc9qvlu9TXZ3KjKTzz
+  z0qu4ZigVSMAZP rVp0G9gAwBxwarMWG0cHA/GsrXbS/EpQbTdyuyHYp2tg9vaoVAOBwT3qSQM
+  yr1X09qjbakxIBAzx n9avl7mKg2vQYygTIRkgdcetN2/OCGUDrjH60/c+8k4BPI96coACse+d
+  wqm31HV5lYjZSWzncoFQ uA+8N8oB6nip+fLZsgI3AJpjnEm3cuTgn2qU+wlK1m27orMCFUBSx
+  x+VNbMkZDvhQw6cVZbAcAq3 3aikAyApHyjn1NUpN6JWDmTXvIrOjAMVO4KePeo5DuAyMjPQda
+  mLAqSoJBODz+tVmz5WBx6moa7h K60KrszHgcjrVd0weeHHUE9amcMMAAnjPFR7gG3OTuz3rdt
+  pJi5mk9NDjY1Ckclhk4qXc2NyqDt9 qrhpRMWG0fUVKoLBN5Kk4wKiCVtWh6W1ZIrSyFQw9uB6
+  1cVNiYPHTr61WCDBJfkn5cfrVtGVsDna RkUpaPRFqXRLQtRYCAMRz3FTRsr8569/Wq4XbsBwy
+  Afw9asqMOjDHoBRrvuQ5R5diYKckndhTxir kYJfBPbA9qpBf3ipuIx6981eiwG69u9DslqSk7
+  E8ZXcSf4atxIoB5OOvTvVVQQdv97HarUZ+RgCB kd6Tu9i7K9rFkDKqGJyeQfWrUa4b1I6jv9a
+  rK24qehA/KrW3d1ID4AIHem22yox0szSiI81iFBIG M468VYRfnXBHIyQfWqUHAByx46DrV5cG
+  3yMdce9Q6coshLWxajUAZHJx6Ui8rypIJp4+VVLNjIwf rSlHVSgOQeh9Kuj5lJ20FH3nIxnPH
+  HQU+OMAgbx170mweuOak2bHBYnJPWsp2u9SddiCQAyqOCQM getRyBW4UFcdfxq2RkhsDHqKjc
+  BztIyM8047JNGvw6opjG3aMEr0p467SAwJ6etTCNR04zR067Sf aiatr1BSSV+hCissnB6jlT1
+  xVuLcVCYBJ6eppoXBUgdanjC5zv29xU3VtiZrm1QqowYHGOO4zVmM MiNkYAIH1qOMLvwWOSep
+  q2MK23IDDgZp8y2aKtZEkZBwRkgdhU678BAvPeo1Q7hhGUnnHtWiqsij GDgUKTWwJxWlhY0Uq
+  ufl9asKijhQTzknPFRhTjnk+1Tx8NnaQQO1G+7EpNdAC4fKjqeRTzy7FlAz 0FSMoDhiflJ5FI
+  yoZFzk4HUVSSkwbjfmeg4YECqy+5NOIBU4GfWgKwiOG3YNOLED5lJ4OQDUcqWy CPLuiLYdvA2
+  +uaCoD9Rn0xUm7jHQ459qYUYliGUn2PJptoq4m1TgnJLdvSpNrA5UbgBg4pyYwO2R wTQEGeG3
+  AHHB70tGzPnSViHJdQudqmjywAOSeOD2qUtiYkqRxT02lVyeAMe5qrS3aNZaLyGKpEee G7ChE
+  lckMdo7YFTortIG424x0p4HBA7fr7VHMldEvV6WINiqAARuxz7+9KBxlVJI7VYSIPg9Oxpw T5
+  SF7HiiTRL1ZXEZZCSoUHpTNjM53AD+oq0uPKAz17UeUcdOn6ZpN2dmVJJPVFOOPapY7d2etMZN
+  x6g55NWyCpyB8gqJl3AsMD1HcVUVuwk9SuF8sZUA7h82e1MJAZQG3ADk4q75bmMg7QD047VWZd
+  pb ONhPFQnd2QWTYwEmMFgHOeMDt60jH5G2jvnHvTmXLjnGeDjvRsweOctiqtswirDQDu+cgHr
+  UDKPM IU85qUgrGAw7kdaXaSm4FQT7d6ak4vUhy1uRKocc5ODUe07sEEkH5cmpTGynIdeOuPWm
+  kqOW6ZOM VUXqW72uMDsuflBye3Smg5U9h796kC/IvB55qJicjAx6ZqHHrbUl6sj2YZm3A/3eK
+  h+UPnIyM5NT uGB7HsDjrUL8IitsPckU0iZOxXZ/vBRlgM8dSarNllBww4yee9Wn3NI23A4yOK
+  idiJFBHUdaFo9E XIqtln28mPqaqPvxkk+w7c1eGwyDaedvIzUMoIySc5I6DpTcrbg5JOyM9lG
+  7aMbcYyahZG3ZwVxV mYMQfmG44NVnL7MZ4GOPem17oc7lqyo4PlhmPJPTvUG0AvjnuAetWJFb
+  eCQWAOKrudr5b5cjj6UO zVjRp21e5AyDb5jbunK1FIC8KnGORnI6VZO10DFiD2FRE7l2nqR0q
+  rS00MG0upVIyhZM5AOc0pGY 9+04IyamG0M3XpyMd6Yy/uxgNucDPoKTTk7GvM7bkW0AxlsgDj
+  BNI4ILYVSfUjpVoRAHghiOlMyN r7sY5FVdOWphKzd2yg24yE5zkYxUbxnJIzx0NWnQhAcgnHG
+  B1FNO4xkk4XGF9aG23p0HzRVuUqMC FIGM9yRxWczvgkD25H51fdWWQ5yQx6elQOVKkAAkrkjH
+  elGV9C9tTPJJU/KzkH17VCwBRhkjPTNX MAnBBGRyAarSZ2hgQMcEYpxbb1By10RxAR2DHaw2n
+  vU4wEDclx0FRZPnbVHB65NSr/q/mwSPSnaa 3WhnGMpOxIkYcqSSM9BVpOAAgyqnGfWokRTJye
+  /SrUaFM9CSc1LlZ2RUn/Kx6D5k3E46H1q2QTMv zDnt6UyPY0jZHfr6U9UbILevH0rSTbI5dUi
+  wIxuDHd+BqyobrjPNQxbVcjdk+/NTICrBdrgE5JJr K8upspa7k6glsAkHHFXUXDIQM4B/Gq0b
+  KNvRmHJx9atBQNrBjgHOParcrWZLm5aNXLShCFYkg55A q6FGdzHn+9VWMANuySAMgetXI+Y8k
+  YO4Yz6U7q17ii2mWkAVVbgn1FX0j4Udx6iqaj7gHJ3VeiBc 5OSOq4qJau6HfXUsIw2gn5yemO
+  lWUzk4Xgd6gj4QYUdenpVgYJYsTszwB1py2JaSe4w/cGfv/Snq uSpY8gZ5HWpAAyBipCA85PW
+  m8Ky7eR79qhtWdhJdLDFUsxIZcdcYoCnBJUkAcU/bmQtwVHXbQwBD Ku7pwM1Tir6s0TuiLAJw
+  x6DnFRNwTwc9qsBMQvkEnIwaTaRJkjGBSja9nqJct9AHysw4J7HHFSIm HHTBHFNXAycZOOpNW
+  YwrsgByMZIrOTsrlNW+YRhUm5G8Hgn3qxg7iDgcYyajYbQchWXqNvepskjb jt8opxSdmPVsmj
+  JyAWB9eOlaKKTlgRgcEVSjGcHaScfSr6qCPQEZNXNx6E3k1ZE4BXBYZHQU7LC4 BYZBPUUiqRG
+  AgOc5INToEIGckg4UZ6VKskNNpa7j2GSBjjGQaYNocEhvb3pyZUYLAH3puO+B6del LZgiTIEn
+  Q7j2NJ87yMT0xmmgsOh4A5yOTTgrErjOP50k3uUoqIMGRiw+6Rkk01sja5xu64p0mTEQ 3AzTC
+  6tHySCOo9adkyYRfVCnJ6nnPrT14kBBAznioYwc7gc46A1KhJcswBXODijW+5vbSyQ/hijb Xb
+  Pp2qVVTzM+vWmqpUZ3LjPSpgqFxjj+9k03boZbxtIMLtAyQc+tSIAYzn5cdz3pCwLYHJ7Yp4AL
+  bW4XPPNZuPViau0mRgMsxKsGHoKmCEkYU8ZIHpT9pU7sYXPFNAdZyVILMc/hVKTa3LsmtBCpOF
+  AA 9OKWVBv2jPIyTmpI4280biAR2oCKfvE49aUY31bM07S1ICi8ZyQOvNQ7EycffPUE9KsnAfB
+  ySTz7 VGUVXbB3MT070WaepSVr3ZWdWCbGJwOgqJjgJ3HHFWnDOw5xyOMUwqMtjHDdT60NpbkS
+  baRX24wQ jYJORTXIwoIwR0+vensjEAjPShozgFCpwDmnyxHBJdSAoG+YZb0GajcELjcAMdKsY
+  /dsCME4Ix2p N2YBjacdeOarm122G073IQqrGzFWwegJpjBSwYKcE8CrBi/dAksuB0NDLk9cY6
+  ij3G7CUnzXuVDu EgBztHTimEDByxBPSrRGXwrDH8xVXYJGAPU84pytYaW5HJneMsCOPzqFhuY
+  p1OcD6VaaLGOMAdjT HCBgWIBApSfREN32Kflny8HOcYB71DJEzHjqBz9atOPnznGDnk9KgYP5
+  Xy/Mfapemty48zXmUplI A2qzNjGRVd1eNRvUirrABn6g8d6pS7i+SSf5E04663G7oqspI3EHn
+  mqzFVG4DGD3qydwbaPSoJSu SvGT2xVySJnLVXKxJLKvzZ5JNV2A8vjax28DFWJOm3BJPTFV8f
+  vOeBSslcE7FfgpknbjtSAF5c4y CPpxVna2SdoIz3FRHaGyM5zx70k09A50lfYiKBo/pwDTQCG
+  yvOGxx2qQ7NuQrZDcinMP3jHGMnkV TSSVyXKzSRE2M7h1PXHemeWrAF+GI4FSbQhAOPahm4x0
+  JPGe1OVraAo2d7laSNsIy8YHQiq0iHLs WAK4wParxU8gsT61CwBWQMM/Lx70SlqiXOy5Uigy7
+  pGLbj0JPaqjhWkbI2kdavHeocYx04NUyjA8 nODyfU04xS2ZTs7vqUnwp3AgfhVaQFXzjapGST
+  V+URhlGeRnI9OKpH51XJ4C8+1XGTetxKz1OGRH XcDx9asx7gCQBuxk8UkSs7Efe+lWYlZRtI3
+  Ang+tSut3sZ36WEVC0W98KCQRjirKFgQMlhu5zTNp Jz2z8q5596sRxEfJyxJyOcmlLk5WaWtH
+  XYcrMJNvUHjA7VaUoyqSTlSAfc1GigTjIx681ZQASKcZ BGcinFWjYVknZDgpzvZhjdgECrfBA
+  ywztzjHeokUgbsrgcnI71IpJVeV3d+KUWzRt3LUWBGDxuPT /CrKhmlIJGR6DqKgQgkbcZA54q
+  ypDMrEgHGc1lZpkJ66Muop2oCQCO/Y1cQfKpPfpz2qpGcBQCNn bPfNaMIUJn5S49appvcce7L
+  MKhSGHIJAq3GoTGc/n0qpHxKp6gnIFX41LMGOMHnFJvuOTV7lvomF GQCSakKkrljnIzxUXcbT
+  nd2FS4G1T/CB07inTTRCg1G6Q4DCZVlYDOOKcf3sYIHJ75oiXfvDEKM8 U4K2xQqkjtVcqva+o
+  S27jUTG49e4pxUKvuRUyjLnbkAg5yKaDlScZGCOnNZ2uaJ63IGTjklh/CV7 077rDb1I5BpzKT
+  EqnjIwKbtZVGeDuwM85qVDogatuAWNwDnHHSnLgElTtBPODTiCGXJUIO+KcQuM cHb7VUlbR7C
+  +1zE6BSGGak27skZHGefamAEFdoABGcVJEH5LE4PAzUpMaSaumWYlZpc89KuqSpxg A9PpVeKN
+  hGfmJz6VOi5JwNpB6k048vQNd2WflKhs8qe3enqh3lhwe5ojAEahirDHHvQFY4YHK45q k7PQN
+  3qSMVyCoJbvzQ5yFDEKew70oAIBIPB4pCSc5HIPU0nFNXQknsMD4O1OWxRhiAd4JHtSqB5i j2
+  60joAcp1+vWqtdGrS5rCgk5JwfrSkArzgE0iqQ5I5AGKcuHwOgrFtqW4SsnoMUjzAVOTnpUyqG
+  JwCPX61EmFbI5yOanVVZCyMc5zyf0qpRd7kys3cVIyRyGGeetTlcgEYz1I9qSPlQ3HTgCpQPl4
+  wW 6fhQpS26GbkubXoL1YN1xx0p2wALngE8jv8AWm/MHx3qwqq0ZKZLdDROTXU21vZAflwFcNg
+  88dKV hsdWxkbevvSbEaIspJYHOD3FO/e+YMKOnSiLu9EDbuG0GNmw27sD3poG3g5IzkAU8OPL
+  Kty9O2bc EDBxnk1V7XQa21GsmSrE4JPP0qDy/wB4SMjHUntU+zd1O0DjOaTYFLMx9MUrPl3Mp
+  S7MpucDLDq2 aaykJkcgmp9mVbODgcH2qEAhMA9+/an00NY2IAJGyCpOc5x2oZWBOVJ+Xip1yM
+  jIBPrTWLAgsCeM ZqVo7ESqW2RXwfLz91elRONuBjIq0yfNkg8DpTCAxUbWHGDRfUV0lcjHCjq
+  fQGmhOehJY1JJuWba vX0IzTdpI3Ecn+VDWgOOt9rkBB5JGDjBqLBRzgEsB1q2R8nIO4+1NbJb
+  aCuc8gindsV1LS5TzkkS McgVE8fI+Uk9B6GrLIxkZuFxycimFQYyRnccY5q7NLRjjZ7FNlLFt
+  q4GOaruDtUc5A4Aq0V4JAY5 zxUcnyxqXAXAqLaA+XoUXHOTgMB0qjJkSABgcDpjmtGQ5XcF5I
+  wapgNuycZHBJq1dxLc5LqUmH38 kBic1E5LNkgA1cypUsu0MQeTVNwBGScnPIqJxu1czTUpabl
+  BlIkKkcN055qHC72KqST2PNWySGJ6 ZHGRUJQEHBye+PSteW6syp9rkZHyg7wdvHHeowu5VB+X
+  6npUhIJxyF7+xpm3EuQS6Zx71PK72bJj OVmhNuEO5gR600xksfm3dKlUAsVwCucj3oCBo22sR
+  g0c043JipRd0MCoVBAbKnp61CoBlJ6kt0qx 5alzgkY5+tMkTanyDJ3ZOPXFOD08hyV3vqViWD
+  MMADtUAAMbnDcNzV4gBFU/ewMmq7HAYdM0SfMy HorWKTJukZ+QO/1qlInPJBbqeKuurByM4H8
+  6glRfMwW249aXO47Mr2fZmcY/nbcpJbuKptxPg4K4 xj3rSk3IWwQwHT2qhIrlCRg554HSrU0x
+  p6WbOTRcKu0jp2p4R3dQWGcZwBjFNRW81t3HHIFW/LO1 WBz+FVCavqzBSEKuBlyBsOOPepVGE
+  B3HjgGlBA2ZXI6kZ61MFOSI+M8gHtUxkn0L1vqCLgjcCy9R irMbbhnAwTUfV1Gff6VMgJjOSM
+  54IpuF1sXZrccBhsn0xU6hWCtnPHWmqNxIALHpx3qVFAAUgg9R T0tpuVa7J9gRMqu7ng1cjBf
+  kId+MYqBcjb0VcdxVpEODIWAHQYqOawrW1ZcjRvKAPQfdJFXIlKx9 MYPaq0R/dBScqOM/yq6u
+  4qQDjP3ie1SnKXkTF9WW4wjELnHynbmrUKu6Zx04OPWqgTJ3dyOFFWEY jOcnHUDvRGBV7rQux
+  qI5h1IFSnIQHI46+hqDkhSowD1zUysFbOM9unQ1ST1ZKvfclU7HyTtB7GpT kP8AKcHGc9qiLo
+  8Y5B569Kk+UF9z5AXFZ6PdGkeZvTQAoZSQ3OOMVKoHA+YZ96jXbs4buB0p+AFT HT1zV811a5k
+  m0mMdVDKc5YcAf1pz7gABjOeR60qjdIWOGbOOBUgHO0927dai7RbnZLuQqp8ojg5P GR0qRVzk
+  cYByPWgrmRuRweacOH9QAMkUpO6Ls2rdRyZaQEHoOKurlmxjKkn8KrxjlvlYgAbWHcVO AQ27J
+  UEZxUyV3oQ4q+hOmAo6lhxyasCNkbkbh0z6VXADMVGMjn61bRSF+6SDxgdacUoaopavUkVf lD
+  EEAHGKtqPkLbhtHU4qFVygKhgw65OalQ7WOcFcYx6Vo5NqyCK0EB+Qb+mc8Gnkcnn60KMLuJVu
+  OlPADkAg5xSaT6FNkZjxFuORnktTGyd+CBg8Va2g5zzjpzUAYBSpPKjAx3rNNvVgm5q1iMbg+5
+  iM GnjY0nJxx60vCg7uv8vakCZBCg9eM9vam2xqKd7jesgUY+tSqm0gZKkN/OmquJV5+cE1MQS
+  MBWIB 5x3o5ul7FbpxFO8QFRjcDgEDFTAd8980i8QscHr370oYttIwfUY6VUddzLWyViQEuwcL
+  x0x71MIg sg3NhvQVAMEDBBqYN+93MpB6VN0ti32RIVO9FXovBNOZk8rvjOajDHL4VmPfnpQAo
+  CyNknqc0Nu+ hDTtqA2vIxHy9+e9SM7PGGQY9vaoWc7vkA2njHpT2J4weh4IpN6mjSVhCwNtzk
+  c8imhwW5yaeXV0 wfvHgimAgwbOMnnPpSdiXqtRjfMSNw44ppXBIII4AP1p3zFCSm0n1poAdwV
+  JIHv1pxTfUUpRURjf MgGCec5HpTOOOScHpUw+U9Rnp9aiI+bB45qvIhNNJiEjzCOCc81XIJGS
+  cKDxVlgmCCGwTkmmEHYT jgnkGpSHpazI2PAZjls5pnU4XIyM89qlbaoye3603PzBR6Von0BbX
+  IzGVjB3fMcd6a6Ig3KCSelS KzZG5lJ9AKYzPvIUA49qmfNcabvZkRVmPrxVQgnGTsA447fWrW
+  87sdW9B6VC5UA9s9c1pG6bVimn flRXO7zc8kAdKjdQ/BwQRwakKbpAobnvzUZyCNvPH5Vb1ZF
+  7PzKpxxuBHWqMwMaHaR16H0q6yZbh sZHWqskZLbWHzMRg1DXW44pLR9ShKm0jZjOOAPTvUGxV
+  XGc8cZPNaJUeaQB8o79aqsuCx2nJOR/9 al7Rtb6jnJJ2RU2ZTpxVTb5bHGSxIAJq6cknquByP
+  eoTBlGG4jnijp7zMltuVCoZs45BxkHFMZSk m3ORjmrAjw4Qk8rwfemiP5sSAkY4x60pWS3NFN
+  dWRk7uCNvYkDpQFMbcuNwbn3pxXClgSQH55qRg JFXbg85xRaL3FvsiJRuGQcDviggAHoV60rB
+  vNwcbc9vamlDjCnIIyfarml1JnFt3RWLF5gwUehpr Iw+7gZ7kVO+PLHzDcBjAqEqWAUsQuKUo
+  p67B73QpNgxnI46E1TYARu4+bB6E81fZFIAzuJ549qqS EFCF7cHjpVJLboS+YzphnG4jBPQCq
+  sh4I5znn/CtB9rjGMcdfWqZCGFjyR156k0TjJx2L9pFnGxq fMyh3DoWzmr6HcQQQFAwR3Bquh
+  CgkJtbPSrCcHOzAPIFOfvPbQlwdlbQWPltpzkDPPerQ2uQO55x moFyowV+dhj6CrMZBTGVz6i
+  m77obTTv1HoFHyqMnv3qUR9DuBA6rimffjXt1JPSpVQqc57c81DXN G97DSVrtEyMQqEkKu3nj
+  vUyguQeD6moE6fPkZXFW40AT7/A7DvUyShHmsTzpeoLndllZgep9Kuxf fycBTxj1qJcCIY6Ed
+  KtoFKHbhmBHAosmjWUls0WQAYuAQc4q8v8Aqxz04J/CqcI2yNuGVbnirMfz L8wwrGny+RC3Vn
+  oWoyAkec/1FWdzcEDI6Zx0quNm8YVsgY61ZjdmBjYqFz6cmla3vA9XsWBtaPqd 3bmgEp83U1G
+  pAZeQewFKchGJKjFXzW6jhF7E4O0DcAR6YpxfdI+evrUIztyD9OetPjIYnOBjrxWc 5ajhuy4j
+  qVPTcKC/Izg8c4qsjEYHr1yKnj3CbgruPXNTFxTuE421uSGTbGxOcZwR0pQ7bgzfIoGe e9MRC
+  7fMAFJ5Bp7YRxhhwMYNXzK7FzReg7ftckc5OPrTxnhR/EC2KRM8Zwdwz0xUyBAoAB3j37VL 5V
+  uipTfzHxgkK2GxjHWrKLulYMCcjiohyAEBwD1qZQwAOwnI5GeRSbbegJ29SzHtwOhHtU652lhk
+  A8CoUVi+1Qx4GKsxAlCjL3wKm7QNO5KNyE4yccE1MBhF4OGODxzSLtzhfQk89TU6nCrlcccE0+
+  Zl uOl7CkDYAuOOB60zHzgsCMDGalx3GOCOaNoJ6d+lUpaWIWzGlQ2WRgcDr6VAIv3zn7pz909
+  asNhV AAw3tSEjILAsfSqvJBzS2uQgLyGbBznrSgENkfTmnhwWOFGW9qMZbcSOOwqeWW7NNle4
+  nzHqvfqK lBwSCc5NICVQkkFhxSkAkEDnpUu7Rj1JFEag4ySQD1oJONvyqTzigY243DGfTrSg5
+  5OA3ao6XLjo rpEgwgCPtIJzkdqewPOc7ycDPtURIJyMA+lAJ2A9fTmjVoTpteQ9tyq2Dg/zpU
+  IOM8nv6Co2bCqN wIHWmFsSkLycVbV1YqK5r3JgMHJ4BPp1pWkUbcMMDrxUCsW65GKU/KNhIPv
+  ik4rqDi21Zkm8M5bg c0ib/M3ZGMYxTVwEw2cdqeWOMEZHqO1K6UtBuTfxEZb5iSdvfB70hJ3c
+  MOOOO9KQpTK85PNLwW2j gHkE0732I0TuxXOFUNgjPaowuG9QelBY7SFBLHoKZLuZRs3Z6U1Cx
+  UJ3W+4b2LfMDjPHsKc2S3yq eOtKuSfmUjaMGmM4XLhjz0FK6bIabdyPq3HU8nNBC4BwST6dqX
+  n5W9RSbcgNjJIwMHGKvbUf2tQY plQDxVc56qeTxxU7ECPbtwRxn1qAqWIw23mnF9w212GFSkY
+  cDJ796iZg7AYI7kVLkrGCxJOcYNVn G2HGCSD97NKMXJtsnrfqRsjLKCpGMdahYAZ67/6VZcFz
+  klRxjmomAyOo9CD1q4yktFuaOStoVZFz GTg9MjHpVcjMmCSvy4BNWWO5sjdjoQDVeQMMEqxI4
+  Jq0/PYmWjViswKqRkMw64qiwyuOcZ4z3q8c FsqGI/izUEqKHXnk9ABWMnrsTblVmUjtV+Qy54
+  PNV2V/KbBHtVtwTKOhUHHAqux2uu4jGeKuyd+4 4SkVfmyoztz1zUhB8wc4UDNPZVKlgcYH5VA
+  MqMhs/XuKHZ2Ha6GFS0yqxAXGSR3NIwJlBQYOMmpZ FJxnpmkUjadoOQehqua2qKsnoiMKqRtu
+  3EgcEGmFxJz82QMHFTgATkHJ7gE9qCdqA4C4PTFDehjz cujK2Ar9Dnjj1qOUhRgnqRyKsSAZI
+  zyevtUDpiQK/OSM1EprqDty6FcqfNyD7/SqEjYkPOT0IFXZ AVZtp5J69uKrv5bFixPJ5x61p0
+  7jgktbGcyAuD0x0xVZowzlOq+1XziOTP3htwRjoaqSkHkcYGM1 HLK4ubojkPvFSdoBOSas7AE
+  G0hhnINVQyliuGzjqDxU68lAXA44AFaJPe47qysTocN84wvYmpgBt +XrVc5zgncegx61OmTwS
+  OmD9aSuVyJa3LKKWVWI9gRUirhMkr1PB54qJcLgAE+ozTxh2ySAc0byu LVq0noSInO7D8HFXc
+  qrKuDuyfxqvgg4JHJ4xUi/NIm48jk570NXd29hP3tyyhOzcAF6EAjpV2I5l VhwCpJB61VjIyM
+  9CfzFXEJyWGBk4A9KzS966QnGz9S4p3Jwd3Ye1PQlY2DfezkVXjOG5DAjjd2q2 CDyPmboT2NU
+  48m5TdtCZJMKWB3ZGPpUgXMI2MRjjr1NV4353KoAqRcsSobGTnPvUweumg7u9yzuV IVJznNPV
+  wR94bD0JFVSdu0EhlBwaeJPkZgv0HYVnpzXKUmuhYXJlYKSBjIzUqs3mKmVAI5qgJDuL Drjmn
+  7jhXJwCPTkVcXd2Bxv1NNJCi4Cgj37UAsxP971FUxk42tz2FWQcsDnaaizT0Iah0ZaB3oWd WJ
+  xnIPFSId4XYhZsc+xzVRSuOjYX361ZR/3gI4B9KqT8tRtcsdUWN37wg/fLZAP8qnwS2SSv4VWU
+  rHPl+hGAT0qwHYkuMYGOKrXroVzNapElvn5vfnPrVwN0KnnuPSq6BQ4Byccn2q0CPlLcA5/Cp5
+  rv QiW5OobOQTz29KtRYHBP3ujZqCLDtlQ2QOM1MgOz5x0PGKe+5SldpFsBTtwCOevapgmcr19
+  ahjIZ eFbC9h3qbpJ0OSPzqW+g1fqxBncV56/ypwIJUknOaQGTaQVBJ6cUuMR5wOOM4qXbqH2h
+  8h/djODg 4x3pmSX6hR6kcUobcSCp68e9DcnJ4wO9XDXQE9NXqMCFSem007arjKsowOlCnEXBy
+  e3eg7SQccjj IqZKT6h7smOHC4PBPOaRi20fIVGOtPx8qgkHFBVi2T09qmLSdw5etyJC4TcQcZ
+  qYAFjgZPUUDqVw fpSjIO3+8OvrRJ3Fa6GthGVQNxx1FNb50IB255Apejlt2frScoMDBOemKd0
+  hpW1AYCjcDzySaXHz LkjIprPztJwAOKRQTk8nPr3pNuw3orpbgN3mfMOvHBoBfcegUHAzRk7t
+  vUA8HFRKWyQScn1quYnV u5M0jgHBBPXpTEOF+Y9cjFMwBwTlh70pY5zlcAdMUou1mSmuxIvTq
+  SM9u1OWQZIYYOOCahVwEzkf 7VIXDBmAyQcGm3HZlpq6uWc5XaSBgdqrlmLDaQe5UCm4IkHzA8
+  c0xxhmBb5s8e1NJb7mkZa+RYZm DYztOO9J95sFlA+lRo+Y8MAcjJ9qRc7gwzjHc9KOVcphysn
+  C4TBIJ6cVH0bdznpjNGcNjB/wppKs oKsAwoW4SWgz5wzEnoKbzuwSAByD9aduycAn+lNwfIO5
+  TnGAattpotJ7MhcsPQ9jx0ph3NCM7Tg4 HvTmHOPmI9B2NM+QykANjHek3Ym12J8ipuY5YdR61
+  XOCQcgDv7VY6jrtB65qu+ORxzyKT9NQd9iB yGZhk5A4A9ar7vMxnltozU7DJ3Y4Ze3aoSAIsj
+  vxmqjazuVFt6FR2YSbVBIIzxUBVjggexBFWzgI eRnHBqrsJhUcjPOfWiTe6DmuyqyEKxbJOeB
+  3qFsHB2MMHknuasOSM5Ukg4TmoWGGIySOhyetOXMt yNempWPO5cYBppXC7QOVP5VI3DAAdev4
+  U1wGHOVLfeOajV69DRu6sR43HGfb6UwoVcZBwB1FThcM No3etRF3MhJVuTz9K0jonZkp6asGJ
+  6gD61Ccs5DKT6e1WAGw2VyQeKaWGSp6gVN0tLApLsVnBZkI IL4IpoX5NxYFyp21MQS3GMdc1A
+  WLnHAYfnSvpfYznC+xA5xGobBJGRxVR24G5So7/Wr5Q7SrMAcd COao/el28HgHPrTg9BK1io4
+  VmByCx5J7VUkVckHOPbvV9ioUjgLyBntVNvv4PHcVd3fmKsrHCs4A JzgZ/WrMeUAYENniqabV
+  y+dw6gVZQltwyFOKpT1aREoO1uxZGS2cFsnp6VaHXd2FV0w0KnOCDzip kxkgnPpUxipFddSwv
+  LbtrKNufxqwpRh3BPP1NV494Qcg/hUi4Dlic44wDTabauO/ctIMoRjkdR70 DktuBJ2kU2Mkjd
+  jaAck+9ShxtJ2nGaiKd9iabd9CaD5XxlSFOBmrkZUzAnIxyeaqJgK2whcc5Izi rS+WyI/8Q4I
+  FTK6Zc1zKz3L4KKp3MuC1OQpuPPHb1NVlEXKHnHfPalVUMm4E+mKrlb6kq0VroW1K hyACMjgV
+  JvZSMkZA9Kq4c5JcZB5x3qfefLBGCp7YqOZCV97DkcEAPkjrkU7IIJw3QnGagLjACgk9 8elER
+  z8vTHB5qancqK5ndE8TAncrY453U9STMy9eeKqqymTAbkA4GamRmEgUZ6c+1S79zVxu9S+j EA
+  MNp4yTUyliqsDwRVRGJj3YwmfwqxG6s+3BCf1pRm9yHpsifB3evParablTIxgjBz2qoGw+CDx3
+  NTxk4OMkHsa0c+rFzXLgctDtYAkdxU6nJJ45HPtVRFQg5OCe2atg7WPBA71CcFsOL5XoWULMdp
+  Gc 9cHrVtSTyV9s+lUo5GVCzHHt6VajYSBSSVHrn3rSLb2QSve7LaEqCBkHpmrqBnChuD1qivM
+  pI5bv zVlM7QxbDZwTUyfUvR77l0ElxnoBjjvSsZQpyQAvbFQopwDv5I4HpVgIOm73+tVFRQnK
+  2lwDOQu4 HI6gdxT+DyuWA5poOMNuDMBgAVIMNICflUjpWc22tQe1xSR8u0Y96XaQVx827nFRq
+  ylM5LY6c9KA SGJXp/DSSsKy3Q8d/lxzxgUhQEv/AHjjimCbkECpl4O853N29KPeiXyu2ogwSM
+  UpyJNrEEegphwT nawPQDNPUuGDHAOeuOtCVtSLu+40BSm3BGCSDSZO7g5OOfanNnftA3c9R0o
+  OMt8vQYzVPcL3XcEC 9W4PrjioyzHKtnAPBFPCj7rHr71ExypAPvRC8gTtLVXHMGCbmXcN/FRj
+  7/3sAngUMSyKCTtHPB61 GrfvcnIB9fWlumXyt6Cl8ZCgkg4HvQCGIDDoeTmmM2COmF60ze2d4
+  6UPVE8rS1JSwK8Yz3JoJ2jI PamEfPnIwByaVFCpkHdgnrTjZLUNtRCyliBkg9hT1wApXHI5qM
+  AGZmORTcAcjI9OetVdNBKVlZD2 bDdB1496UMG6kEDvQHyvPODzx2pnmDAIHOKSetlqTZtbD94
+  4BXHbPrTgxZsggKBgjFV925+AQONw PWpMp3DdOvrVyaTVg9nJLUkZcg5fIJ4+lIGb5sLxnGaY
+  7sQMD5T3xSKxZckMOefShXaHeyuLnEpU jk8k0g3k/eGMU0yKxwMjPIJqHe3lY/DP8qOVvoFlL
+  R6ExJD5TaOMHioWIDLgjOMU4EBipbf6ketR yKu4NtIbriohDUGmpWSGEkqVZgSOtQORuAU4PY
+  VK23ZuIYHBzUJx5anBGRyaq73CU2RFzu29s8kd 6rktjpgZqd3BO0YA3Yz61DKWCAYAI5px7Eq
+  pJ+RHKoVMnG4DkVUkOIQR261MzfvWOC2Tk88VH+7U 53cEcjvTk+hUYpLUiYq0obepxwBVZ2Bc
+  hlzj0qaTBwwBHQAmoHUMc9+MAGs7q7uJWIcrszg49M00 kH72eRz7VMyAOFHT161C3DgkgjIAq
+  opJ3KcvdtEQBOmcHd601lRZGwe2BmnkHeTzuBz9aQkSOR1G 0+1PmSepOuxGM5x3HvUL5DtgDI
+  +YHHWrGQkJbGD71Fu3tvAbrg47U4xdr20HKKWr0IZMiMltxBPH PIpgUKpZc8HP1FPk3FVCklS
+  agAMjKORj360ON1uRb3dxj/MCWIAJ4XviqzIFlPUMOOv6VYZguAyM AD96q5ZndiQpHUcc+9Uk
+  76E3sUpN2/bwRnioZVxl3bPGAF71ZfiWQH5cEYPYmqsvzYK9O496HC5b fVs84R2AYbcYOFwOt
+  XI2yyMM4Ixu9aqLtL71O4dMmrkbjcowvfpTSM38VlqWo/uBSWUE8GrUYVYz 13AcelVY8BWx85
+  x8oqePJwSu0AHH0qlrpcat1LYHyqADgHjmphjaQeR3xwahRlEOcHOelPicknO3 g4pxTWpa12R
+  cGQcDC80qsFnJwORxTIySoB6E8kdqmVdvyfKMEjLd6ykv5jO6UtSRPM2nbgKfUVMv yocHAzjJ
+  FQJ8seSx9zUyNubJ5XPSqjF9Byk2ix0ZmXG3PrVpWROSBheCfU1XQuWZSVUDse9SDIJY gHIII
+  Hb0rKXM3YrSyvuKwypKZVsjK+tSOSFyGyR1wOlKq/KRnnHBoEacqWJPpnpU86e41JOOpHuV UG
+  0Zz0yakRgRjODnJHemupOdowMcH3pEG6QgZB6YNNxTSuyNL7kvfK4BHUkVYiO5m3lcD8KhWPLg
+  kZ9hVlQHKlcHPUd6hJPRG0dOpImAdgJXsQasxjawAGMnkGq6lVnJxnnH0q3zkjPfGarpsRK6aT
+  ZN jhufoT0p6SNtwe3GcVCqknaCCAM4qYcKwzuz0AoXK99y5rSxbUq8jZbGDkVYT98nU7emapR
+  D5iww uF6N3q3CwEG0NnDU5abGMrpouZIk6ggdqtRsocgkbQetU+d+TgjHFW1I2ZI4OM02rW7F
+  u76FtHUZ AxgHHH86tR4xk8gjNUozk/KeM81YXk/Id3baPSlyrqLmaL4KhAnDDqMVIpz8wByD6
+  1TUsspzjHGB irHKwBjkY5PpSa6I1cLJXe5L0bOQAO38qcSdyjrtPJxUR8tlC5OCMHB704AZXL
+  EgHnnvT6ahovUc NoZiCMN93NOVy0Q2j5sc0HhBgZOO3egHK4zjNRZ9NQUtbsXy1IOVx2z2Bpx
+  YiM45IOKZvJbB4GOD mlj5AJbJx0Bq9be8TNO97kjcw5CkECpVKeWrMGJpMAZIIwRSbcBiTkbe
+  3SpWqNJO6sKANrMemaib 3O0HvmnAtnnOBxStngEgVPK76C5bakIyvzDP3T15pBhgMEBunNLkg
+  YKlU7saGIVc8Z6gVdnuydLX IwSHbcQRTWcA4bkAce9O6zcDJpSMYbIwexHUVOtx8sXKxGCDtI
+  AzjPPvTGxtH97HSrG1WTnqTyem KgUDcSwJYHr2qU3dg1Faobt/dkK2455poyJFye3SnbSJSRy
+  SM8dKMDhwwIwQRWkX3JlorDRsaQBm IGOcGmlSAQvOemKGCbG2LknjOaQBgpGeBjH+FCknqNKy
+  0Yi/LhS3Q8GpG7kYAHQ0w5CnC5oPmEjI CjuSOKaSbuym2noxQABnrntUaSFUwcjnBzSkgnCk4
+  xnPpSAkldwHQ9qclbSxMpRa11JztI+U9+/r TWwS3BHPTPeoySytIBkdsUwlSmWbIx196m7urF
+  RejfQA2JFO0/WnbhkgLgYz7ZpOkQbjpjJ70xiG CjPA5JrR8vVC01Y8bcBdrHb6Hk1GTtbC88c
+  k9qeCQTg9fX1qNmEfmF1znHA71mnYzbTZE+OVyTnt moWYlAQOg4FSN9/c+cnOMdqhyuOWyQO1
+  XZNjjdvQY3IC7MN14qFiRIBgZxzUhwW3Bvm9PWoiSykA c5wKlvUbkra7EGQf4TkDFQE4dhlcH
+  n8KmLDldrKCeCTVZnLYLHg8VTTew4yitRsg+UbTu9PYmo3A DKBj1OKexAcEHPXP0pGZnjGAAo
+  GCSOtDctmRyIiJXlSDuzzjtSPFkDq2R1HSl5ySwJHTI/lQ27dw VAz0PahSl00HGTjezIG/1QQ
+  NkmmbFVjyW7A1Lg7MdRnP0qPYCHCkqpbrnpTcrIUX2InG6Tg4cdc9 KEyqjcCpPB96kbAcgr0H
+  B9aYR+7BwxGM4pSldFtXepAWKv1xlc4qAtnayDDemanckS/dzjp9KiZQ qgMTg+ntVJkVHFWTd
+  iuQxQn5iOpx2qB8/MAwBY9D1/CpnPyl92Ez09aquTztb/cNJe90DlS1uR5U qM5J7+1Z5YbiGB
+  9jVuRm5jUZ+YZPr61TZAFDbScngD0pyavYSTtc87ixsQKeec1bGFkAABHXFVVE eGVD1PAHarE
+  IXzVdcjj5iTVuV7a3Q4zsrXL0ZGwFR0PHtVhRluCc5yQD2qqpyfkOBnknvVuJwpAB DNmjmdrk
+  pxT0JwvzblBOexqwuSuCAORg1GxYxZACkHrUiBccg8UuZ213CKb1J4wc7WPy+pHWpesi gnAx3
+  7mqyHMyhmyAMfWrAwYgwHI9KLNOzHLe5MpZiFA2/L0I6VMpfy92Ax74qFEK3GSTyKl2soAG QW
+  Hc0khtxvYsfwDOemCfepg42luhJxk1AjbSdxUknke9TKA2A3A5ODWeq0Y07bCgsfunJ9RUqFto
+  wQSSckimxhAxxuAx1oVgZAT8v8PNOydgt0SFCfMRlyoGSe1SAbWUg7gOtOU5+RjjJ/SpG3kHao
+  Ax 6VEm7ibb0sEe5pl9WP6VPG6rMAOQM9KbhgDgcjgZFS42MQMZzSdt0OmlLRk6KNnbGD1qfLG
+  PCkHA HBqBAzIvb1qwF4AXk45xTstFcTjrrqSKuCOeCcilCsJFJI46cdRSD7ucccZqQuCOCMk9
+  xT1T0Qcz Ww/7pJDAsTVoEBME8DgEVAGAO9zyFySasRhfL6gA9aTk1rYuMu5YTaxXBx3ye9Wt2
+  EUDOD1NVkOY 0Cge9WAm9VVux4NTz2SbBSu0pFuNj5pXv296mVtrj5s8cYqpGnIOcsTwAasoGO
+  1gvfJ47VpHlLdk X42/ixn6mp9w5YHPYiqiq52v0XH51KuTwpyAMfWhW2M5RVxYx84fIwRwDVl
+  eFAAzk5qNUBReMEDF OG7ewOQc5z2oqT1Vyp3k7IcMSDAzx+pp5RgrOoBbHI9cVEnXjIXNWTnp
+  0PbNEk4kt9GRkBsDjjqO 9TZ4Hy9+/eoVADswyTmrC/NtJBA7kdqzfmXJJWHgfJ0Aye/anAMmV
+  fBBHQd6a2FOSS3vS9cAKWAH WoSuHKnsxrcIOABngnvTAGLDPzZPb0qXAaPAwwz1xSqsm8sF2g
+  DtWnoKM+UY4zDuOcg4AqJgzDJA Crz05qwpyQGIHHpUZ3BieAOmCKIprcIbtkeDuBxgdeRTScg
+  579KmGSF/rTHUjAGCR6UpJXHFvqQy sVxgY4wajA3YBOPpUx5cAjnsKT58j5Rj6U0tBOViA/cA
+  2sB2+lLtJ3K65QnjHepmXbGV5bPSohu2 YY5XOFIq17y2Ji7bETDkAYVccEimlWEYCjJBqT5WB
+  G7GTwaQ/e8vuO9Zv3XsU5PsNziIE5zmo9+S 3JxnkUuG3AE5UnimswHyMMA9CKtJkpxTIyq/L8
+  56dzTkILfN8x6AU18bhyO/5Uwbig5ByOaJ2a3N LpbEzPhBheD61EwJwSvy5xigMxIUMoXqCaY
+  pVW+ZiRjjB604NWtYjl0JQ5CFcj5egIpuAYznAx2P Wm5J3E8YOM0EER5B+b0qWS7XGFxtLc4J
+  xj0puQckndmlCtyTnHVhTAp2sBjHqa0TQ1roGVMuVJK+ lQyJG+4rzxzg0YAPI4J4NNYDeckjn
+  J96lysEbt77ERDBVJ2kN7VAygMfmIbPNSSBmjyT3zxUZXA4 OeRyfemtdWJu6K8pG3uSeeDUWc
+  lQpUkL1x0qRvlySRuP3jUBKHAGAGPrUt23GrdCJmcPuK9BgDFO WQNER8qjOeaR/vZznj0/Wq+
+  D5oKk4xkVbUXZFOKepIW+YgnHtTCW3HjoKUDdzjnuKQLiQYbBXjBo uuUTmrWGniMdeTk+9MJ4
+  OMnNSjHkr8pHXgnkVEWO7GKaV9h0+wmNqKW6kdTTONjYJAx1PQU4McKv 3hg5FI20dTgHsam7u
+  ZXalZlUsSm3BbnOc9qZIpB2McjBqVyQ3TAH3ahYhw5LY+tDjLmukO22hA4A jC5DA8darZzCVU
+  g4qZ/kUMMEHggVBjO48DPFVLSxMdXcpyttQ8nPt+tVXBK5ByoH61ckC7diqSPX PaqTDGRk4z0
+  9qLrSxS+E873DDLjkHr61ZBAZTn0yBVNPMSLLkMw68VbQLnewbLeh9KttWeu5MlFL YtxZMf8A
+  cye9XE27lLEDA7VWAX95huBg/jVhcmXPDLu6+lZX1Ha6si+CGfIzsPr61KhYgchWxjFV k++Cc
+  jP6VYUETbu2Oc09X1M9k0TbNhwPvZ/Onpy5Cgpz0NMT95yxAAGOalj25JXcwzyaqTdrGu6e pY
+  yWPXHTBqQAsm7dkZOKQGP+LIAqQhfLYLu3LgVClZp2JtrqIiuYyM5I6mpSVZGL5OBxikBwxUL1
+  pGJUggZBGD35pNu+ptT31LDBBIGbcvy9/wCVOxmJeCfm5FNAKSGRnVs44IqcDLqVOBu5FHM1pc
+  mT UXYWJW8ggkZB+WpxH8oIz7/X0pvUlcEYPapVA3kFuOg96h3uKKbTuKu8ybenPBqyPmZecHG
+  fwFRK p8sgDBLcH1qwEIwTwONpppsG0x6lQOCORkj0qVeUBGTnsOtIEyoUDgdDUu0cc7ferc4/
+  ChSn1uOU MMhuB6HqKcqgHBJPNCr8ud2cjJPvUrR4iXJ5HXFTKdnbuNSkxVCiFSTu9vSpxgfNn
+  BBweKhjDNE4 4GSCPcVJgBSCQ+DzSTsF3HZlhJdhK5VgQSMCpkyQobrgc+9VyoI3cg8AAVYi++
+  RgMq9xSvFJ2RSU Wty4vEn38EDr71bUs6hQSQOpqmoBw4PzA4YZq0GGFVSPT/8AXTitBa6WLPI
+  UnPBqwDsKFSpK9cVX TOFB5wKlVl8s4GT0U+9PltoNp2SLOTuPoTjpRywPPfrUKu2BgEjOalU/
+  Jg/LzjFTblKcZfMmxggE fMOTin7v3hAy2T+VIuGVW3bh6ipo1IBLAZxwacprRii0tGhgzgjHO
+  4U8u+WG3cM/dWl24+Y04ZKc MBkVDs9UFpIcqqFBLKWPYnpinDIbbuyG9KaFyvBUHGfpT0IT5g
+  u4nsaNWOT6CEqVA6Y7CmsHLDaa njjDOcgHnkCmvHj5lO0981UJpj92/L37jX5iwAcetMVJATu
+  +nSnlmA5KrkdMdabglQRyP4setONl qOV4dhpyOMd/yphA5K8MDx78VO5UJnPzMckZqA8rnBAP
+  SktRNcqIlU4GcjnrT2+UsecZ4NLtHmIr HaOetOCsVyQdopylsTe0tSPBJAOGGDUDcr2AHarB7
+  YOc9hUfJcgEKO5I71PNbcSTTuQNjaNqkf7W KjOBOSzAqR+dSSKVLbRkgjimsOWPdTjGKZUOVO
+  7GMdx+XqOeahLDOD97vmpNoL8sOnzVD97cMDOe TVWSiHKlsN35KnaRtGMnpTc8sScZ6cVL90f
+  ezngccVFsHkkNnJPrUc0dWyelkIfkUMRz0znpUZ3Z P8QJ7dqe2ApVidh6+1R4USPubGcc1am7
+  rqSko6XBV+TLHgHtQeMlckgfMc0mVDE8njof0pSNifK2 T/EAaTlc0vZaiFiQo6Meuab8vz8+m
+  OafuUkZ4brVZztAJHQYFCvs0TFJ6j5JFEeGXnOD61DLJhQM Yz09hTmysW7GTjnio2BJBOMnnG
+  OlOLsEbbkLMyhj2A/WomJIU4PQcVLksXyNvOCCKrmTpubsCcUW bb6icbIhD/vgS2QTnHrUTcS
+  c4Ck8EipJGDSbeAP61AVyqqwPc7aqST0M5JrXYhYsVJYgYPXFKxOz jGGFMJCyDP3M8j09ajZg
+  pDLz754FG0i0uVXHKF3jduyDliDTiwMvUDPTNR5bJLDHYUAZTLYyD0FW pNi57eg4l/MHQL61E
+  5bIAHyjnipC22M54FBcCQggEgUlUaaVgTTVkMVnk5VMHPTHNMfJX7w3HrxT s/6QCFJ9SDS9mA
+  AIzxmolP3hSdtCsC7Lt28k5we1QSMPL5UlurAVJJncSAxHIAHeoDgDoTxVx01K dmiLg78gjJy
+  BVNiRJlgd3UDPSrbLsUEA8jpmqLuASwDHI70Xv0IptJtdCCRz5uw4UpyWHQ1TkGSC HBLA5C+l
+  XsgIzMADjvWax3YUIw56miM0nsXDV3jsefgsdyqA2CMmrYIJBQ9OoPrVE8bemDx8tWou GYEHn
+  APtVSXKm0ZO+7Lkb87cjsT71fUdSCAx5wRVJFXaB/FjrVuHzDArEZOMKfWlOF2pJmr6P8y/ CM
+  k5G4ntUw7gg5OKrRsGbK/fU5zVsllJOMZpRSvqZuVxdoB2qee2R1q1Fnapwc96gQ9CQeCfxqVS
+  ehO3PSrUtGW4uSLIO1/mUN6CpF3NMyg7ecmokGIyZDnI5PvU+R5KgZzt59aye+w3Lp3H4AgHyN
+  vI 496OhRcEHoc+tIUyuDkDqM9qfGr/ADcBiT37U6clcUb8trljaXkYsjEHgVJGCMEABc8imAk
+  8dV9q mCfLwGU+9EovUHK2iJV2gSEg5zzzUyqiDPUkcVCnO5SQRjJqUZDoPLPy8DIrNRaZpfrc
+  kxu43fLn irURJTDjPcCmooDcYbdzUyFVHGS2cE0m76WJVncevy52thsd6kT5W5xkjqaAuHI6g
+  9falLELgYLd qqEuZWQpaj8YQ/KRxgUuFCjG4kfeye9NXeXxnJ6gEdalDqwwQWOMDFDlYfNdWF
+  QcKSpBPGc0+NC6 ttZeeo9KaC2wK3GetKMK+A4I9Kh33Qoxdrkq7gy57DOfWrKoFAIJBPqaqDI
+  GMHaauhFIyT8w6iql JrdFyjH0J02qSeemTzVuL7o+6Sc4/wAKpKMsArAnuvtVwMoIO3kdqfK7
+  DUL6J2LYKiNQCUYdQak4 bIVl3dsVCq7hllKn1PvU8S+UVbaWAGMUaJeZLvF6EyAiLYzADvx3q
+  RQ5Vdy7sDkAVBltxYLu5B9a urnduHLEdBScrPUavFkijg49cY9KeciPackn3pi7mXGw89frUy
+  5ITIPHU0l7u4pNSWgg2hQvJ5pP 48EEc5Ap5X5/lIOeRUjACIc8HuaTbsKLaV7jVXqcc5wBU2H
+  BALIGbtiq4DBCSAQORzT18wgMWXP0 5FO19bml2KU2S7MnB64NOHQgAbS2eaaH/eZyC+O/ak3M
+  2FwPU4FJvUqbskrgAPNy2ScVLujSMdTn oO31qJQyynOCMc5605l6HJOeAPaok13M5STdhPmbI
+  KgDPWmncI8feHsKk3nagTHTkmo3yF3D8QKa auJybeoE7iWyDTSV8gZbOcZA9akDAgnYQ2PmNR
+  Md6gEr1p7ly9NBhUFyfmGcVWJAdN3G5Sx9varT kAnad1QO6nICgYwCTVQeu1xOV9LXRA25lDt
+  yQe1RcKflbLHn1qct+76dT2qry78HC4xnFO7e+xSl aOpICokwcEc9BUPl7W4PGM5zS5AJCtjt
+  zSFl3lmPTr2FKO9kzNq5Ex2oFTO71J4NRlgX5baQ2SKl b7vp+FRyDcAcgYHJIpy8h82uhG+Sv
+  Xv+dIMrIQenFOJPlhQM49RmmseORgk5B9KTTaDV3EO7Chuu MZHalAKklRnHQ0jOhHIYkYyaaA
+  AM5LZGeDxT3uNu0dR2CS2c5x26VAyjJGCRwMelTDduy+BG1RBR v45GeTnpSunsRGdncQ4VQoB
+  LBehNQMWdQVG3Pb0qRwDyxII6EVC5XYBuG4ngU0+ZBZXuyJziY9el VpCpi3A855qeRT5hySST
+  1qCQgDYwwM9cVUddAT8yFyoj3kc54yeoqq0iAfezj3qZshcHHI4qux3D sW6dKpIm1yLPRTkA5
+  xmlfDRDhT601gUTaTvXoAOpNRD/AFIYqwGfm56VN0nqU+e+iHdQGyMU9Xyh ABHPU1CcrMQeRn
+  IPajcxK7gADyMd6q/NuOUko7DiwAAI5I70mSI0zxnjB60qYxjGD15pA+ZMMVPH 5UOavawlG+o
+  8sBCdqncO/vTH3gglgvHUU1nGNwJb6Um/12kHqTWV12FJtLRaCZOAVB5PXqKrswGA e/A4qRid
+  pAHocCq8m4nkAHvmtORNh53IflDFdxyF71Ucose7nkcVO/31BBBJqCVVI2Kc+nvRdpak ucb7l
+  Vy5baBkd8VRlYHhvlIORirjMSwAypA6mqLsvmY43E8H0pWQk9Tz8KpjJDFgO3pVqHAtx8pL E5
+  BzUChIpujONw4FW12F8E4I4rZWWjRKqPlS3LaffXH3ic5q1HzGrfN16dhVKHkxkZy3I5rSjc4J
+  I4J5A7VHLJbMq6jqSqQANox6+1XomDIN3JAPPUVUTmRQoAJHQirCLhlGCAOtFr+opJPUlBUqrD
+  Ab r7VYXPDsASTnj2qCM/OwwAM8ZGashQ5BcgqAfu+tN8yfkaaLUdGHkUZxnGasqo2jLbiQcKO
+  tRKmR hRgYJ68inqSsiY+7twKE+bWxklfW5YjJ+VcEt/dNTKMIG3Luzk8dPaoVBV+Dn196lUDa
+  3t/D9KV0 nojWy2JMDbyQzHONtTooG4knHHNQJ83JUDPIOKtpvEI3Lyeg/pR0stxxajoJySxwT
+  zjIq1FxKuQe mMGoUwQepOMfSnqCZPXuSKmWq1Y3YljO6VVIKnGB9KtooE45xkY57VXUkyLkj7
+  tWgcle+BzUXtsT JvZDt+JTnJGe1TrzGVAJXvnqKrZ6dAOpzT45SBuyDn06VVna6C2mhOA20FT
+  vA9OopSxVidnBHGO9 RlnKrg4PAp+T0JyAODjrSk4p6iV7oAflUkdD3NTHbkA9AB0FRBXJUYYn
+  HFSP9xF3Ddijmje472ej JshpAoI2jgipRu3KVyQDzVSMb1O1stnJJqzGxKk5xxwaUk7jcGti0
+  rLn7uCOM1ZyXG48jPFUlYlF Lde4Aq2kgEcfqRyKIxafcIt3ukWs7Y8iTI3DrVtQfmzzz1qih4
+  AwMdR71dDkNwcelKo5dR2luiwM LwBxjgk1OnyKQoJPc5qIEFACBjP409VyMgj3oik9bDb93sX
+  EwARzxwOaUfK5xkIetRxklxjr+hqT d8w6An1ovyuxLbt5D9+F6cdD9KVgNw3nGRkc05mwoPyk
+  49Ka+PL+YZ44OKEwjN20QDiU+XkjPc9a d8y5bB3E0ijcvAKnGeOhoEjPnkL+FQm3ctvqG1DI2
+  44yOMUb1BAGSSOQO1DGMg8nIOevWmgHYflx 82QfarUrIIO6Ji29BjAwcc+lDEeQCOe4qBR+/w
+  C4X1p+cuMlR7VD3FyNvUXdhSPvetB3CPkYY8ke lIWZlzlR+HWmM2VGQSc4x7U1eW5Suh7PkAA
+  84wfeo88KBgtio2YuxXBPTBpWxwOVApyVlqLlVtRj MzEHG1c1G2OR8pB/SnM/zYbkED8KjYnb
+  kcjuaq7W+hU1MhLgkgnA7ZFRMyD937Zp8o7gYOM4JppC qSxxnGckcVNnYhxttqRA5k6gEdc0O
+  +EVtpIPX2prHKliQD14FIZCUyFHsDVKydxxetxh6sXIOeQB 2pn8POQemeooOGl3g4bFMBONzZ
+  HfHpmktdWKT5XYT5WRc5B9QakPEHPXvTHIkIKnnoP60gLNuwVJ zTTuKb2YnO3HGO9KcKByOBk
+  H1qPc3CjB5546UbS7ZzznI+lVJdxTaaJWc/LyvTn2qsTjncCM9hUj KB909ByM1GwG5iAC23JW
+  iDgtCIciegh5UAj+HOf8arSKoZT0zz9KezNtzjaCBkU1pQFJzyOoHaqb lH4TTkb0WxGzNhQAC
+  MZ5qkfkQ5+YZ4PrU5kZyw+ZT6mqrnaFUnJ5zU6MXLb1GM5zlVLehqEswTgd uuOKeS6ttGCeB0
+  qs7Yf7rZ579az0voC0VmhpYEEpyAc47ioeWYgA46nmnOxDKyjjZ27mmBSGyCCc c4q5WWw7voR
+  lHKgHAyN2acmQoO7JOce1NO5txPUnJA9qe7jyVClSx7DtTdSTtZD57jix8tS5AJOS MUpaPYxC
+  nkY96rjO5QQWJ/KkYsEyeB3I70pQ6tEQsP6SgjB5GRS5bzmGR6jiogf3bHIB/XFMZ2ST KsOec
+  EVad2kiUkk2JJuaYOHGc9qa7Ycg84OCKc+DEoCkAdvX3qq0owxwPcGh2Za1WiB2ONxB3AcE 9q
+  ol9wH8QBzuFTOTI20KykcnP8qqMCAQflwOhppxvZkxtfXcYSqtnJYEdfT0qhNuMrbQCoHX1qy7
+  rsIRh1/Sq0j7V27i2eeB0NCdpbWEna7OBjLrcgyqT34q1GvKNtYk8cGq0QO/5zyw5J9Par8ZKQ
+  4O OeFNaKKixSUlui1Ep+UYPX8h6VdiQjoG5PQ1SjOUwuR6k96vh2CrgjJ65FJyadkEd9CdOHH
+  zZPoO 1WFKYBBye9V1XEoKsGJ9quRgA7SoBx19aXNd6blOcbk65CBSQT16danwR8qg5POagVm2
+  qrLjaMA1 bQqqhiRzwKcrp6oIpLzQ7bv27Th8/NS/MzgNkbR9MilC7SCp5H86k2tkM4JyP1oTX
+  cjn7f8ABJUK mJwGABHyk09B8rBcnA596ZCRtA6sOOKl5Ac7eSeah26LUpSbRYiZSNjIV2jjNT
+  DhVPU44HpVYOS3 KHgdh14qbBaMGs0rajknFpskTJkPG0A9+9T4POM7SaroMRbG5HHNT7hyw3d
+  fWolZsaqST10JsgKN 3UcVKpCSYBPoc1WDYOeWBxUpbc20AMc9/Wrd4vTQcnZ2ROhyWx90HmhW
+  B42j1BHpTchF54Oe5pqn bHzwTVpXQ7LcslsJk5G09RSo75IBAye4qBWIX5sjihWxkEgqTkGs7
+  LZ7iastNS3ucyZJyMYGKAxW VlCsx3dahYgIdvXsKlUkrgHBbrSdkitbX2LEfJ+VgpI6Y61MgH
+  mEBlyBwPWqu4hl4yvX3qWJjjad pbGOByKLahHa5cQncp64HfvVxVBTIK4Pas9XPC5B9asoWUA
+  44oS0ugvZdjQUgyEggLwB7VaQlgwO Ce+KqIyBgRk9yD0NWFbdHlQQSMginH3mRz3diynKEnJJ
+  6j0qwvRMZyOoHeqiHG7nBzzmp0fadwPT pnvUSUraGlu7Lqv84HAOQcCpFTPLNli35VSUBRuOd
+  zcA5zVkNgYzk5GTWlraoU79CT+IqzAk+lOG DjPJHUCoULeYN3DE9COtSBwDsIAbqTRPunqTKo
+  3uPBZQT37Ugww+Vc8cgUxm+fgHavSkLYOTgYqI pmlpJXH7gCFw24npSbgVIJOOn1phb5g4PX7
+  xNIpBlZ/4c4ApuKWqTJfw3bJozxtY5WkYgHK5Oema YDjIU8Z545oYFkUYJwucipi7SuxqUn6D
+  95Em08EHnjv6Um853bScnr6U0H58nrjvSFvlBwVB7Vcm mKKtqOBbcCMNgc0yXnaPmzu9aQnK7
+  kYEHoaYzhXGWww6A0t3oHQa7EggjAA4Pf6UxORxuzt6mnkk RAZU46Co2ZSSQ4HajWzByfLYgl
+  B6McA9aYPvYGcEkYNSO4XGQSe1QNJlkHXvn1oTk42Y76CEcjJI I9aYx+ckEnjIx6U44bkHnJx
+  moJTtAABxkHNS7kryEcsWLAjoBjvSlMKScn3qMsN5zxk9D2pu5t5x 86YzxTk5dNAVO7JFPy9V
+  zt49qUYCYHPTkVAhJhztOfbtS5AMgXOd2atxSbQSbegrsAMLxzxmgFsj CnkZyD0PpTfTuBjJp
+  ruTFlfuk4oa6WFKAfMcsx9M8UwkdCc9uO9Mzt+UHqcZ9qiB2uTtNEvImEls h7FtoA5UdCB1qA
+  suWGC307VI0hOcjAYZzmq2TlyF6EfjSd+U1jGUU7sVmLP2UDnJFVnALYIJ460r 5KjqSfQVCzA
+  uCxGSOMdqmDa0Iv1GEnc2OhqBlHzbvl9z2pzOvGC27GDVc5GSx3Y4xVxWlx2k1dDM qBvHHPy8
+  00u2SMKG3dMdKRSGB9M5xUJIOCTknk+xqFHmTZTk0tR2SsuQMAr1NIULOjA/MwzxUTZV 9ucZ7
+  ntR5gV9xJwea0SaWhE6bvpuSMWWTHLEelM8xS6gZzjBz3o85TICSMAHFRNIPLD8ccAYqEm9 ZI
+  E7rRakm4lmXCnnrURxj5/vZqMFcsTkEnkZxikkC79yt823nPNN6MnlvuSNJk8YHHB9aqM20tuO
+  4Me3YCpN/wA7AgAYyc1XLsycYwy9SKuF9hW5XqhrFN+QTtHU5qvM3ICjJbgnOcUjEH5FPINRMQ
+  xA OVLcYB5qrK97jvHQgZPk7YI7dAaqsCsjgHB75q1ICq8ZfmqTjc53EqCKOfm0voEXdO7OOUj
+  zF+cE dh9KvoWZo2ypXb0xWdGSQPfjNXot4QKE4J3DNae0bCSdrMuwhS6hsEYyAK0Yvmj2kEkd
+  CKpxkM46 YI5AFXEUmIEMN4GM1EbvYybu9CULmJeAFBwR0NXN5BH8LDgk1XjVSSVyST8wzU0RG
+  4koTim02+Yt O/QskZB2LtVj1z1qyhOeQox61XRsDplQcfn3qyAQxkwS3AI/rSumrMWpOQxLbg
+  Cc4BoX7+WcD19q i39CPqQaeOHPQkgcVlGLvfsEJ6PsTL94lTjI6irEbkPh0bb3NU1CrErZIP8
+  AFz3q0h3x8sc7uRVS lfRAn1LC8KDnHf6UsbjyyRwM/eqKM8HJOOQOakT5QCOVzihrqUlH5lkN
+  lV6EYORilUkgbSD9KgDB 3AY4bGB71LnfGAg24GBUxTiU5cr0Jy6CNskAjoM0oBOSGHTg1CcEB
+  iMPtAyacSQgZSG5xTdltcLt 6LQduywyS+DnOaejfvgx5zke1V1ZuQBk+1OVgHJUcAHNNyfUuN
+  +m5cJJZRjnufpSYJCsB823oKgD 5YkkA4596AxKHJwAO1TTk73YlK3Uubl8vkbGyAMmpI+ZgDk
+  ADHJqmADLw+H9D3qRXy4HIyOppyin 1J3TLSkh/vcY71KrfviRnnrz1qupDfNnvU6fMVGPmB7C
+  lJdy+ZblxXQsDjCscD2q4gG1MsDxjGf0 rOU4IBx1/KrcQIj5OBUPR2XUlzvHVl5MmTDcY6GrU
+  U3zFTngZGKoxv8Au++OhA61ZR8OuVx7049g 1toaMUihQzKST61JGCy7eAR0zVIM2Bt+bnGM1Y
+  DAxnIII75oQ4rtoXg2FI9B19KkjI8xx3H61VQq eTnb9alXaXwThQM5HehWdkNJJWJvNcxgsrA
+  5wD6CnqGWQh+gXvUcZDKFB/E05WzknIIPQ1T0dloR a6FBUgDPvTyxzkABQOtQh1VjgcHOBSKw
+  8woT9PpQrvVjlAnRlCHHzHdTSPkLcjB4/CmsF8vjOAOv rQFQIM7sEHPNZ8yvuCjyq7GrIS2Oz
+  HJp5kCH5mHFRLsU4zyBwfamsCAG3AnFaJxbsXzJsl+8MFgx BHSnbsSAEEjHGahRv3OSQWzk4p
+  u4MSec9AM1m1d3uNkiyElRwo7DFMO4jAIZsZBo6qG4zSAZdWJH XnFXzaaEuKirsRuyngk5zUT
+  OhdCFIwMZ9akdzvPTj261AwO08HcOnHFKyW+j9SkrLcj5YAqRjFMw pXcGAYHAqQ8n5QNpHFQk
+  FkBQAE8nNCm0gspDWUFm5xnmomBO7d0B59qlwA5Qg9Ofr7VCTtRiSfoe 9SpXVkyXroMccEHHT
+  J4pmQY1XcFwMk0u0MysTheuKiPRiULHGMA1XLrqxqSTSaAyeXFxjrT92Y2X bgdSR2qDBO4MNo
+  B4pwZmwQQvrx1FVaSsyZWtckLZTtx6VE4PlkZwueBipN4VT0AHTPpVZiruMucd cE4pRm7jWm4
+  0nbCQxIPIFNZgB8xzwKQMecjg9CaiLDCknGOufSm5N9NAlKLsx+4spG4Edj6VE7bl U5LAc5X6
+  0jN8+ARsx1A601G29BgHORUpNq4p8rSbImIR2AYgHqPSoWJ3YAPrT3YEMRjrmoCQS+GI JpXuh
+  +VyNiS/ODt4+tRZwgxktnk1IwYsF3Dbjiq0nB2c4BBq4S3QOXQa/CnjC54yOajI/d5AP5VI ZC
+  FcfeUtkH6VBJJIXDBTt9BUpyetiOW+vRDB8wCk5HXIppAMTBSODhaQvmQj7vFQOcgqeBjPHHPp
+  VR10RXs5Rt9485wSBgg85/nR5gORj5VqKNS0LKTgHpzSNjIXJVfX6UNO+opqO7HeYfMACk57+t
+  IW QuScHA7U3eABxyehqsTlAAGxuzk+lOe3mRTS5idpBtDYZc9agbhz1Cg8Uudm4daiZgAoYdO
+  BikuZ 7lp+RDkl8bSoHrUL7V2u2ckZ4PepGV+X5yemKruchQykHr7YrX7VhcuuhHIwzlTgexql
+  Ix2Nhhz0 Bq2xBBGRuPI4qhKxDk4yB2p00kyVHXU5WIPgA/Kd3TFacbHYM9CeMCscvm4AVmZFP
+  PbNaceMgsTg t096cIppSe4oQdveZfhU+YeeM8CrUf3mYfdPvVeIncdw4Pp2qyqho9xOPl49qa
+  31B1d7F2LiIgkZ xjHerYOVAY4XntyKpL9xVzweCcd6tDccLwAG5JqJKzUuwKVtbEqn5gUxgcc
+  +lWI/u5LZ5yBnrVcB t+Bjk849KsIACUK4zycGkpNsi2t7DwckqMktzmnD5hnkc4564qLJLjgh
+  exFPBbaMnopyfeqcXpct Su/dHsSzsNwx64qxESyBcgKO4qqB8vUcDp3qdXAI4KkfeqbJ+Zb8y
+  yrNsYgfP7VOrYUBmJY/rVJW RQ5JIG4Y96lWQPGNnDdBnvUTve2we0didWywC7SM5b2NWFbMi/
+  3vSqQLo42kBe4I5NO4JZt25uow ad76kc3NKzLgIIO7JK/ewabuwMgg5GcHtVbAC5IIGRg5608
+  Hk4H+9mk7l1Hd9hxyuMnGTz9alU4P tnJNV0O1/mDHJyfapI2UrlclT696mV2hy12J94BYYLEn
+  I9qkJTyzhtxJ6f1qtyZSHODnqKdgAk5z 7UJx0voJSTdkWS6hyQcfhT4h+9y2eDwKpqGExYsCM
+  8CrCtgEEng9fU1F7PRluLtYthgpcjIOe9Tx bsh0OCfWqZYspPXBH4VYVzs+8oDD8jWqatohpO
+  yRfUlQTkAk4GatRnIznORgVnIExgszccc1Yhbk FidvQKDWTjzEWT2NKNmO4NxgdRVgHLrwTn0
+  qipWRv3bDGOc1aTy9u1m556U3ZO1hve5cUtuKqOCc VbVSAehPQ1TiK+UD2xVuMk9hgqKcm3r0
+  QStLoTJklSwzxjC1Lg7lAIVQOM1ArFMsMdOB7VIrgxqS ec8j0qk29hWcWTpkKMMB6e9SEneDk
+  HnBqEFdoK9hkCnZLLlVbJGcmm2+oJXWmhPG+ZmIC8c8iohg yjcBk9KcG2Rg8ZxyPSkDqcDgMe
+  pFTFNXaCKiODYO1jgLwR60KN8h+b5cZ+tMYswXaBz0NMLFc5Bc AYODilyaaGyjpZMmZflxwoH
+  eoxjaf4mB4pT+8AIOBtzg1AASCoO0g9SaairE+9Lcn2gcgH5l7dBT VZApbhiDjg0zls5bjouO
+  KamDn5WH9fepkronybJfMAk5+Yd81FuYEkHPPbpQW/esAMHbQMFkG5Sc Z4pOKaG1FbiYfYGYk
+  U08qPmKgDn2pWJVflHbpmoHLAAjPv71XvSe4K+iYhOGyOw6CkyMhiMEjqPW lXOct1DcZpjDL9
+  T1zUpp+odbXGM2wl1OXPvUBILIT09aectk4/D0prAbQOrY6elKLj1FfR3ISylT jJCjFRMGVXJ
+  OB1B9amcEJtUbsjJA9fSmHLR/Mp+lOUtdBR03GphHLHJZhjFNdnyNoAH0pGKonck/ 3jSO7Lyg
+  GAvI9qbb0E0ubUiZ8SHcSQTgYHFNJBBUsu7AIGKc3zbWUnGM/SqxY88jIPHHJzTSG7dx zNghi
+  eAOMDrUTsrSA/eHTA707dmM7uD0AqEKxXIXAB60PVXvYmLsrLccVUIpOc9jnioyYtuBk8fN z3
+  pxb5twO4jjHamEfeIGB/OhOyTuDhciZsqQoBNMJznO0euRTyPnycAZ60xuWAJGAfSnv0KurWKs
+  jFcYPA4zTGwTubn/ABqdii43Ju5/CoGBEXHXGcelSlfQr2t0QSMVGMAsB0qEO5UnIz6YqTlosD
+  rx zUMshUkDH9aqTSurERtuiOTb5YycMDjNV9xwTkccHFSuysfm7rkc4qH5TDwCCPyoVt2Cm3o
+  MGQD1 BPIJody7M2cLn86YzKYs8l88AGneYwjw6YHvROVlexXNqMZkYKqdcVG+WTb368GgqPu5
+  wB3qMD94 R3xkGtoqDs0wtZWuPATGMtk9CTTC43MAR+NGAB7fyqJt2PlGQeR71D1l7pDV72Gqj
+  LIdxyR0qJ2O MLgt/EfSnklfvZHrmoyDypXhl7VMoO+pm5JNMrsMxkrgtjvWZNkQspPzAf5NXi
+  eOAwYDnFUJs5Zc 5OevrW9JvpsXFOMrtHNwKAFBYMScjHetCF87Wbam0YOfSstFO7cSPp6VfVs
+  NgDdu5OPSs4qLd0Ja q5qxvldxwUzzVhY2Mqsp3DHOKrQqRlScjsPWrilhGQN3J/Orje/MiG0r
+  8rLqgbQeQccg9jUiFjuG CTuAxUMZwuHwSefepkJDll4c0ScloKCstSVMA8ghgeecUoblMZwR6
+  9aiAORgFjnluxp5faQGG054 PrSUXzaDdtEiZT8uPm5PQ9qkz8uN25h1+tRIMyckZzjJp7IpkL
+  Ek5ORipvFsttbk5kAx90NSugd9 24rzwPUU0FQG3dB1NM3dGB+mO9KnJ3skCT3uTO2U2EfKp6m
+  pgVUgJ1B4J71W3YOMFjn9KlKn76hv qe1Nyv5BBPUsB3Eg+RmKipN4+V8HHt2qDLBSQQSOpp4I
+  P8SjPNRFue5UoqLLDOcKu0AkdaaNzcEg rjFMUEDcpL8/L3NOyrEhmAA6g0pNJ3QLmXmKWY5+Y
+  eoxTg4C9Qfp61CVBG9enYCpDgKTtx9fypXY udOKDzB5I5781OHEjEoM88VWiKbyB+JNTK+0n5
+  MDoDVW622CKS0sS+ZsfdkYPQd6nV2JGcdPTqaq 7htUnle/t7ipVQkKwJ2rkY96W+5bk3HYvq3
+  yqdw3Z5WrMcnzdm4x0qjGNoBYjGeo71bDDIUfpURm m7WEloWlbDYxwO9SIwyrAH3qBSNp3ckH
+  oPWrMZ3rgYJBzVRnZbBbsi3HgkscYPAAq2iN8wAHHVjV FFZl5YDvirIBwu3K/Ng5NEn5iXxWu
+  aSsoVFBwvcCrKEAtnP0qggJjKnbkDOcdasoDsUg9PWnN3Wj HZbEjt/CDwecjtVlSBncPmA5NQ
+  BcFQVOegp4Y43ngHr7Uoy7FxkraEylcHHBzyDVne7TFVIA61VB woB4PBI9alz1ZelEo66slyb
+  RNht+5iMY4wKe+1o9xPscDqarA7WQYzxzz3pzEhwGUnjnFFpJlXF3 7DtByMjHrim7ShJHIJzz
+  3oY7WG7oB6UhfAIyNmeDTadrIcJauw7cDxtJUDk/0qPy24xnceoPamBg rHJy3UUquTIxJx7+t
+  JOUdkEnLuOG3zfmDeoxSfLuY/NjPODTfMCyHnIIwaY2GYkZLEYIBoctrkPV 6inAYsMkAcjPNI
+  rKr+uO9NHQEEbv4hmgFsDcoGGyaHJNWKkiRmQrnkccGmE/KAOVApCQCoYgZ7Zq PI3YXcDnj2p
+  uy3Jd2tGDZGC2cHrn1pgYgZPrn1pc8sXwwzjHpUXykswOc9qS5Wxy0Qrk7QFYMSOv qaiJUD+8
+  3c04hD1JJzxg9KRVz0IwTyPSiVSCTfYhy2ZDIu3ocE8ioy2EAY4APX3qRvl2AfMp61Vc HcB94
+  fw+1SrSdmXGLkxjjdIkhODjilIOCScHOPanHBAOctTWweeDx+taXb2JmrvcR8KjBcg9OarF WB
+  5wf73tUzMSncc9fWoGJJIUEnHBPem/dVib2dxkpbegUAn1HekYZbkbexx0p45kUlh6sMVHklWO
+  Cc96zUrrQtSXYa0ZTJUg4681EWcOA3TsfWnO+IgT1xyKaSdv3SCeMU+l5BJNIRmAYgjd6H1phK
+  rG ASMAdCacQPN5YEY4xUUgHBIJA6e9OEbbmfuN2sV3zv4OVJPJ7Uxcqwyy7duQcdanLNvZSpw
+  AOKrn DBsrsGOAaOddTXV+hXZgcYYFgckAdagchs7Rk9Qo61OxCyA8MT1qttOeCO+D60JrbuDV
+  newwg7SQ Ac8cjpUBkJypAU55OKldiqbXHU5qqQGlY5znGMd6laXRiovqIwKuwIwAfvAVCPMAI
+  fLMTkGldynG QADgmohvZevGOfrWkNVdm0bN3X/DjxzGcMAT3Peo844OSxHPtS7ioIABY8gYpA
+  Bt3Akdcj3ppva+ gOpaLQ0BgAOdw6/jSO+F2hTkcDmlLsY1XK/722ojlZgQMjj5vU1XNd3fQzu
+  t2DszSnI2gDkY61Dy Mj+NutWS+ZCRjOOpqgWBdwFJOeTQouXQatezRC5LHpgDg8VSc7Fz6nqa
+  tyA/Pk5x29aoykkHd8oL fLQuW2oSbscvEXB3N0HUHvWhEAATjHGV5rNgceUzltwPQetXomAUM
+  Qxz94U3F3aasT8PwmxGSU3b vmOMe9XEfaoHKjOBjnis+3G4ggjYq45/lV5CCwyvy579qFCysh
+  Xu7dS6Dld2R14NS5CEHIJJ4qoA d3UYHBFWVIbjac4x9fehwT3ZK0JFdgeOFzyalCKTnDH0qDG
+  Fznv0qSMFgRg8HjNVL+6Va71Jd7KR 8wJPQVKWAYAdCcE+hqujqJRwNygjPapAGweMemai10KS
+  8rEgQK2c8Hkg+tKPuEkY5qNVwCS3U5NS bCBgklQeRUu17FRaHn5lxzn+I+tSoSFIBwB6+tVVf
+  ORjBzhfcd6njUM5xuwf1pTWlnsOcblnduxu UghsfWkOBM67h97oBSKTGCf4u+ecUiBmf5uAe9
+  EYJq/RCT0vHYsLuIBJKrgjPYGmtIwTG0DI5OKX DDojAY6noTTWfcF44OaElo0aQemmogB24BO
+  Ac/SpWHDBWzznB71Dvw5wCASMj3p7EOcjIYcnHpV2 bJcFLqSEKZAo4HU49KcBtAcE4xzk0wMh
+  JxnJ64peUCYO5AORWEZu9gndLyLOAxwMFs8VLGWwvXB6 1SXesi844I5qaORiegwBjNU4y5bvY
+  bskncuq245XO0Hmrsajdy3zHkc9KooPlJ6BQB9TU8LZ5IJI bn3FTvpHYpu0dy6rkoqjBz6CrM
+  YbaT3PcVVTZ1Ugk9u9TI58xQcL/e9qpXtYWltGaEO5lJIG7PQd atKAY1bsRwKqL9xQA/Xr61Z
+  ickfwlQeKXJLdMSRbT5ARnag6GrAJLp/e9+9VUfeCcHnpxU5diBu5 7Ad6qFuZ33HFSUtiz5jb
+  23HhjwKeoVQwLfMBjGelVjzjJwOwqUMdw45NJJLbQNVt1LQZWIODtI5N P3oMYI56k9BVNZOgJ
+  z3x61MAShYY+hokrEyunqywHUD5myCeo705STjHQAgfWq2/quDjscUuS020 EjA5Oe9EdtirW2
+  Jzgq28YYjjPpTcbcDdxjkVG0mTg8sMA+1EcqLMSQxAPr1os+XQlybdkOZwJPmX K5GPpTGYLJx
+  1I9aN5KjcFVevNMbeu45UjORx+lCkirJhkiHaMZ9x1pCMMmWGTyCO1RF22MxTPWgP iE8AEf5x
+  R1KvfdKw7euCcMMDFMySmwsGJ6GoSSAOhXv7GgsxYlfl9eKTTuHLtaxMSWy5IJHA9qYC dzHID
+  DpxUO7Oed3c4oDgxkkgMBjFVJMlwWo9m3AABgAevqaZtYMTn5c8UEqDhSffmoncbM5AGMcV F7
+  6WFboPXBlHOcGkLnO3BxUGSGU5Bx0AphdnYnjj0qlRT2Y4au9ywx+U8/w9PxqNiDEW6cimLKS4
+  LbdvUkd6Vj5iuSAVJ7UJNOwnF3SYwcg/UEVGzH94QdpzgDFPHTYDt96TehfO5cdTQ3dkR0ukVi
+  Qs Pynvj3pN/wC5YL1zg56ipyEaL1btioXVgzbSvXoKlyj3EktmyAqythTzjmkYsiK2Rk4PSpM
+  KEOSd x/Wq5yHAAyB0qU5PU2u7DmZc5wCfWo+THubnBwMUH+7kDI4yKR2xGAOfWr5Laoydo2Eb
+  arBiSCeM VCzFmQHsMfWpc8c9e2KjfcTkOARV3TKtdDcgjdzkngVUk28lsgnoKmztnbecY+6Kj
+  b5n3EcEcmpb irNAlZ67FVmUM7gDBHeqzE+XnrjJwPrVqZx8uQSuB+VU2fdJgKetJy5TS7b22K
+  7kc43FTzzVd5dq Y4Ge+OtTSBQ4CnHt61C5VtpAwAeAeorRxW7J5o3vYPl3hB+vvULZIC7Tx1N
+  K2dmWyQDmkVkZujZ6 4rNcqRN1zXEEny8DvlcjtURkG4b/AJVPb0pzkYK5yWGQF7VCS0jbSR26
+  itb2d7D5Fa6HEkMSD26e ntTS+dwB2kcn2PpTG2iUjOPm7/zpp/iLE57+5qtRJXsmSFsBGP3jz
+  VRzk+hxyfWpBvaLocVE5UPG OhBxTp3TEmkRuwE2Wxg+lULggN1wMA81ckZQxU4K5+UiqExOeR
+  uQjrjrQtdNrgpdzlLcLt2nue/r V5CDcKT17g1TVmEYBKqSCcEdKnjyp6goRxn1pxmpRV9xcrR
+  sqSIwFwcHnFWY2YcBwQKzF/eR9Tw2 G9quptC5Vvm4xnuB1qFbW7ErPQ1g4OC3c4qZJcSbkPAH
+  f+VUIwSmVPfuasglUzgjjOe1WlGWjMuV bssmYmFRgcYzxzUivuIXlXHBqHLBjnAA6kipQqgoc
+  8k4J9aJuNti+aNuxOFUHO3cR90jvUse0yY2 sGAHWq4DKxDAlQ3B9KkQcr13Dk1k0mm2UpO2g5
+  QDPySd2fyoO7eu4nBFI3EhOcL0zT2zxyDjnFCY 7dmPGDjAO5epqQf3s8YxioEJ8tTzyOR3zUg
+  OUwzYHXP0qbu9rijpLQnX7xLHginKwMeXGCTUKA7Q Mg7jU552pj5geBim7LSxd3fcUM28bySN
+  pwBSfOCAD25GKjLOW4+THHI6ikjU/OfmLA8VUYpapgpv XUnBAj+YZYnj2pW2FxtIA24z60Fw2
+  Mqxz/FTCVYZA2gHPX9KNXrYyTe9iRGRdxYEgDjFODqUyAWV uuOxqH5ZGbAIBHTPenKxCJzg9R
+  7Vnyxi79S0kt1qyePGdrA5xjJqWNV285GTjmojk5crtxUqkG2Y k7z14FP2lxxeti4BmE4557V
+  ZR8EYZeOnpioIyFOBgAjBz3qUDg8AjHbtWSnG9jaLi4l8EBV6MT/d 71YABbI45qjCFb7pIGOR
+  6VawAB97d654q+ZNkpR2vY0oxmHk8dRnvU0WDJllwM96pRD5gCe3HPtV 5WXCBlOSOue1Jc1tD
+  Nx6LUn8shixb5cZODUytmIdMDoTVZDuBwdwJzipWKiLGCCCCaHZ9Q12LHmf N8oUEAcmpFLlmx
+  jAPp0quDuUkDL5FTLkPgZG73pylG4XdrJC5AHzcMD1/pUqOcDLDkflUDZMzIeA uDk96RDgtxw
+  OgzV8nPqXJpKzJiWLAnB9cVIZG3HLA9s1AN23Bz/hSY2p3Yk5Yil7t0Q5XsiZD++D MwPHPvSg
+  nBGB1yCahyHwRkDHQinA/MduSDyD6CqcmU273uSbsyKcbgTSOxy2eFx61Hu3IMHJA59q iJBY5
+  BIPIJrJJp3C33Em4mDryp549aZuA/2Wx0Paos4yM9emKXdn+JQQOBjvTkne4pX7A0uZXB5R ue
+  D0pA2EBDHA61Az5IIQ5Jwab0Hy/Pxzmh9UEGm9SYuBk5xjj61BG4JI7Z/E0zcMckEn17VCXKue
+  wzwR0rS9o2YOyLJkG4rjn69KYGCtlhnnj0quXbeSB8vGfembgWVhnJPc1HvdGOfTm1sWgyqAc5
+  5z SkqVDD5SeeKr72YLhcADoKRSzsRnr93j86Ol7hJR6FjLAAgDIyMAU3ewjQFSF7ketRlmwOm
+  WPIpy ksm0YUA5we9Ta24003ddBWcA5/p0NIrAKC4UkjsKQkLxtLHdwR6U6QgQBgCAOORVwu1a
+  xKvIYGHz Y5HsaiJcyZCkc80rn5xkgITkgdqjLeYCoODn8aU421Y5JSkrobI53YZCee1QgrwcN
+  05Ge9PYqsIU /M/cUw5ZAQRVr3V6kPYYxAm7n1wcUEgRMU4U+v8AOgsMKWIzjkVE6kng4Hfnis
+  5PYbswZioBOHx3 FR7gz4Gdx7D2okKiJuTnoTUAbEhAPAAJwOa2i9DSUdE0xzk7y2OAeMjp9aq
+  vuZAM9QMAVYEm5Sg6 EZqAgeVgj5T3704zsmmjL3272sRHgnLDaowc1VdiR8hHP3j3xUrqC2GY
+  sSc9euO1VmYZZh8ox0NZ JXC+mpAzjaFK98Bqru2A5XGCcZxxUhy0o+7jGM9qiYbot3Bwegq52
+  SL1lZCMvzjJ+UDmoCxzkrgA 8mlYtglgctyfbFJIVXf8w3HHFZp9EJaaDfNydwPU56U1RmZstt
+  PPXvTQSFHzDI6D1p24hgGGTtNW 9CXGw1SAdg5XPJpPlDFEJPuaa20LlGPH60Fj8zgj3x2oe+r
+  G72uMbgkk5QnjFR5DjAPQ9+tBYHcx Ofl4AqM+oPPc+tW5XVzJS11K8u3e/GFU9aoyktje6g5/
+  MetWJGYI2RuGc/Sq0pVTlFJ479jVOTlG yZautf8AI5SMtKRwR6A96uIMRkvztzgVQi3ZJzkMc
+  DHarS+aCfYDGRSg76tk2cn2NGDLg46Edv4q tIWBIUFRjlj/ACrOjkOGz94A4CjGKtJlgNxIXP
+  AzTs7astrlgaSEh1O4qDyCatpuEqh2Jfb09az0 KeV3yDgjPSraksDI5Jx6U4prXoRFrYuId0o
+  BJDHOM96srgJt5z3HrVIK7Mr5AYcirEeTnuTgDAqb LdBbXcsB2aZfmO2p1yIjtOXzwB1NVeQw
+  QnA5O4jgVMpwwYn5QOtHu9LBp3JlbABCtjHPFNLfJtJy yjkAc89KGZxvAPyjHNIoOZGLBvXFV
+  G6V7jSVtCdSuxcNtcgcmn/dIUAMevFVQ5WLOFHapFLBtzDg 8E9vrWbpu92aOF47FknCphfwBp
+  4bMYf5s98/yqJPvbicANjrSNIFyAOjc0lByZMVJqyROwVlO0nr gUAKrhQ2GJyMmo8hWYtnO7j
+  3pw+djgEknjFTy9wimtCQFi/JGAccjrSgkvkfdB6VF1YAnjvz0pFP +kDdwo6Z7Vak0i5OKehI
+  h/dEcbi3GO9Skny2+XjcMYqsXCttB6HginjcioSCV7+9Z1IpPzGnqtCy WDMx3deasBsY2jO0Y
+  PvVPzE3jIyuKmhYs+Typ9qhpJik4vVFoHJ4ywPbNXkPyYbIU/mazyMNuALD 2PWrYKkDkqfQ+l
+  Obi2mNSfQvRHPygMBmrCu+/DLuAbg/1qou/goRluoNWkIWXG8EDgiiKT95jcW0 y7ESQSPvdDV
+  xQUVQysR0+maoQPjb1GRmrMbnbnJGPXmh/F5Ecr6F4Hy4Tzk9x3pUJfGSMDqMVEGx uZj6dafv
+  XaQrAgcHHWk0gu7WJsjzRtJI9qN37veu4E9aqpIN2Oq59amG5WY7sqT0rT4dRqNupYST OflJx
+  xUhZVjU4JzwKqxHI3MduTkink/u2wQcdDU/MTUWyRmZioDdjg0FyBjBJI7VArDcCM7s4p4J bB
+  bIbg1EnYNVp0LGW2bgdxxzx3pAxL9QGPXiq4O0sOeT60iFlLbs4/h9abcXqg0ZMZSIsdTjBIHF
+  DkeXGD90Dj3quzjeQVYgjpS5XYDg5B6Z6Umr6hyajzIWITjIYYNM5MjAnoRzTC5eY7SMk/Lx+V
+  Re Zg8tznJq3toilFW0JJCGIAb5hy3tVdnKMGwfmPTPanMAZHYA5LDAFQOT5uCc4PGaFq7MqPY
+  eSDnI ZQOBk1A7nkxlQMdSP1p5ZjGSRk96hZ9q/Njbjn1xTjduxKuhWcsgBK9QRjvSbgH6Ajoc
+  DvUGcqCO gPSlyhcBj3zwalvoyZJvRonZgAAThgeSPWlVgp6E88c1XEuG+ccnue9CnccqRgdhT
+  vrqwTsixE37 3kEHOeTU/mZyEA39c1TEkaz7gWz2p6kFgUYBei+tDjZ3a0CUepbA+QkMCB3xUR
+  J3krx7Z4qNiyFl PcY49aUMhUZ4yPWnDmtcLisQVLEdeMmmvnawXHHem4DHhjjPAph8wgYyfXF
+  S9VqNOMhJCSEPAzxU OSHb+HnPPSpSNpyPTjPrUEj/ACsAOeBTbVhJdENdhtTZtb+8RUO4AlSe
+  e1K4ZITk44qDqSw+4OAe tVpYdl0ZKzA5yTioy2NwyMjofam9X5yQR096iZmUsTjavAoTjazHK
+  0WrClhuwOucZHpTHD7Gw459 ulIz/ug2BuIOAOtQblRck/Nt6ZqeRPYUY3I2l+ZdpUgDP4VXMg
+  MZAUnBJBpAzCViFyOhpu/94eR0 xjFXyRWlhcq16jH24Vty4B6Adc1UkkI3JhQAe4709w/PKbB
+  3xVZ93m/Ng88E0RjHVFQ31FILxgZA 44GKrF1D/Nz1GfU1K+4jeO3FRsBtAce+aatorkyu3zDQ
+  Dx37kU0LtZiSSccc08ZUqOQDn/8AVTXO 11BGPX/ClOWtgc3JaC7gykg59qiB4yWUKRnn0pec4
+  4A6moZWG0EAYB5I6CiGl0jPmauhQY1QYPP6 VGZG344A9xTSf3TBCAcdD2pj/KQcj0OPWnNt6W
+  uNxtqROWxhsbjxnHBqnL5iySE8j+H2qVjtUt82 BkcnpVZyzEnO0gZwTUrnirrcqk3FnMoFZSM
+  nIPPsam3MqZyc5G6qyFDtByB61KSwK7fulT1Nb02p PkbFCfOXBOD1YZ+nXFXbeUmND/F/Ks1N
+  vm4PzY4AFWomKjA4IGeaThFaW1JaSdjTjZSCFHG45zzj 61MjgySDJOOlZsMrgeUFwauI24ggB
+  eOR60rSStYIxcXroaCy5QY5bp+FWklGAOQV61nAbnyCQe5q VQiyHcwI4ORRypPbUtST3NMN8h
+  JU5wM5qRJS3yMPlAwDjrVNGBJ35H49qkRwQevqvtVciT0Kv5bF osQNucZ7U9WY4I645I7mq6k
+  EAntyc08PlvMAxyciqTSexE2nsSxmQjgKTnk4qT5sNuPfp61WL/IM kkjsKkTax3fMWHbNZOLS
+  ci2tidB90BiQRkg1YVhjoMZ9KqAgAqM5wATUwRRCGLcdeKzlNaLqTKLW o/zMR53oVye3NPDNs
+  4YAbutV2UeWQAVwRtzTztb5NxY5HT+VaqXLG44vlSaHnaYiQQW3cnPFCnbJ nIJ9xTPlRDkHIy
+  eaQujfN944zgVF27dhqzW1yQyq2xM5OetOUzSOSBhe31qDzNpBMZweoxzVjcfL BQrzyBUybto
+  S5vsSA4ZiSd2fwqyPmiYAhXBqiCS+Xz6nHaplY4CgEg1EU76Cv1ZfRxsIZieRg1bj dTM2RgDu
+  azI5CJAxPy9GJHQ1c3Hy02+mC2M5ob63K5tDQibDgEH5hn6VZjI6Lwcck1RR+FUN8xGc elWVY
+  7h3GODRF2K23Reib94MOMhcgYq6shBIY8HI+tZiY3jHLY5x2qwrbiRnqauotnuKW2rLgkYf KC
+  ckdDVhjj5Mc7dwqisgG2Rh16D+dTGTc6nhvQ09biU1cs+YDtBHG3kjvThKMb9rECqxZhHy6g/T
+  ofSnBm8plbkg8epNFkDkm7ItiRWdgSMgYwO9GflxkYYVAM7gDwSckgU7cAcFx0NZKLvfcXXQlO
+  0K Q3zD245pDNu2kcHOPoKrmRQwRiXGM8HrTy2JMAqAOuacoq1maKyWo9WZXBYgnHSpeDzkjnH
+  Xiq6u zA4BJHT3pWkURnggZ5FKdrabmUtWmTl03D+Jj+mKiZsPkKykng9qg3MjoY1Zwe4/lQST
+  FvOd3YZp xSTsy2rInQZJzjI6c1FvVAey+4/Wog5zvORk460oCnJORgc+/NUopJpkKbUrsVpAE
+  65weTUb8kk4 B64PWlDYBPBXd0qu7MA2GXI6Aj9KlRUmXFtrQC7MeCMY4pCcsSQ3A7UbiAASOn
+  YUxuFBJ4H605WS CU76XGNjgYO3tUeflyGUYPzE9qCQyEbu361BuInA5IAzz3qqabjdFNN2J8/
+  KOdx7VIoB+YEe1VlI LKd2MHjPepuTu4ycZPNQp3eo5/DclHI5OR1z/SnoQuGGNpORVeMHAJwC
+  vT2pzMwj3ZJx6Cq5r3I2 67lnfucjIzjOD3pPMbLJ8u/OR/hUO5XjQ888/WlDK7Es2DnPPYYo0
+  S1JSTeqJTy69jjioSSzHk4z g4pxfMTHjp8opm44APB6kd6Oa6uO3RBIQGGM4Ax1qBypJy4Az0
+  70rP8AJ1yQ3PtUZfJwAMepHein a9hra41i+0Asufeo9xMJXIUDBzimtJkAnp1J9aY7Mq4JB7c
+  d6bTfQGlLqIznLZ7ntwTUDOC3cknI 9qldm8oAKC3fFVVYb2JIyetSo21auIWORTKQWzkHmoWw
+  ASGzx1NTOyiM44B6MB0qoVAlJzv+Xrmq VrdhOXYQsHQ4wr+lQH5lY5A5HSib5ckZBGDTVfgsS
+  o6np1qpNdCYySdrETAFT83IPrVZidzNkHtj FTtIARhSQc81A58uMOgJJ4ORRdtFvfyItwYspB
+  OB2prL/o4LHljnGeKU4YnjBx9KgYqI+pLA560J pNJEtdEDsfl5AGaRtvmMfmPI6GmuS6rgZwc
+  emTSrw4O5cE/NmrltZEpaNtjQ5MZPJB7Y5qFlxGzI cHuD3pWYtKcDJPSq7FgwQsGIB6Ci2uhX
+  PZWHO+bfI4yO45FRO2QQx4OMmnsy8YOFPXNRMwwU424y OKm7a02Jb6WK7kqzBsHBwM96rvgxb
+  jnrxUhO6Rh94YBzmqUhbywp+bk4Naq0ZJX1Hfn0OaVmBw2O OelSCUNtKHcvIqrHiQ78nKnnnr
+  VxDhfkwPTIpKKb5uoc0bNrqTq+VYDO4kfjVrBJzzjg7qrKSrkM ue4x2qwjbshc4PJJ9aG1cSl
+  ZbFtWCyEg5weoqyGBjHXLHNUlZFBAYccEVZRiHxjgD07VD91Dbk1c vrgOBndyCMVZYgMB1GDt
+  OKpoTgBCCMfL/jVgblxlgSDyxHFCfVCk3uWkYr8p5JGcjoanJwPTjnmq qEgKQRycCnqxySxUj
+  2FUtWJa6l778RAYcenehThixIK5waqKQV3Ipz9aCxymDjHUUkpWZcWnp0L2 7AZGxjtx1oBHlk
+  LgHpzVXeTtZ+BgYAp4kB+bgc85ocJLYOVK6SLqkbwrNyVxxSkjYOcDIyKqiQ7g WxwMcDvS7+j
+  jle4qfZtvUtKz3LIYgkHLAHpUuWRC4AA3DHvVQElcHC89TR5hGd2WA5FW7WaQra/o WncswLDA
+  waiB+fPPHpQZAzEHAGR/+qotxwAMH09qhb6ExbtZqxM8hwzbScfrSJJ1G5Rxx7VXL/Ju H5e1K
+  N4fGzI7nFTy2+I2SsXQxfcSdw3DO2pEdhls/TFU1fYjKDwx9KsIxchPup9OtTNSvqZKbitC 5F
+  Io2jgluevFXRKTIEJwO2Ky4yAcKQVBJJx0qzC28lyDg5/Cl7jfMLmcmaQbCb8jhueeatLIzHrw
+  elZqksSCpVeM81ZSQKMD5gXz9KSSvoyoq2xphwp8wn5c4wOoqZWbPDDJ5OOlZccn77BG5Tzx7V
+  dV wMEcZGTU8/Ky57abl9GUjG7d7elPWTqvJJ6Gs9HbqBxg4zU0LuwUEEgLkt6GtL2V2yaitZp
+  F4N9z JJyccVMCobnkY79RVIvhVPTj73anKxKgnJ5GeetO8rXuKMVqjQV/lPzDJqMygwg7SWHB
+  qMSKof5c 5OMelJhd+7kDvRdaihZbkhZty52gZ9Ke0gyeRjFQAOJSeo4xn0ppYYAP0pWTVjRS1
+  0LBmIfCHHAJ JoeXEhLdevHvVbdjnHQflSblKN1YD35NDjGJPLbct7yqqPfJpTKM4PUHt3quGV
+  iOchvSmJgH7rHa MHmp5Y31E9XqtiXeS5JXqeMUrKzSAbdox92oXZnU+x4pu5mdxnGDketXEpw
+  2bH7nLjO3AFNdSpPB L9aYWGGJPWo5pG2BOcj86Uua+hV76iuRvBz0HamBizNwRt9+tBx8rbXA
+  2VGwKsPmOxuQauLurDbW 0RJCBIpz82OcUAEHhgVNMBO4MT14z6VG2clFDDkEZqeS79A5+ZrXY
+  mfgLjv+lSEEREr8pz371CCu SGBwB0qMj90x5GW4PahxStYlSu73LasScMw47Cl8zJTuuccGqw
+  bDh8hmyQRinq6iNhkKO+exqGnb QN3ZFkkLEMck9famkgEvkF81XD8rn1/KpGkVkJKkDuaTi1o
+  Db2JCB5YGTu6ioncHaeQT1/wpuT5K tzwcnJqMOzBtykKTnPvVJcruhRl+A5mC8DJz3pm9lG11
+  IZumaU4AJYhR1x3qJ2BbeMgD5sH071Sd 9LCnNRFYKVCg9Bgiq7MFlIc4HT6Uu9ch+vHTvUBYt
+  IzldyZArWF0rkpNK4M5KEg9DnH0qENmDpli Tx60skp35UYUcEe1RO4weOMYX1qJPsK1la2orE
+  BNmd2R3/hqvwAVB4xkihpBkDn0YUw4ZGyMccYq oRvuNq71GHexJHzY9aiJf5m4PTIpzMyOCAW
+  Ug/yqPK+WE2NnBwc0QulsU7Kz6EW0+SwLZ9R+NRs3 y4ZsnPBFOH3/AJQRxyDURwM78Ar2pv3m
+  J2bI3I3MAQA3vyKiLHJC8sODTz87EEHnmo1IBw3IzQ2k tR8sVqwHACBucc+1Rs23O0n3zTcru
+  Y5O4DkZpnmbc56+9K2l0Q5/IduZW+XHGSeKruQWDZySOR6V MMKBkj3NVzlpCSyZI+UAdKFbuV
+  HRa7jG27cLxnrk1CThWHJwOQKkcbtuMFfb3qu0qguFU5HynFXL
+  l5dTOd1qxgYfaS4U4KY4NUJA23byzdMDrVl2D2+5c9gT71Rk++cB85yMmiEXF2Jb5ndH/9k=
+UID:934731C6-1C95-4C40-BE1F-FA4215B2307B
+END:VCARD

Added: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/AFBB77B8-0438-4825-A1DB-A75D76B6C3A8.vcf
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/AFBB77B8-0438-4825-A1DB-A75D76B6C3A8.vcf	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/AFBB77B8-0438-4825-A1DB-A75D76B6C3A8.vcf	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,25 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Here;Custom;Fields;All;
+FN:All Custom Fields Here
+NICKNAME:custome
+ORG:Major League Co.;Macosx server group
+TITLE:QA Engineer
+EMAIL;type=INTERNET;type=WORK;type=pref:custom at example.com
+TEL;type=WORK;type=pref:777-777-7777
+TEL;type=CELL:8888888888
+item1.ADR;type=WORK;type=pref:;;1 Goroku St.;Mountain Top;CA;99999;USA
+item1.X-ABADR:us
+NOTE: Many customer fields are added
+item2.URL;type=pref:http://www.example.com/~magic
+item2.X-ABLabel:_$!<HomePage>!$_
+BDAY;value=date:1999-03-18
+X-AIM;type=WORK;type=pref:custom at example.com
+item3.X-ABDATE;type=pref:1995-05-21
+item3.X-ABLabel:_$!<Anniversary>!$_
+item4.X-ABRELATEDNAMES;type=pref:Aho Sak
+item4.X-ABLabel:_$!<Friend>!$_
+item5.X-ABRELATEDNAMES:Sanma
+item5.X-ABLabel:_$!<Assistant>!$_
+UID:AFBB77B8-0438-4825-A1DB-A75D76B6C3A8
+END:VCARD

Added: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1.vcf
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1.vcf	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1.vcf	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,11 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1
+END:VCARD

Added: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E2.vcf
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E2.vcf	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E2.vcf	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,17 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Contact;Mulberry;;;
+FN:Mulberry Contact
+NICKNAME:mulberry
+ORG:Apple Inc.;
+EMAIL;type=INTERNET;type=WORK;type=pref:mulberry at example.com
+TEL;type=HOME;type=pref:777-777-7777
+TEL;type=WORK:8888888888
+TEL;type=WORK;type=FAX:5555555555
+item1.ADR;type=WORK;type=pref:;;1234 Infinite Circle;Exampletino\, CA 99999;USA;;
+item1.X-ABADR:us
+NOTE:This is a contact created in Mulberry.
+item2.URL;type=pref:http://www.example.com/~magic
+item2.X-ABLabel:_$!<HomePage>!$_
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E2
+END:VCARD

Added: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/F0A6918D-8E09-43FA-9684-226810B8A96F.vcf
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/F0A6918D-8E09-43FA-9684-226810B8A96F.vcf	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/F0A6918D-8E09-43FA-9684-226810B8A96F.vcf	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,2010 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Inc.;Test;;;
+FN:Test Inc.
+ORG:Test Inc.;
+EMAIL;type=INTERNET;type=WORK;type=pref:testinc_sf at example.com
+TEL;type=WORK;type=pref:777-777-7777
+item1.ADR;type=WORK;type=pref:;;3 TV Street;San Francisco;California;99999;US
+item1.X-ABADR:us
+NOTE: Company with picture
+PHOTO;BASE64:
+  /9j/4AAQSkZJRgABAQAAAQABAAD/7QA8UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAB8cAVoAAx
+  sl RxwCAAACAAIcAhkAC1Bob3RvIEJvb3RoAP/iG6hJQ0NfUFJPRklMRQABAQAAG5hhcHBsAgA
+  AAG1u dHJSR0IgWFlaIAfaAAEAEwAJADEABGFjc3BBUFBMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+  AAAAAAD2 1gABAAAAANMtYXBwbFYcEOZVYuhIRg5LwLIi62wAAAAAAAAAAAAAAAAAAAAAAAAAA
+  AAAAAAAAAAA AAAAEXJYWVoAAAFQAAAAFGdYWVoAAAFkAAAAFGJYWVoAAAF4AAAAFHd0cHQAAA
+  GMAAAAFGNoYWQA AAGgAAAALHJUUkMAAAHMAAAIDGdUUkMAAAnYAAAIDGJUUkMAABHkAAAIDGF
+  hcmcAABnwAAAAIGFh Z2cAABoQAAAAIGFhYmcAABowAAAAIHZjZ3QAABpQAAAAMG5kaW4AABqA
+  AAAAOGRlc2MAABq4AAAA ZGRzY20AABscAAAALm1tb2QAABtMAAAAKGNwcnQAABt0AAAAJFhZW
+  iAAAAAAAAB7vQAAQXsAAAJL WFlaIAAAAAAAAFYqAACp0AAAFF9YWVogAAAAAAAAJO8AABS1AA
+  C8glhZWiAAAAAAAADz2AABAAAA ARYIc2YzMgAAAAAAAQu3AAAFlv//81cAAAcpAAD91///+7f
+  ///2mAAAD2gAAwPZjdXJ2AAAAAAAA BAAAAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUA
+  SgBPAFQAWQBeAGMAaABtAHIAdwB8AIEA hgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0
+  ADVANoA4ADlAOoA8AD1APsBAQEHAQwBEgEY AR4BJQErATEBOAE+AUUBSwFSAVkBYAFmAW0BdQ
+  F8AYMBigGSAZkBoQGoAbABuAHAAcgB0AHYAeAB 6QHxAfoCAgILAhQCHAIlAi4CNwJAAkoCUwJ
+  cAmYCcAJ5AoMCjQKXAqECqwK1Ar8CygLUAt8C6gL0 Av8DCgMVAyADKwM3A0IDTQNZA2UDcAN8
+  A4gDlAOgA6wDuQPFA9ID3gPrA/gEBAQRBB4ELAQ5BEYE VARhBG8EfASKBJgEpgS0BMIE0QTfB
+  O4E/AULBRoFKAU3BUcFVgVlBXQFhAWTBaMFswXDBdMF4wXz BgMGFAYkBjUGRQZWBmcGeAaJBp
+  oGqwa9Bs4G4AbyBwMHFQcnBzkHTAdeB3AHgweWB6gHuwfOB+EH 9AgICBsILwhCCFYIagh+CJI
+  Ipgi6CM4I4wj3CQwJIQk2CUsJYAl1CYoJoAm1CcsJ4An2CgwKIgo5 Ck8KZQp8CpIKqQrACtcK
+  7gsFCx0LNAtLC2MLewuTC6sLwwvbC/MMDAwkDD0MVgxuDIcMoQy6DNMM 7Q0GDSANOg1UDW4Ni
+  A2iDbwN1w3xDgwOJw5CDl0OeA6TDq8Oyg7mDwIPHg86D1YPcg+OD6sPyA/k EAEQHhA7EFgQdh
+  CTELEQzhDsEQoRKBFGEWQRgxGhEcAR3xH+Eh0SPBJbEnoSmhK5EtkS+RMZEzkT WRN6E5oTuxP
+  bE/wUHRQ+FF8UgRSiFMQU5RUHFSkVSxVtFZAVshXVFfcWGhY9FmAWgxanFsoW7hcS FzUXWRd9
+  F6IXxhfqGA8YNBhZGH0YoxjIGO0ZExk4GV4ZhBmqGdAZ9hodGkMaahqQGrca3hsGGy0b VBt8G
+  6MbyxvzHBscQxxsHJQcvRzmHQ4dNx1gHYodsx3dHgYeMB5aHoQerh7YHwMfLR9YH4Mfrh/Z IA
+  QgMCBbIIcgsyDeIQohNyFjIY8hvCHpIhUiQiJwIp0iyiL4IyUjUyOBI68j3SQMJDokaSSXJMYk
+  9SUkJVQlgyWzJeImEiZCJnImoybTJwMnNCdlJ5Ynxyf4KCooWyiNKL4o8CkiKVUphym5KewqHy
+  pS KoUquCrrKx4rUiuGK7or7iwiLFYsiiy/LPQtKS1eLZMtyC39LjMuaS6eLtQvCy9BL3cvri/
+  kMBsw UjCJMMEw+DEwMWcxnzHXMg8ySDKAMrgy8TMqM2MznDPVNA80SDSCNLw09jUwNWo1pTXf
+  Nho2VTaQ Nss3BjdCN343uTf1ODE4bTiqOOY5IzlgOZ052joXOlQ6kjrPOw07SzuJO8c8BjxEP
+  IM8wj0BPUA9 fz2/Pf4+Pj5+Pr4+/j8/P38/wEAAQEFAgkDEQQVBR0GIQcpCDEJOQpFC00MWQ1
+  hDm0PeRCFEZUSo ROxFMEV0RbhF/EZARoVGykcOR1NHmUfeSCNIaUivSPVJO0mBScdKDkpVSpt
+  K4ksqS3FLuEwATEhM kEzYTSBNaE2xTfpOQk6MTtVPHk9nT7FP+1BFUI9Q2VEkUW5RuVIEUk9S
+  mlLlUzFTfFPIVBRUYFSt VPlVRlWSVd9WLFZ6VsdXFFdiV7BX/lhMWJpY6Vk4WYZZ1VokWnRaw
+  1sTW2NbslwDXFNco1z0XURd lV3mXjdeiV7aXyxffl/QYCJgdGDHYRlhbGG/YhJiZWK5YwxjYG
+  O0ZAhkXGSxZQVlWmWvZgRmWWav ZwRnWmewaAZoXGiyaQlpX2m2ag1qZGq8axNra2vDbBtsc2z
+  LbSNtfG3Vbi5uh27gbzpvk2/tcEdw oXD7cVZxsHILcmZywXMcc3hz03QvdIt053VDdaB1/HZZ
+  drZ3E3dwd854K3iJeOd5RXmjegJ6YHq/ ex57fXvcfDx8m3z7fVt9u34bfnx+3H89f55//4Bgg
+  MKBI4GFgeeCSYKrgw6DcIPThDaEmYT8hWCF w4YnhouG74dUh7iIHYiBiOaJTImxihaKfIrii0
+  iLrowUjHuM4o1Ija+OF45+juWPTY+1kB2QhZDu kVaRv5IokpGS+pNkk82UN5ShlQuVdZXglkq
+  WtZcgl4uX95himM6ZOpmmmhKafprrm1ebxJwxnJ+d DJ15neeeVZ7DnzGfoKAPoH2g7KFbocui
+  OqKqoxqjiqP6pGqk26VMpbymLqafpxCngqf0qGWo2KlK qbyqL6qiqxWriKv7rG+s461WrcuuP
+  66zryivnbARsIew/LFxseeyXbLTs0mzv7Q2tK21JLWbthK2 ibcBt3m38bhpuOG5WrnSuku6xL
+  s+u7e8MLyqvSS9nr4ZvpO/Dr+JwATAf8D6wXbB8cJtwunDZsPi xF/E3MVZxdbGU8bRx07HzMh
+  KyMnJR8nGykXKxMtDy8LMQszBzUHNwc5CzsLPQ8/D0ETQxtFH0cjS StLM007T0NRT1NbVWNXb
+  1l7W4tdl1+nYbdjx2XXZ+tp/2wPbiNwO3JPdGd2e3iTeqt8x37fgPuDF 4Uzh0+Ja4uLjauPy5
+  HrlAuWL5hPmnOcl56/oOOjC6Uzp1upg6urrdev/7IrtFu2h7izuuO9E79Dw XPDp8XXyAvKP8x
+  zzqvQ39MX1U/Xh9m/2/veM+Bv4qvk5+cn6Wfro+3j8CPyZ/Sn9uv5L/tz/bmN1 cnYAAAAAAAA
+  EAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0A cgB3AHwA
+  gQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2gDgAOUA6gDwAPUA+wEB AQcBD
+  AESARgBHgElASsBMQE4AT4BRQFLAVIBWQFgAWYBbQF1AXwBgwGKAZIBmQGhAagBsAG4AcAB yA
+  HQAdgB4AHpAfEB+gICAgsCFAIcAiUCLgI3AkACSgJTAlwCZgJwAnkCgwKNApcCoQKrArUCvwLK
+  AtQC3wLqAvQC/wMKAxUDIAMrAzcDQgNNA1kDZQNwA3wDiAOUA6ADrAO5A8UD0gPeA+sD+AQEBB
+  EE HgQsBDkERgRUBGEEbwR8BIoEmASmBLQEwgTRBN8E7gT8BQsFGgUoBTcFRwVWBWUFdAWEBZM
+  FowWz BcMF0wXjBfMGAwYUBiQGNQZFBlYGZwZ4BokGmgarBr0GzgbgBvIHAwcVBycHOQdMB14H
+  cAeDB5YH qAe7B84H4Qf0CAgIGwgvCEIIVghqCH4IkgimCLoIzgjjCPcJDAkhCTYJSwlgCXUJi
+  gmgCbUJywng CfYKDAoiCjkKTwplCnwKkgqpCsAK1wruCwULHQs0C0sLYwt7C5MLqwvDC9sL8w
+  wMDCQMPQxWDG4M hwyhDLoM0wztDQYNIA06DVQNbg2IDaINvA3XDfEODA4nDkIOXQ54DpMOrw7
+  KDuYPAg8eDzoPVg9y D44Pqw/ID+QQARAeEDsQWBB2EJMQsRDOEOwRChEoEUYRZBGDEaERwBHf
+  Ef4SHRI8ElsSehKaErkS 2RL5ExkTORNZE3oTmhO7E9sT/BQdFD4UXxSBFKIUxBTlFQcVKRVLF
+  W0VkBWyFdUV9xYaFj0WYBaD FqcWyhbuFxIXNRdZF30XohfGF+oYDxg0GFkYfRijGMgY7RkTGT
+  gZXhmEGaoZ0Bn2Gh0aQxpqGpAa txreGwYbLRtUG3wboxvLG/McGxxDHGwclBy9HOYdDh03HWA
+  dih2zHd0eBh4wHloehB6uHtgfAx8t H1gfgx+uH9kgBCAwIFsghyCzIN4hCiE3IWMhjyG8Ieki
+  FSJCInAinSLKIvgjJSNTI4EjryPdJAwk OiRpJJckxiT1JSQlVCWDJbMl4iYSJkImciajJtMnA
+  yc0J2UnlifHJ/goKihbKI0ovijwKSIpVSmH Kbkp7CofKlIqhSq4KusrHitSK4YruivuLCIsVi
+  yKLL8s9C0pLV4tky3ILf0uMy5pLp4u1C8LL0Ev dy+uL+QwGzBSMIkwwTD4MTAxZzGfMdcyDzJ
+  IMoAyuDLxMyozYzOcM9U0DzRINII0vDT2NTA1ajWl Nd82GjZVNpA2yzcGN0I3fje5N/U4MTht
+  OKo45jkjOWA5nTnaOhc6VDqSOs87DTtLO4k7xzwGPEQ8 gzzCPQE9QD1/Pb89/j4+Pn4+vj7+P
+  z8/fz/AQABAQUCCQMRBBUFHQYhBykIMQk5CkULTQxZDWEOb Q95EIURlRKhE7EUwRXRFuEX8Rk
+  BGhUbKRw5HU0eZR95II0hpSK9I9Uk7SYFJx0oOSlVKm0riSypL cUu4TABMSEyQTNhNIE1oTbF
+  N+k5CToxO1U8eT2dPsU/7UEVQj1DZUSRRblG5UgRST1KaUuVTMVN8 U8hUFFRgVK1U+VVGVZJV
+  31YsVnpWx1cUV2JXsFf+WExYmljpWThZhlnVWiRadFrDWxNbY1uyXANc U1yjXPRdRF2VXeZeN
+  16JXtpfLF9+X9BgImB0YMdhGWFsYb9iEmJlYrljDGNgY7RkCGRcZLFlBWVa Za9mBGZZZq9nBG
+  daZ7BoBmhcaLJpCWlfabZqDWpkarxrE2tra8NsG2xzbMttI218bdVuLm6HbuBv Om+Tb+1wR3C
+  hcPtxVnGwcgtyZnLBcxxzeHPTdC90i3TndUN1oHX8dll2tncTd3B3zngreIl453lF eaN6Anpg
+  er97Hnt9e9x8PHybfPt9W327fht+fH7cfz1/nn//gGCAwoEjgYWB54JJgquDDoNwg9OE NoSZh
+  PyFYIXDhieGi4bvh1SHuIgdiIGI5olMibGKFop8iuKLSIuujBSMe4zijUiNr44Xjn6O5Y9N j7
+  WQHZCFkO6RVpG/kiiSkZL6k2STzZQ3lKGVC5V1leCWSpa1lyCXi5f3mGKYzpk6maaaEpp+muub
+  V5vEnDGcn50MnXmd555VnsOfMZ+goA+gfaDsoVuhy6I6oqqjGqOKo/qkaqTbpUylvKYupp+nEK
+  eC p/SoZajYqUqpvKovqqKrFauIq/usb6zjrVaty64/rrOvKK+dsBGwh7D8sXGx57JdstOzSbO
+  /tDa0 rbUktZu2EraJtwG3ebfxuGm44blaudK6S7rEuz67t7wwvKq9JL2evhm+k78Ov4nABMB/
+  wPrBdsHx wm3C6cNmw+LEX8TcxVnF1sZTxtHHTsfMyErIyclHycbKRcrEy0PLwsxCzMHNQc3Bz
+  kLOws9Dz8PQ RNDG0UfRyNJK0szTTtPQ1FPU1tVY1dvWXtbi12XX6dht2PHZddn62n/bA9uI3A
+  7ck90Z3Z7eJN6q 3zHft+A+4MXhTOHT4lri4uNq4/LkeuUC5YvmE+ac5yXnr+g46MLpTOnW6mD
+  q6ut16//siu0W7aHu LO6470Tv0PBc8OnxdfIC8o/zHPOq9Df0xfVT9eH2b/b+94z4G/iq+Tn5
+  yfpZ+uj7ePwI/Jn9Kf26 /kv+3P9uY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AM
+  gA3ADsAQABFAEoATwBUAFkA XgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtw
+  C8AMEAxgDLANAA1QDaAOAA5QDq APAA9QD7AQEBBwEMARIBGAEeASUBKwExATgBPgFFAUsBUgF
+  ZAWABZgFtAXUBfAGDAYoBkgGZAaEB qAGwAbgBwAHIAdAB2AHgAekB8QH6AgICCwIUAhwCJQIu
+  AjcCQAJKAlMCXAJmAnACeQKDAo0ClwKh AqsCtQK/AsoC1ALfAuoC9AL/AwoDFQMgAysDNwNCA
+  00DWQNlA3ADfAOIA5QDoAOsA7kDxQPSA94D 6wP4BAQEEQQeBCwEOQRGBFQEYQRvBHwEigSYBK
+  YEtATCBNEE3wTuBPwFCwUaBSgFNwVHBVYFZQV0 BYQFkwWjBbMFwwXTBeMF8wYDBhQGJAY1BkU
+  GVgZnBngGiQaaBqsGvQbOBuAG8gcDBxUHJwc5B0wH XgdwB4MHlgeoB7sHzgfhB/QICAgbCC8I
+  QghWCGoIfgiSCKYIugjOCOMI9wkMCSEJNglLCWAJdQmK CaAJtQnLCeAJ9goMCiIKOQpPCmUKf
+  AqSCqkKwArXCu4LBQsdCzQLSwtjC3sLkwurC8ML2wvzDAwM JAw9DFYMbgyHDKEMugzTDO0NBg
+  0gDToNVA1uDYgNog28DdcN8Q4MDicOQg5dDngOkw6vDsoO5g8C Dx4POg9WD3IPjg+rD8gP5BA
+  BEB4QOxBYEHYQkxCxEM4Q7BEKESgRRhFkEYMRoRHAEd8R/hIdEjwS WxJ6EpoSuRLZEvkTGRM5
+  E1kTehOaE7sT2xP8FB0UPhRfFIEUohTEFOUVBxUpFUsVbRWQFbIV1RX3 FhoWPRZgFoMWpxbKF
+  u4XEhc1F1kXfReiF8YX6hgPGDQYWRh9GKMYyBjtGRMZOBleGYQZqhnQGfYa HRpDGmoakBq3Gt
+  4bBhstG1QbfBujG8sb8xwbHEMcbByUHL0c5h0OHTcdYB2KHbMd3R4GHjAeWh6E Hq4e2B8DHy0
+  fWB+DH64f2SAEIDAgWyCHILMg3iEKITchYyGPIbwh6SIVIkIicCKdIsoi+CMlI1Mj gSOvI90k
+  DCQ6JGkklyTGJPUlJCVUJYMlsyXiJhImQiZyJqMm0ycDJzQnZSeWJ8cn+CgqKFsojSi+ KPApI
+  ilVKYcpuSnsKh8qUiqFKrgq6yseK1Irhiu6K+4sIixWLIosvyz0LSktXi2TLcgt/S4zLmku ni
+  7ULwsvQS93L64v5DAbMFIwiTDBMPgxMDFnMZ8x1zIPMkgygDK4MvEzKjNjM5wz1TQPNEg0gjS8
+  NPY1MDVqNaU13zYaNlU2kDbLNwY3Qjd+N7k39TgxOG04qjjmOSM5YDmdOdo6FzpUOpI6zzsNO0
+  s7 iTvHPAY8RDyDPMI9AT1APX89vz3+Pj4+fj6+Pv4/Pz9/P8BAAEBBQIJAxEEFQUdBiEHKQgx
+  CTkKR QtNDFkNYQ5tD3kQhRGVEqETsRTBFdEW4RfxGQEaFRspHDkdTR5lH3kgjSGlIr0j1STtJ
+  gUnHSg5K VUqbSuJLKktxS7hMAExITJBM2E0gTWhNsU36TkJOjE7VTx5PZ0+xT/tQRVCPUNlRJ
+  FFuUblSBFJP UppS5VMxU3xTyFQUVGBUrVT5VUZVklXfVixWelbHVxRXYlewV/5YTFiaWOlZOF
+  mGWdVaJFp0WsNb E1tjW7JcA1xTXKNc9F1EXZVd5l43Xole2l8sX35f0GAiYHRgx2EZYWxhv2I
+  SYmViuWMMY2BjtGQI ZFxksWUFZVplr2YEZllmr2cEZ1pnsGgGaFxosmkJaV9ptmoNamRqvGsT
+  a2trw2wbbHNsy20jbXxt 1W4ubodu4G86b5Nv7XBHcKFw+3FWcbByC3JmcsFzHHN4c9N0L3SLd
+  Od1Q3Wgdfx2WXa2dxN3cHfO eCt4iXjneUV5o3oCemB6v3see3173Hw8fJt8+31bfbt+G358ft
+  x/PX+ef/+AYIDCgSOBhYHngkmC q4MOg3CD04Q2hJmE/IVghcOGJ4aLhu+HVIe4iB2IgYjmiUy
+  JsYoWinyK4otIi66MFIx7jOKNSI2v jheOfo7lj02PtZAdkIWQ7pFWkb+SKJKRkvqTZJPNlDeU
+  oZULlXWV4JZKlrWXIJeLl/eYYpjOmTqZ ppoSmn6a65tXm8ScMZyfnQydeZ3nnlWew58xn6CgD
+  6B9oOyhW6HLojqiqqMao4qj+qRqpNulTKW8 pi6mn6cQp4Kn9KhlqNipSqm8qi+qoqsVq4ir+6
+  xvrOOtVq3Lrj+us68or52wEbCHsPyxcbHnsl2y 07NJs7+0NrSttSS1m7YStom3Abd5t/G4abj
+  huVq50rpLusS7Pru3vDC8qr0kvZ6+Gb6Tvw6/icAE wH/A+sF2wfHCbcLpw2bD4sRfxNzFWcXW
+  xlPG0cdOx8zISsjJyUfJxspFysTLQ8vCzELMwc1BzcHO Qs7Cz0PPw9BE0MbRR9HI0krSzNNO0
+  9DUU9TW1VjV29Ze1uLXZdfp2G3Y8dl12fraf9sD24jcDtyT 3Rndnt4k3qrfMd+34D7gxeFM4d
+  PiWuLi42rj8uR65QLli+YT5pznJeev6DjowulM6dbqYOrq63Xr /+yK7Rbtoe4s7rjvRO/Q8Fz
+  w6fF18gLyj/Mc86r0N/TF9VP14fZv9v73jPgb+Kr5OfnJ+ln66Pt4 /Aj8mf0p/br+S/7c/25w
+  YXJhAAAAAAADAAAAAmZmAADypwAADVkAABPQAAALA3BhcmEAAAAAAAMA AAACZmYAAPKnAAANW
+  QAAE9AAAAsDcGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACwN2Y2d0 AAAAAAAAAAEAAQ
+  AAAAAAAAABAAAAAQAAAAAAAAABAAAAAQAAAAAAAAABAABuZGluAAAAAAAAADAA AKPAAABXwAA
+  ASsAAAJ5AAAAlQAAAEwAAAFBAAABUQAACMzMAAjMzAAIzM2Rlc2MAAAAAAAAACkNp bmVtYSBI
+  RAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAA
+  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAABIAAAAc AE
+  MAaQBuAGUAbQBhACAASABEAABtbW9kAAAAAAAABhAAAJIjAgAqqcBCT4AAAAAAAAAAAAAAAAAA
+  AAAAdGV4dAAAAABDb3B5cmlnaHQgQXBwbGUsIEluYy4sIDIwMTAA/+EAQEV4aWYAAE1NACoAAA
+  AI AAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAAoCgAwAEAAAAAQAAAeAAAAAA/9sAQwA
+  CAgIC AgECAgICAgICAwMGBAMDAwMHBQUEBggHCAgIBwgICQoNCwkJDAoICAsPCwwNDg4ODgkL
+  EBEPDhEN Dg4O/9sAQwECAgIDAwMGBAQGDgkICQ4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4OD
+  g4ODg4ODg4O Dg4ODg4ODg4ODg4ODg4O/8AAEQgB4AKAAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQ
+  EBAAAAAAAAAAAB AgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhN
+  RYQcicRQygZGhCCNC scEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RV
+  VldYWVpjZGVmZ2hpanN0 dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4u
+  brCw8TFxsfIycrS09TV1tfY 2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQ
+  EAAAAAAAABAgMEBQYHCAkKC//E ALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXE
+  TIjKBCBRCkaGxwQkjM1LwFWJy0QoW JDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZX
+  WFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWG h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5u
+  sLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp 6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A/Ie6uZ
+  LZoIYXSSFWZhjkgn1NZS7Y4I4yQS5woHUnP+Na MsLq7qIXfaMN22nrTWmbzIWeOMzBmckLxk+
+  gr8+hZLRH9Z18KqM3KhBK/wB339SvFbQrNGluD50T ESbj96QHjHHT29qeG3hT5bIQ/QnoQc5+
+  lLayrFJuBUqSWb1PsD606OJriMIiFwpLZA6nv9aqTd9T PDQlBuNlyteX5l3yTvExmWMFjGjbS
+  wORknionheOeFHDNHHGVyvBIPpke9SWrJJp7XJjdCj7QrHI DdTx79qhfe8yF43TswJ556CsVe
+  7TOujOMk5T1b2a0/4P4knkhYYpX+WM5wpPLDtiro+S2AEqMpcP tJ5OOPTjipZGtBYxwLcKzxK
+  FZjkhjnkj0A6VXnaFEa3KjeAfmLHll5OPwrJSctzahOM1zTXlr26f 0hk8iCYCVGcRqVBU4ySe
+  vTp2pYrkRWwhijf/AFo+Y/wDqc+vNVgPtE8QY5LMNuOy46/gakjAdfOa VDGSRu7buw/OrcY2s
+  zOc6DlyzfL2/r/gFuaWzkib93I85kZyScDnv+lRbBFYi6iuVE44ZCvKhuo+ tU3SYNC/lERwhV
+  nXbycnOKfI5iu1yI2Z3JAK5XCn0pKFtEzCbpxg47rzas/LYfbWtotvHbxyOpLk M3ufT8KhY2q
+  zpGqzOrEklTyxHA7dKgaaJpt5+RnLeWoHOewq8lqy6bFNNxuAxn+PP8Q9hVyvF+89 yeWMZKHM
+  9dkun43IzEtveq8b5IXdDIP4hjmlDKvlvETvRQoOPXmmTpDC6qsglOSAy5wABn9al8pX 2BiI/
+  l3Kw/rS0aTZ2wjGcWra9dOnn1J2kkmuVeY7nmYvImMY/uipmVv7QYbvNlOQcDA9DT2SBZys gE
+  gLsQ2/G4AduDVgNbLIk+/y1VVDRHJZuOSD+VYOVtkd9GEqauruJUt3QXscZieVHbaXQ42rj5jU
+  gtiqQBra4WJUZt2RwW/+tUhVTEv2dgiIjIzH+Jc/zzUKwSN8gcyHesZBORuOTxSb1vsTWu5c83
+  Ze fT0CK2NytuFOCAoYMOpJOP5VVuYLeLWY4nR/JbkgnlsjqPbNaEUUn9pBY5Nq43ynHCsoPFU
+  mE11J GDJHLM5CxqUx26dKqMnzb6HHUjJVZRcvdtuPsZrpLee7eMwCOQ7kdeST0I9qqJMG0Z1U
+  Au0gOQeB k/NT4nJmW3uGZU34I6c