[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/NT4nJmW3uGZU34I6cY6n2FVot88oQwi3UDa7MOhxnn61ooq7bOCNOFObdR6rrbd
+  FrdNDFJCu4h ZMwZHQdzUxluC5ijmiETHccjOCKbaW1ybR2mjwu7y3DDLKx6CoYU3/ZpJy0aJE
+  yOgX5icnBz071L 5Xc7FHC1YP2cb37atefYltoZbefa0sKJJnJwTkZ+lOhSe1v7a4BDRupywXj
+  nI2/U0yGTyFikJVZj 91X+br+FaG95VjXI/wBWcpj/AD9aU+a77M6amAnUklF+71v0+5Ic+nH7
+  RB5S/Zl+64kOTHxwD79a 6OGOKz0xGRxFLCqou/n5S2effr+dV0s2uAkCy+fEBucr958D7wPoK
+  fLJcqvmQ2zpmUK5lAYFuAhH tjPFeXVqOpaNzzZJSfspPZ+Vv839xZuLO2llZyfMZzJsVTggdS
+  D796ykkjjihZIZVlaMbGJ4bDcM B6ZrQeeSO2l+0OivDdFXkC4BYentg9Kz/wB297AY3zuG2Ln
+  gj2+nWlSUuXXYKUavspKb93p2fr0L Cwtc6oZWmj+3RAxt8vBPVjiqAkjltpc3cCJJIX2Eck5+
+  U5rcmtjLB5UkkeXk3l4/lOPT61j29hbq zxoj4VsBmbIUkH5frmnTnFq7exyuLk71Hr2026a/8
+  Ay/7PimJlW4WUcg7ONp7VlH7WI0AiIWJGXd jtnk/rWtdpcNNbm3QwTR4S6VunqPyNVru5JulJ
+  ZZHYnzggwGbjkDsK9KlOXr+hrTryvda22XbzMq 4tJmuEKkxwqWXLdx1p/2SNba0eeUwPKDIwY
+  ZJIJAIx2qaRNt8n2hxIkjE7o+MnNSfZX+feduyXY5 bqpPQfSt3UdlqOqubXn19PyK8kCKiyS3
+  MbTkEjjAb1xSwRSecs0Ubl0BGBztLcc1ry2dssrm7R2O wl1XA2noO3bvToVaDT51j8uSZ5Rhg
+  Oox1+lZe393+rDlUUYuPK5X6tW+/cyhCfICSyxxXG7ALZww xx09ahgjK2oiDrEHO9lYcjjH8h
+  V0Q3Hn7Agk3bXDAds4yKlubf8As+/iJQXAZHVW7Nxww9uf0q/a K9r6nXSjTU4xb5uy/rVGZHA
+  Yrf7xYMwKsBxjHI+tWHUNbKwR/KVgScEjd9alhVo7dI5M+Wq8ZH+e tRT53feI3HLIBgLVczcj
+  veHlBLmh+bFgsYLeKMl8ySSZCg8px1NSwzz2lzHInD71/h9P/r0yN3ku HVG3SEEhcdcelVgGG
+  wZYZOWbr+P0FJxcr8zuYuh7kqcndPtbT5WNGa7+03Jk2vnYRIw6Z6k1qxXl ta2kA2s/7pmyD9
+  0HsfcnpXNWl00drJtVJfMcktjtjFSi6aRSoYDAC4C84/8ArVnPD306Iy9gqqip ydlt/wAAvLe
+  2UlpHbLBLaOxG/wAxs/vAeD9KoXN5IU8yUhJ5Zw7nH3iARx+lN2ESxCSaPC8hthO7 3HGe2Kpm
+  G4nMD3JO4hmjAGMDkf0rSFKCdzjlhnKahZv1/r8CHYgiKMwUkbhk+nUVYxJcQqszIRt+ cKOuD
+  nj6YFPjhdrYZXcqMAOmcH39KU2zGVUE0RILK3BwR3IOOlbOSNZS5VZp66WV/wAfIqBQqBYZ Bh
+  cgMeg3dqgjU21itvHIAryF5UIyRxgc47DmtGG3fjy9jKcn7oIB/KoVhgMNtGyuk4Y5JP3ge9Vz
+  o4sRgnzRm4Wts1/wxLGyyBlk3Pt3AFeAw/hxUi3ziwt1kGXRtkQAxlfX8DVUKxjjEP7xdwDYHQ
+  DP tShVLlQ4TnKp/Fn/AA71DhF7lV6PtUnNv1/y2LNy141xG926uWRmygxnnBNRW8+9nTzRCjF
+  d4cZy vfFSSTb512QuVOflYA5AHHQVGro+2d1j2wZAAQAMzDAz6ikl7trDqKcafs6b/wDbX92o
+  15GMwC4E THAyONo6dqRY7aOSBo4pZCrBmAPQjmm7oxb2ykFZo1KsWOQ2TRE4K7MFFZwVJ64HX
+  86trQmUE4Wd /nv+RHOZGYhV3KuD7nnINJCwi/5dJ0yCSSQce9OdpQ2duOPm/OpCkbuzM7uEHB
+  U8PV30sdqT9ro3 fzt+bRRfyxlllUlgMYHfpUaOzop5aUZ3Nnj2GKsSqYn8wxOQBlRxxjHP51U
+  SR1kDKm4nOSBxn/Jr WOqPKrt+2tK6S7X1JXvGjgCyfvS45UDsTTPs6i7kiRAPLyrHOee/8qHT
+  McYuYsbV5IH3v88U64ig Z90TlCARjPfvmmrLYhqrUfNUs1HZPf7yqi+ZIFOYgSCM85wDVyNFL
+  EBxtHK5bt6VT8lpJ1VXwcjy lOeR9anWSObISKUuQRtUe9VPUww1aCk1PRrXV7r8iUw70kYXCg
+  LtzgdyOn1qtHGk1mJVnHmqzOR7 4HFNDq4fZG9uORhzktgf/rqHygVRh+6KtxnvTjF9zPFS9q0
+  4bLXrr95LGt9IV/eRKVBLHb0TuPr1 qw1zI4kXzV2bsKSOoqKNlAPy/KxYg+uOMVZtI2kMqYjG
+  Yy4LDjCg1M2lq0Vh7r3ud2e+rexBHKWU +XukPm7sg8DHb+daUkMbM2Uk4J8rLDG09KywrySwF
+  nS3TPI2/eXv071LFvGNisMhiwkOfWlKPZmt CX71Jpv8n8tSREkS2FvwIgwLE8jpx/M08WpQEx
+  sWIX5lU4NNijaWBsFiv8BA68frWgIklbyw5UMh ZnXtjms5TsddGjR9nJvRLs9eu1inLKGg/eb
+  ywA4H5imvPMTkElWAx8vbuakVf3JlVMZkAOQG6DIP Paoo3Kyp5hcbpRuKgfdOeOnftQrEzxk1
+  Fzd7eW5RnEk9wjZSELgNuHOMVLC72+/5FkweWHY4/wDr 1OYraR5GxK64BHPTsM/lVa6WMzliG
+  A6nB6mt4yTtFo5VGUE6yer8/wDgWR0NzDKtzEqGRwpwxwQP XB9+aHTY0KSJISI/lAx8oOTz61
+  El6r7Y2lDGVioYEgA+/HepfJDSKiv5jlQWYnjODwP89q4NVa57 6rQdTmVtfWy+8diFLfeqhxt
+  4PbJ6dqVHk8uOFoyjpG5XaMFgOpqv5DmGN5oXiMZ5wcZ9PypIJriJ ot77pIQyL8vr1BoautCa
+  1adRrkitNr7E7TPBB5UaFsKAXA+UKcEk/wAs1I6tPcbIgCXOTg8rjnB/ lVRblJIiSRgHaDnrU
+  sckksJkASEliNpGTgd6ORo3pQp83uy37L+vzNK9juJIBcT2wjhdyVUIARk8 /liqUELXNwsfll
+  XIZyS2dzf0yMUhlaS4WNlYnJ3dunNX4ZYvPE/ktHvJJZSAuPb2rF3hGxzV5+x0 STfSysvu/wC
+  CQ+ZNBIj20QjVkztdQSTjBwfQVloI5ImcYIU4JU4y3Y1cG1PKywnjdt5APLYP6CnG ONt6b4kQ
+  kyBVXb8x4A6dquNkVSpxcuZQWu7/AK1M+B7q2X7KXzFJMWm3c845Ge3FTsXi1AGNcsSf LDYJV
+  MdD7+9TvZPDLEZhmZVcMo6Agf8A16bYgjLNLF9oKBF3rwQepH04FW5Ra5kccYJNuO3boIUk vg
+  w8qFHV1KsqAYAXrVwPc/ZYkkClPLPlnbxyen1yMCoRtW6RpWAZNygKcZLcDPtV8xxxXgidyypG
+  RIQM4bnGOOnNYyltobYOnL23M1a22jbX5FORrpWe2WEcnJURgsoC884qSK2d4kkDAITsBJ/i7D
+  8a sl7gRIU2RKdqNLt5Ax606dY0+0I86zmObKmMYDLtwMD1zUc72R6MvbRqfu1pfXTX9EPSxii
+  nME4k bGWV93+qA+8G9TUc5R4cpaMkZbdvzznHH4e1SmeGWO3ktzMJlUocnOSRj9RmhIJjLbxB
+  dmEypccL z0ask3e8maU1Je/zX8u3ys/1JnNx9ntwyxw7oecxjghsntUgKyXKvPewIjOXGFwdw
+  6HgdKinmSRp VEbInneZuJ3bTjGD7e1Z7/Z4SHjDuq4G1m5G7t9RSjDmW1mbVKEpU/fVn3snp5
+  XW/wAydFE9/JK8 6Rxl9sjZwCSDg1lMpjjQEPxyDu5HpV2DdLbPabdpVvMXPXj1PeoI2228hIP
+  mAEdM7hnlvoK6IaNn LOkpTbe9vNW83p1IFKjzGZwjhsktzk/lW5pywSwj7QQ+3fFIqcHcT1/C
+  qyfYjGY8CSUuQX7NjnOK nfLmJ1gKOYZDKqjq5Pt+FZVpc6tax5uIpyxElGKs++zGuzCaexBMo
+  3bQUGOR3oWKI2DCPDO21pSf +WbZIx9O9VYYdluJhKHCuoZQTknB4q99me60yN4X8yRVyFQYJX
+  uT64pStHqei6c6XKtEu67+Y2K3 SV900tuAgYhiv3wDgMParM9my2SzC6tXULtXYuCecfzqtBD
+  G9ypuIplZZQqIMDcCM+noK6KOH7Pe Szq8XltIpEbrnC45H171jWquL0ZvUquDvGV/u1/yKMMN
+  1azxMtzFG03zcj7pBxt/HNasst4yCBQj G3baAF5YZBP49QKrTOJHjYSRbURgeOpIzkfSnhkaz
+  WWK5V5JkJi6/dHGT9O9cc25WbX4f10OOpyy kpVlr3tp82tSW6EE1v5e8O+M+XnlvmyxPuBUds
+  PL1k3gh3RIAsW0Arz/APXNR2k8rt5DRb5UnOJV XCuCOCPbrSztLZeW/mqwRHUccHBBB/Wps17
+  ncy9rzxdBR+L8fQS5upbbUSkzoyIQ2AuCxX0P1I/K oIpDqdvcTMjIyEoqx4G8sfvfnUpSwYXT
+  zzK1usoUEtk54IGf50lxcSWz+dCY3G2RWVEwMn+orRJa KK17luMI1oRowfP+H4GVqG+P7RF5i
+  J5LxhmP8TYBPP61nS8Xlw8ULOfMbew5HzDqPQA0s7b4YYQD vjTLHOS/vVgRTNHZKh+0bY9s3l
+  9ixyM/hXfFckVcqvGVGznBJ32X6/8ABK1rYx3tpGEbLNCzKpOS WBxke1MeEQrFGRLGshP2jzD
+  nJ42kelalvttryDzJY8GIlAo25HO7FVkjEbRyRMXAUlt3OHHQUe1k 5Pt/X9epxc85y5VK3a2m
+  vqI00bM0cwIDL5cbt/c759TnnNZCRrMojMpEqNjcCQCfUe2BXRefI1rF LNZ8gFHZkADMeePSs
+  xLfc6K1tIZBtDhBggnOCaqlOyf+ZrSUuTnlt6poIQLQh4tw3gsm9s47gfh/ Wm/apLlvNkgkmX
+  cF+X+DIwR+Oafd2c8W+JQxij27mPc4zkegqrHPcJeBomBmfd8qLxnHp0+lXFKS 5luUuRw56du
+  bv/W3yBpm8+EzJHt2FQMY3EAqPyphWNre1aMOx2ESknOSOc1OJw6mLCTwsrNlV+YB R1B7DOfy
+  qmsrSBBEqqWLKBjO4AcmtYp+h00qsZSUpOz+eo1EM0wliO11IfIHXB/lVpHS8nkkZ0Tc TuCr0
+  P5YxTJBHFEoCSxuQAVJ7cEGpfswM7bWUBHCsw4BJ5z9M0SaeprSgm+ZRs+uln+oxRmLyYzH Fu
+  kHUckjsPwqeSJ7edQI0CliXjIBZCOACa0bW1hlTzDFIH8wsqhumDwPzp32JZHmVRJGWcNIHfJB
+  6msHWXNqW051GmtF0M1YVmslQQyi6DYBByOegxVEFTkfPmMlFDHpnk1uJauLjz7aGZoSGaIk9N
+  3A ye+Kz1GYAtrt2jmZ25GeRx6Zq4VEOKjBpL/N/wDAI4EaZZUWGRwSASDwuPWldpRegSRxwsm
+  UbKjO WGM8DoK0vLZ77yYysUjvsUjox/x60kihYZI2ZfNaUm3BGS6fxNn8OM1HtE2YVKkZ1OS/
+  5XXmRyxM LY28c9lIkRysoj+9gc9u5rF8qdbjdMgZt2WCDGwZ5B44NakqolqwKywxcmFnPLEe/
+  pUDKqR2srF2 Mineu7lZBxg/zq6bsvU56kacIr3t9NLfjYqrLBDc4kXaG37EHHTpmnW5865hia
+  AEODkouCCRgc1M scTQOrgxyrMIzvGSCTyfpTolKSPFGrozNuScnC7VB56d6ttWfc5+Sm4Nyf3
+  30/ryC0+zlhBNG5Ql dpDfMAMhuf1qGe0iacFAUhZG8s5zkL/F9DVqRoRLbuCJ0jiKDbxtPO4H
+  jqM06O2As0kadCVUDC9W UAk49OtSp2fN3KhGEZqb+F6aa39Oxm29qGuIlwFG3fhurY6D8aI7J
+  pLnzImSBgzbUc5IHbPFXGtT PbJNbF5WwPlXkg4yf0qncvvKJCSY+gI6kdQa1UnJ6MJ041Obkf
+  y/4BXFv5bKHky8ZJAYE8f3SO5N SYkjjjWMKu6PIUg5FSwzCGaS5kiWYyHaoZchgeC34UnS+cS
+  MDHl48hcDbj7w9hVOTvqHtYU3JxVn bp/V/uZlssghVJ2EkpUkOnAx6fhVeWWTciIhcZ+bA6L3
+  q7HdvLAZEaEBSYz8mcj/AOvUjwB7IzGS N5A4UxgEELjmujm5X7yOOUeaFqcnZ9df+D97KE8v7
+  xkSVX3NtVj0bHpVULJAuwsMMTnIzmtjykct EHhyFLA7euBn9azpYj5ce4kblDDn3q6c1sY14O
+  Tb3a+RXkaeZoyVyBzlONoz/iKcYZ3BPCHO4np3 qd/JiLhldweFC1Ahjhk2K2/erKMnP860Um1
+  ojlqUlGTcndvfX7ug2VIphGGkMuwll2tjk9c+tW4Y vMhYBh8rZJBz26VF9mSLT7Zt8UiHPCDB
+  B9+KWCJxs8ncAPv5PXmlKScdGRg1FyUuT1SJDGHk2ylU VQNuxeTnmlW12tF98jblSPTPf8asG
+  Jhcuj7mKnBKjgelapvJpLe3VPs6rtZB+7+9g5z+PQVzzqyV rHdOmlUXu3bfUznjUlhOhV1lCK
+  i8E8ZOP0pZ7URyLHcDEinDkZGD17dulaNtaRbC9zOvLqSmfmQ5 7n+dTXEMPnrsdxIzl1aTlWQ
+  cbunTNYe3tKxq5e+1O935afO/+Rl7pgv7t4oI1m+8yjAY9R0qREba 77CpmkZlz2Vei/nQ00Vt
+  cFFT7SN3Ddsdc89aspELmWLadiJueUsPXgKPc05O2ttDGWHSrKpb3eun 5ozZo7iWaMzZQyP5g
+  TGOvf6VApVQxjjJ3Nkkjg4PUe1aSxzpOjeW6FZdnzgHB9KrbUQsSDGM/Ip5 IBJ4rSMtLG1ClD
+  mfs3p8tBDG+2TEbRHuOuDUE1tsQCSJsYLo2efx9avWscb2SyPMytliw9QOg/Go 3s5nijZSXVQ
+  Bx2B6/lTjNX3DmpV1zKO3Vr8hwsxa+Vb+X+82szHHUjn+Rp0Mkf7sCKWTcAF57k1X GJCsiyyP
+  s4Bzzk9/oKuRguD55VG88YKJtAU8Htg84pS2u2drxCov91H3enW3qKuILh4Rwwcx4zn5 h0P0N
+  QxnbCfOkCS+crDKkhgBgnilkSWOTZhWfqWxwcUwHfPE0iYc/KBjoewI9am10XK7ilzbbdEP Eb
+  GPcDCEX73y+/H86mDBbxVSSFWdiqq6574xVJFLkAKyEA8N3NXI4h9kb7s2clW9MUS03NFzTg4x
+  W6/rqh8cjtG9w4GwuwXjsOM/nU0RcwDJUoGAQkcKCOatgFLTyDENzMNy4yBn+Hp1NQPH5byBV8
+  r5 tzK3X0ArDmudOFjiPdVTp16/cCRqVaQSBpY1JxtwB+GKZEF+/JltykjAAq7bxKw2yRSO2AZ
+  Npxnn oKkEcflwq1tOjMhYjd0yeD9MVDnZtFRTjUd2/XS/5lCJNojdMMPLLEZJyc4x+NaElnY7
+  baMLKrYK ctyG6kfWrUdvBJNFMFEEEbEAuf8AWcHDD29qYGFsSdyNKiqN7cq2T6euM81hKq5PQ
+  83EU6lad4dO 2n5f8Eo2FgzXzbwBhfmLj7uc8mrcFrHHO8UiSuyx7Btb7xIJ3fnV9JSzXaxMqq
+  kvljIyWUnAOfal jkt5r3EkcskuDsZeA+DgYqJ1ptts7E6nvSinGy/pt6fmOtDbRrHHbwvPcOo
+  ZyxDBCvVSPXrReQKs LxxWbKJvmQ8bmAOBzWeUul1W3iWCSVcsrNEMc9zn2PH4VoPcXtldW7yR
+  73+ZYlP8Kkc5+hNZSi1J OLu35nL7KSq88Xd+r/zKB0+G3+zuZ/tEkUg3JESCdpyT9Of0qS4Fx
+  cWqzzI8aJuO9OA+48H6cUye bz7iLYzCRR5hC85Zf6Y6itOSLzdPee4BVS22NAcAh8H+daSk4u
+  LluelSw0qVSFSUU9db7/cY6yKt oDNBKT5jBwpALcDJ6fSniNGkSD7K6xjO1jjI6ZJ9eKrJbTT
+  3CRKQZmIyp/vdMfXirNxHMk1qof5j HLKMevdffgVrKydk9R4vmg1FSu797WMmRoortpEDBVOI
+  wW556Zx1pbaB5YHlkkjk8qUZSMYJGMkV YkkQwruiGXG5D/snGPxFaGnxOlmZBEZ0dyx2d/X+l
+  aTqcsLnHWnKa5pRtfTff8SvC/kyLIjQxM8L ja65xnv/ACqJpWlhkBjZrltgG09Ox/E1It2SYL
+  nckDueY2XJBAwOx4rQlhkklmR0E4wG8yEAfdAI 9KycuV3aNVUSp891213/ACV/vJVkijS4Rts
+  eydMggHqMUxYxa3H+onit5MpG5bgg9R+dUrBZL9Vj YeQxYtM7/wARwSD+VbxtLifTEjVPMR8y
+  Y7gcEL9a5qjVOVm/U8+M6cJW773/ADRAwu54t62xT5i3 QZQ4wAfc4qQXrLoO+6s5VYS8oxG7A
+  x8307U4JJHDClw/Mo3SFeNhB4U+5pt0Ymt7m4hhuEkBIdZH zsyRgfXqax0k0rdTohCNRwUYvf
+  0/G4ly9u9wXhjeZH3bERudoNZNoXN0yMjOoDMqqcFV6gfh39a0 rnU7FLOaILvZ5FYMnGD6fTi
+  qktyHnit3j3ZDI+zAPJBz9Pat6Sko2sd1KVRx9+DS23ennb/hgmvF GqfbIZ0MbfKoU8Zx0qe3
+  gi8pmuvNmRGCvl/uE8bT7nrWWI0lnWHymOWEhKnA+XI4+tSwec8zR78v PiWRD1Vl6fpWkoaWT
+  tb+v8yMRTlVk9bNL00JLmSG3ln8sLHtZYyj/N1HX61SuHAiSOJZkaNsYds4 9j645zTZZJZZ5G
+  nCrlt2x1+9kcEVIyxy2cM4LjHE565Yenpwa1jHlSudCp8yi5X8rf0iO3hCSs15 kxE8Opxux6H
+  0qe3ke10+OdyJY2I+WM4O7ORz9K2ATdQRgWrS2rRF1IwNgHG0++ax3hmMiCZY7WIE Id4OMseP
+  xrNVOdvmOJ0/aybqvRb69P8APuMuLiV5YTcSQGRZnkwkeCORgVblZ5ba6khXlZywKL6j PT86W
+  C2tlupYZ7232wvtEjKTuBBzViDy7ZYonu4XiaEMyDruGcc/SpnKK+FbeXzMXRo0rcsL211T e/
+  3GNMjzW65L2ylvMBckggYGPrzmrVvZpuVJXklaQMAyn7/Pyke1PuLW4khgRTstjbGQOwzu29cf
+  jUTXoSM3USZZcRkHoCw4I/WtOaTjaLIqxqxpr2Oj9dCdhZxRSW4eUMXVWd2yNxXg/QY6Vlrata
+  Sx BicSvlZB1QKCcH3NLJK0izxFMSkneCOm3p+OKrO0kiS+Y7NGzq+M9DjitadOSW5UcLNpcrs
+  3vcWd 43uifOjiHllAuMdV9h602PdbWFsgik2BS27HO4n1pscoLtsWN15Xdtz+PIqaKCN5UVhI
+  wPLKD0Pr 9K2doqzOicFTiqmjXp/X3jVndoyskamQ42uUyWXByRx0FXQ7xg7fuugVNycMT3Hqa
+  q2bBp4w8Duw RxheORyD9ParttNvKbpYoV4Rd4zgnms6mmyN4zVKm1ZW31T/ACLtttggZQDdN5
+  5QtGcYHGPxp9xD Ld3riNDahGJZnPQAdD75/nVRkQ5kil+RZCWjz8xx1/Si2uJWlLgtHbysXO8
+  5JA4PNc3K78y3HRjF XlGV/v8AwRfSW4tYFkKfIZFj24+4CCSakCRQvLJK8NwrcKsa7cjsaZNe
+  faLVXe3fyVdsHONw29fz xinWcMQS28yeOFBGpbzed3JJxWT+G70NVFSpubXrotV2Wl7kE8UbJ
+  bhJogxYs3tjk/jWbc4F0rvE YoJmYoSM4A6jNaE0LER3EAZvNHmtJwVB3YwKzpXnN1biVlABBG
+  5eAGPP61vR8mKlF04t0no/v/D/ AIJbmu3jVRHGkkboz/MARzxx6VmWYW1uobggSxpzIeoLNxj
+  nvin3EKeepEu7HIweOPb0J7VJHujt Y55SBM6syIF4PbOOnrWiilCy6nFiMDCUbKHxfL7+q9SO
+  Wa3MyKqM8bI24Dgk5+XJ9afBbO8byS3N vACQ4BzlgDxj27VJbi0EEZO5H+z7SDnJPI3Z9Paqp
+  aWRkhDBV+X5m7EDHp/nNUr7LQinQai7tqK7 q/5ofPIkjj5GQzHz3YdAewwB0pkJuJJGtzJGq4
+  BGV64BPHFX/IWXUCZImjhcuzRg4aIgYwT7dadH BFbtaTupEUkBIlBOOuMn6modSKjYxq1MOo8
+  kdX0/q1yhNPch42lUQu43rsXbjPGeO2KS1ma21pXV YbhWOxiEGD2B56cVchghclppGjBIUM2T
+  8g+8PqT0qNp7Z2eFYtkLHzowPvDH8JPfvT5k04pDqqMq UowpuzWr2+6+7KbkLem3FuyKXH2dm
+  wcIMk/XNMktLq5d5ZlW3t5SSHcYAHccVoMsFzcREsyokLBC OPlJ6/zqt9octDGq4VY2UFjkMS
+  fl4qoyl0WpwRoVXdKNlbqv6X5lX7DbWV1Bu8uRWi37kHynAOeK ijkY2ashtk3MA6NHkn1q2Fm
+  iKPcwbT9x+ANue5+nFUp9n2sx+YGXcQJVGAcDrW0W5PV3G8LHnfJK +nay/wAiG4jS0uh5DLNb
+  fOiPjPbIyfxqmsiGHe0bsVAR+RgN+XpV0pF9iRFjYmRAyHcBwCQfx460 x4UVy4CoAwkwec4re
+  LVrMKeGrqnZO6XyRXeGH96RMGBm2x4GT04H4/0qiI4hp7+aU84TqyHB+YYw fpg1dVF8yFzllQ
+  ZIHBPOalMa/ahmFmLI+fqe9aqbiclfB1JQbntHW/f16fkZkRCiIRE7Sh68jrkm pLZJJt0bzxx
+  sCWBI4Ix0+uasMR5sEMZSJCn7xmHK56j86cUnhvCjLG4VsEoNvaqlK5goR9ooRbT8 l/wdSdXl
+  +x7/ACnhm37SsmDle/Hr05q7bWjFyTE/l5Ii56joaqWMc00u1tsKLJ8zscgAjgfU461t W1xFH
+  YC3jVpy25vLJ+YZGOD7d64603HRHXepCCs25d30XfyEFxPbrFAPIjiKjLvHuJ/rUy3EYhlL we
+  cUxGj5wFwM7envmp5tPmexEm1vPQLCseMlivVvpioV024W4khCPKMBl29/b6/4Vyc1KSvc7I08
+  O6ammpd9bfcKytNZmRbeABJAOIxl1xlselI1tcNpweOHKSMXJUY2gfdB+lVIrm9hlPAYFhlNvc
+  5A q0t3cnfD5oCGZcE9MHjP0puM47WMJYerSkpU0rLXr+WpmzWrRXcQEgO+RSVHJJA61DIWm8r
+  DRhWT Ifb05/xq/cLAmqrcCYqYZwqjOQwYcEVmxb0CJcIxw20qDgqBnNdMJNq50Rr1IzlHldml
+  stf8iqlr FHbDe0jyIxKhScEHv+FXbVCjw3QYtFHG3G772Ov86r2sbyPCkTAOww5c8dTz06dK0
+  ZbWQsIVt5pG SIquw8A9wfU1pVnrZsxn7Hk5NEuqf+ZmrFiRY4AQuCXyMnHer0i5ulitz54ky4
+  CjJUYzg+pqOWZ0 fypRFHKVwy7cFjknjHTjFTo0wthdTR+fHIefKAU+gYH0HpUyk9zor15U2nH
+  7v+B/lYosS37wMVXh T6564qxPsnufM3BXaQnZ0wVGcj2qEmN5dkYMg35ZwcA46HFXkgWRSYla
+  6lYCTKDhSeCKJStZm8W5 JSk7K+t9iASpHeCbiXcCxwf4j0q3b/Z4woLuWYlXRV5z6gkYxUyQw
+  vMYGi8s7dysewHU1ONOhawt Z3k8xtzPiPgt789qwnUhszSdoVUotpvt/nqWRa+fdokaSGFYjt
+  bPLvjg5qt5ZXTp5HurdyZAsigZ bd+XFaC+ZFe2rElIS2+MZ67Rjd9PWqmTc3DbEAj3h5ZgPkL
+  Lnbx79K5oyd/I6ZVZqs/eVl2/zf6E cBuIzEBC5lAV3XuoU5z+VaNwGup3nWeJpGLNEEGPkBya
+  maZPtab2Te4cttX/AFfy8qfx6VWkDTab GwuIpHjiSIBE27cnv7mo5uaSdrDoRU5+05Xrpqrp/
+  gkMjgDOkiSQ4Me5sDoD2qBZp4oZETyywkCb SgY8DIrVYWK3lxHuMKCQLhj904x+XWksYrO3WQ
+  efHclGyNvYYPB9z2pOro21+BWKxk+b3o38mv8A gfqVI5pDf77qaKOLyysi7QCWYZA4HXOKhUb
+  bS3EJRpFQqx287geRn8aFjtpLfaI5nkfawTdkrg42 n3pLdZf7Ut4A8QdnLkEfcGeVPvWmiuwl
+  WjD95JWXbYm868ljg25LoqeWijBbrg+/vVeYyXFgZpLh ZJEm3LjPzgDqPYGtGa5kN3tAS3ihl
+  LAleYgBwGPfJ6VWia3lkaNgBdyzeai4yBx93HvUxl1t/X9f 1sc8KylO7gl/XdrYRbea8ZmXy3
+  dnDZVcZ9SPYirElpI9zPbDd8qlzluM8FcVXGousKskP2aRiFO4 cBTxVkCO1u7ppneVi4SMoSN
+  ydz+FJ86f5HpUZSbbS9F/W5Xulijt0Rfmnum83cp+6y9vx5xWWl4X hJBH+sznuOxH5VrXIdrp
+  BbxO8ZbIfr+I9KorOXhUpBHAoZ1eMqMtwMHOPXP5VrT+HVXJ9k5Plcb/ AC1CztlfUBEWKQ5Yu
+  SOVUcitezjlRdkcTxxSDc8h+7uHQj0HtVG2aOQB7iKSRiQp8ttuT2P09aZP PLatJahZfJLqW3
+  NnGPQ+45qailN8o8TRdWTj0SW9/wAFfX56Gnd21rCYhK0c7spYqnGQOSRUD2sR kSaKK4ZZUDC
+  MSc9CDz7danZ7K7WRo3y7ThQuecY6j0FRW0Zee1iZnclBllYjGScj8MVzxclHVs8h OolJp7fL
+  8CS3keA28cKpKoUHIHbsf51pyTyXEf7wtFG0hwqfL9Bx6077IZ5GiVT5bEspXgsp7j2F Qm1Js
+  4Wk3PCV2OQfXowz71zylCTv1Ob2dPmVRuz9LsqwRPE0Jud1urxMVaVsg7Tiq7yTTTIZZEb+ Jw
+  owCemPwrRFpKkCR3SSXHlSbZcHgDjP07VJcJFKyiGNvJBYNg8s7Hgj2q1VXNc6PauT1fz0sUpI
+  55A0VrBBKBIfnMYO3GMZPr1ouLeQx3O+DzFSRQ5jUAn3FWU0poneRJZYvKVo5kLnl+uar2yoE8
+  wX gZ1QxyR5J3l+jD2FOM1unt5GuHc4vmUrpdtPxK0lvLJAq2s8AVVwqkfMFA3Ek49aaIJbiyt
+  naWJb jDKSOM7uc/0p8HnWt5aoJFCMuZWcZCODyD9c1anJniMUqCNDIxhIABJxx+FaOTTSHLm+
+  sR5tt7+v y1+8zLSwWUW0ckUjyNESvzdgTk/hU8kUK3EkYAjfysBmOVyR6Y6mrP2NURRcTlZUT
+  CsrEDb/ABVk 3apJFHIgZVX5QSc7we9aRk5y3PXwik5Pm1vtuv1sWDcPAYltn+7DtcY4ORnI+l
+  NWbzdQsjdobiIo wQJxuJHDfnVY5t7oeW3mqA4YnkEAVJFHvWLfLGSsY2Y4IXOf0rRwja4sRha
+  c73VvO2v4ECRRrbJE 0qK4ABDDnOSTz9KtzLasqeTnzZZPMXLZEZA4U/UZNO+zRuiXcJQxrPsc
+  MMnpgn6c1QjJ+1IwSSSJ GYqQCASM4Ge/pRfm1T2PMrynOFqU9F6flpcspLNLexwREh/LZEjbn
+  aoOTn8Kbv0+2tmWVtrSTAWw Y5JGM/j9e1UFb/SbabftZlLMcnII6A0x1aa4gLI1xDFExDKMYP
+  OOSK1dJXtfQxxmHnN8t/d06fkN 2oXSRkcjAJAOCRnnJqKYASptGwIpXkfe96crPJDEpI2rEck
+  d25xUs7eYib0L74wcJ25/xrdaPU9K Eabp2m7P+vuIkSXzG2hQ23cPlGOnIx71YRg0EnnREfON
+  p9BjoapwFn8xmjliZSVYk9M9varMtzMU ghVMrIQM7fTvRJO5FoJxqJadO463kmiYTKECsD8+w
+  cHHAzSfafNtcCFWllbexAA2npxTB5scLjIK LJ6ZAB/rV5/tEVuDFFEsYLbiEHBGOPxrOVr3tq
+  czd6ilKKTv94uniJtTEbqYxjBU9TgZzmtWJpks 7dBp8kplXOQAAOTuFUreS8lkTbAJG8t1d1j
+  6hhwfwrW+zXIs83TMYfKby9nBU4wM1x15Lm1NK8m6 nv2T7f8ADMryNO8u0252LuCxgcsRgKfp
+  mqLzyS3G4oowAJAR1Oecela0XkLHZKI7g3MSYK7+SoPJ /DrU8ULTSIoSMwYZiwXnIGefr1FZq
+  rGO6NIYunST54fozPupC2kSrJEYsTD92ODnr+AHpVCKVDcX TmLzYyQc+4GanlYOiuFJCKeG5z
+  u/nVeSRvs7wFfLEeA7Yxg4zg/lW1OK5bHTD2UIJOOj316FdTHM lu9wdptlAQKMbwxOSfpVe4l
+  ZnUGUbI49qjHJGeoojbzwH6yMhby06jB5/TmmxTxSFYpo82zOozxu Az1z2HrXWo2bfY5Jyo0V
+  KUE35ak0MkeZGby92Rg7Rhh3A9OKc8quXkSEDzNpiCjHA4z/AJ71TeOb MqymPMbkbQMZIPX9a
+  lku3jRFhhA6bTjI2j72KXJd3RxpSlH2jTafT/h/+CSyXRQb5YpYpJZc89Gy MMfoKcbSeQJbxu
+  ziJBGnPDLn7w9qh27L6QIfOCEomRn6Ef55q2i3l9GSWX92WVSo2gk84NQ/d1Wh MKMoPm0Ue/b
+  5Fh4YVtnt1kEjeb5ipnJAHUZ/Ws6QK1zFDAylI0ZY36A7j+vpTUed7UxbWWRCAMDJ I60p+zyw
+  l1byWMi+XlumSTj9KcYuO7uEYypx7t7Pt52H2kUlsJUkBjSRWBLDow4xn8c1DOY5LKxi t4XZ1
+  ADuOpOcA59K0CZLQyCQNcoTuGOrMRyR7Cq0lvC0DCJiZkZUCdd64+99KUZXldhGc5zip6Jd tV
+  8yocbRJMs7gMyStv43nv8AhxxTbiS0MUdtFGTKmUZ853txjAxViaAWdvGVYOrSkgEcYA6H3Pr7
+  VVaO6BVjEwkLgYAAJYd/yrWDT1uc1OKac29Lvq7fdchMLiKQLbywFWKhJDk8nlen41XKFrtdrR
+  gv NhMjjZj+uKs36m4lS7WUu0U+0qODjHB/SqyRRC3hVgytt3Id3Pet4bXOaeGqV17OOke+uv3
+  lkJFA HABYNkgdwPSqIQP5aDckRXLORyuD3OKvgqLXauzcQPnY/mKpyJKbqWPOIweo6DvVQO+v
+  QlKiotNJ bWtcj/dHa3mLkqdpwPWljVVijaR2YFmO3dlgSO/HeqBNulvEqo0pYnc2cbD2zVm0W
+  JI3nfFwqnYN vTd1/lWso6XPKVeFSpF8vvL+t2vyL4hHzS+asfQOGH8eOFq7EBbiWRJbeVklVN
+  q53EdTjj8KoskE lmrBXjYjeGZ8hsHGcU44F0G3M4znA7DOT+Irnacla500ZTqQbk3u9NNf68j
+  X8uNrm3X7aUadZHjj YklPVT7miZpreFEX7REduSS2SrdMfX/Gkjeymvo/OkYbpDJE+cYHdavy
+  XFvLYi6f/SVdi8oQ4Ikz wPpjtXE5NSSauZ08RKNRRnG66K36r9SigvnhEks0LNJltnlgMdnHY
+  e9OvDNDfwybUysbKPl+7xgZ 9acLuSNoRAFGVPmbhnPzc49KbJEkt6R5NxHAJP3bl8/L0z78mm
+  rqV2tA53CV6isu3l6f5alEcm2R 4WaR4/nPoT0/HHSltZrWJth5ZiW8x+QrjOFP1ovnBZYY4mM
+  inAkLcEDio2mFuohlEZAY4UKNwKnl T7nsa3tzR9SMdVahZ3XlbX+vuIrOTa8dysTFEkCgD1OT
+  ileSG4VnzLFIzcgnqTx+n9arW81tLdLP skSLzGZ1EmQrngHoOgqKP9/CgK/Z1TO7J5fPQ+1bO
+  HvXegQU6tS8lq1pZrbz6GvFFE0cklyqtLIN 0WTycZyelJ9ilktoGIKLnCNuwqg9c/U9Kmjl+0
+  QPLJsklLYAjGAqkY6e3WplsPOYQxSbmbHlyk/I VHBPTPJrndSz1djtlXjOKU2k46/CvzKJtTb
+  XEiuMXCOscfHGW7EeuM0+FLWS/aRRKlsofG1iMEDG M/Wp4o5IL9YiDcoW2xrjlmAJU59qo+TM
+  kFukkBgm2sJMjAbrjA7VSfN1HQlKVXlet/67r9S1GzND HMCUeNAkasMkoc5PvV+2hU2tlFIk5
+  dly5D8dSQoB7kVnpHJJLBGIGeRY8bB/F74q9GYWvkdUaJwf vGQlcnoMY49qzqbaHozpTjsrSX
+  W9vvS1/M6GSa3ZYEdPs6eWzs7444+UD60RLCliZZQqhx80fcn/ AOuMmqFxcWElmu6XzH5ZlUH
+  tjFU2aP7P5zQXEbM52s8ny4A5HH1rz40W11Rw4ag5zV002/k/vZam J/syVxEAIpcwuy5BRgMA
+  +tTobUKGkCnJLOkfy+UQMBD6n0rLaVZpPMtnAkPDRMchmA+8B0wBUwu3 ijlNzBujdgVZV28Fc
+  YJ7n3rZ03ax2zg+XkW67Wu/vf5MkdZFtN0sahAVZWK8yEcbvoKczxPpbFxs Mh82EoNpKrwc46
+  81nm+jMIhmk8mEYVA2SSepUH16VOpguppNolKgiNFB5j4+6feqdNpXaMnCjTs5 PVav+r7jUuT
+  au/kRlmLqised28ZJH17VCt5NOiReULhVkCymMYZjzjBxxU1zJbx2Qs4cxRRzhiXO 53wMBgfQ
+  dxT7SQwSCQxh4mV2aVFwsjAEAj0Har05XLl1CU5Onfk19df69BzXIZZo1jZAXy7NzgqM jP1qB
+  Im8tp/9ar4kV0HcenHQZxVa0ZLiNVtw80+zDqB1cHOfoB1+tXfsssUzfvY9oYOQM8LnJ/wp 2U
+  XY3w8ox0ST+X+T/Mkjltb54zJMsMxZvMkIyvboParvliaWVYVF6SwkRVHKY6g8d/xpdOtIbbz5
+  ZXgD7225HAPUDFWItQSI2/CSSzQ7n8lMFuTkj0xXLOer5NbG6dveUW/L/g7/AIg63LT75k+xwy
+  Rl 2yvbODiqKafFbS/eS6tG3eW45ywwRzirE+2GysRHdLcElxIxJPDHr9Paop1+zRQxSSC5hOW
+  ZI+CC Bwc9hSg3bTr+hVOcqkE1Gzbta35dn53K0QzayS3dpLbSSTCTOMDaOoAHTNOlNzNbQyIy
+  PG43QIEB cgHvxzj60599wYlKSsVQbjnjI54+ucVatoWbTVmhdbhxIFaBOGUnrjsBWkpKOrMMR
+  VVCC9o7u+z1 X/AK8QK5lcRw3KSqjnb8uepbA6VrQ2aILqe3LEzsZEJPRB1x+dY8KzxzRTMVtk
+  jUoWlGQ/PLf0rc VVluPLWZZ1IwFjODj1Ht2+tc9dtbM893jUdRS0W9rteny9RcC2tIlfzSzQg
+  xtuxgZ4H54zSuYRE4 a5T7SsgQJk4PGTx9avGWC0y8LKYdshIk+YrnGOvY9qzphfNBb3cDW8gl
+  iZjGIvmVhwQT61zQbk+x eHqutNRS18+v3iR284h81JCzyjzPmOcBf4ceuajSNXggmlmCOFDOq
+  5G3JyeParP2KT7Ku6R3ZIvl CkjgY/nmrMkyqkkRCK+/93Ht+Ypx19ap1OxvKpUWy2/rzKSTxu
+  ZE/eujtlpFbhyTxj6in3NrIoI2 R28Q3I7sv3WYgKD9R0rWSSC2nkZ7JsKzonTGD/8AX4FVZp5
+  005V8n55CDscAlNg7/Q4rJVG5LlRn GpUdRcqt266nPGG2+2tbtdKDCGjZiSdxwTuHsO9GZoYL
+  FXkRkcI8cuOFAznP1qY2sBtt5mElxMFk lAODnODj6iqk9vsD+W/+iLMIclskZ53fQV6EZKWlz
+  2pRhJcs5aro1+Rblle5V1itnYvKF+U/dX+I fWmynT48JIjARM2Bu6jI5/Kp9rCSdzcRSQ+cpP
+  lcc9x7cVRP2qRxNtWGFV8phIuTkkkfj0qYJGbi pPlUvdXqtf68iu09ldmZYfkgDERjv07n271
+  Elq/2dZdwdfJ4IGBx1FaVvYQPq1pCwGDGQ+3gEjOT VieCW2t5Y2ddvBicD5dg6jHrWrrRTUYs
+  3hiIUpKlT95rX7+3mYlnPHBJCBGZIirRsoP3i3THv/hU l40IsIoIyDtx0OOB/U1cRIDI00iNJ
+  iNgI4yFOOzfgOaoMIfsX7u2mXDKvmO2Qxz249KtNSnexk8P TlX5nF/kr+jf5FNba2k05RG0xu
+  dwZFBz8oOCKlKFbu48wbRvwUHGCeg4qyzrCiDyg6OCTEgAeNc9 z+tUrqGRYxKQ8WJQAGH3gRk
+  NxW0ZXerFRcIcyau+m/53/T5kSCORLYQ5FwM4GcjaOp/DmqbMHkjV B8zKc8Z3YJ5FW2VwyyKB
+  kjAK4AUNkEGpjCsZjVA4kH7tDj7pJ+nvWqkkzKOHkpTnzrRdX/V/Qpu7 CYrEpJbqCOMcYP161
+  IFltpxLFNCzLvAyM5HQnB9c1OHhkihtMjcCxMv949KiQPIYklgk2qjN8pAP HWlzdzk9tKpU5Z
+  K3l/w7/wAiXzJTakzwmNFHlxqy8kkZ59TVyWK4mhgkkyFiUqwTAJcjg/j/AEqu giaWykWG4eK
+  SBiPnz83IzWhYi4iEG2WMIsbb/MXIbGcEficVhUlZXQ4zStZXaelv+A2MSG4WKN5x IGfBGw4D
+  Kv3jQ7xxXwkVLs2bSfIXc4P9DzWtAwEdrJdyxSvEhiUJwDuHP+FMkmE2nLZxxH92/lbC MsnOS
+  T+Arm9q3LVBh8TUlV5asb33euxT8m8EE7tguW4bpgnqv5Vntc3ioVVWR3cbmBwMjgCtSFd+ po
+  QXVjbsyBjxgevqfeqS3EUC/ZmI3MQwc8jI6D861g99LnfKNvdSWnlb7xN1xPcKkieWrg+awXjd
+  nH4AVmY2xlZFke6kbEW3ptHUmthrgs82UYTuMMOMAngjFQyW8M0zRRxyRSLOq4c8qP7v145rSE
+  +X dWMKuIdLr+GnzM5FJtIdqsswT5COAVLYz/n1ojhjOnzRxyQNmTJGPmUjoM+laRithfOlncR
+  kM25N xztxzj+dOmFnHKZ12t03qhxlT/F+FP2t3oeXObcly/l/n0MiK2JlxIWZv+WhU981sQW8
+  P2gSCSOF 9sikOM4UEc/TNU4GtVeQRbxznc7E7mydmK0bS3lFqs8zxowOWVhzycE/QdxUV5vq7
+  GOMrVeXlba8 rb/mTRxQT2StIV8/cdxXjJPRh6DA6VjrC80aLbxyywuAyFT0Az19T1Oa3IQwin
+  id4UMMwiA2+vQ/ lUkUQtL53hdXBXb5S9ieN30A5rnjVcb2ObDV504Slzarvf8Ay0MKS1X7L5s
+  iSQlFKoCceav94e1M t4hJ5DRvEspiIbcuQG/h/Tiumh+0rai3eKOchRl9uQ2OGIHYYrJs5Ekl
+  VAUWOIFUbbjKnOOfXNaR rycX5FrFVZU27Nta3vp9zRHayTmBbe7CyMs6NtAAZB3BqvcOm+a43
+  KjSox2Y5ByBj8OtWTAQkW+K V2f5iVOMDoQffNZsVrdjWIZnhaCJUYnzOR0PP8q0hy3cjOlUVF
+  udt/Sz/r0K8xMcbxorg798xfnD EYIHp61EFWOWErKTcZIB7HsCMirdhaXV5BAgG1Sh3O4yCSe
+  /4VpNp8SXF2qIT5NxGQp6njgA9vet 3WjH3X/XQ7VXpezcUtd7Xute9+nkcrBbiaaG33GBXbd8
+  /Odp5z+tXbpITqNxJ8oCy4iRBjC4/pXQ XFoilfNjadt26UwjbtJ6AemaomLakl3JbneGO1Wx0
+  BwQfehYnmdzmwlV+2UpL0WhhNG8l7Dl0jjY cs4yvHJNOu8C4HlkbJAWDHoR2NaTWbSSy7djbC
+  qsRxyTxj6jis6a0uA9xGbeYMk4TaRkpz90+9bx qRb3Omo2q0pup8tLFK3tlkhjnLQywxkiTYu
+  OcVGJhEYkiURKPmCvzub8qsMpS6kjdNpUlWXbj5um KesYQrJNE02wFVAAwMD5gffmt+bW71PP
+  +r+z95Pmb/ruRxXM/wBsjlgMTMqlMFMqAe2PWrtmZBIz Twqux/njdASRjBqg6qpDoQGDYDKMA
+  jtx61VkeO4uEKzDOeSWzlhSdNS6EVKLnJzmkm/vN+Ke3MjP NaSiBcquGxtOOn40sU32p3jZfs
+  3ntuDEfKpVTx+NY8UUwgdDcIq53iNsknHXt6UxvMW4jaKZDEWD lcenb61HsU27McY1FTk4r3v
+  np8n/AJGr5scqwzufvxElAcfN93P046U0eU1hFIrTLIpKMxYlQCM9 KpzRzSSlslNzl3UJjA74
+  9BioxLGttNHvXLykxtnqOAKPZ3WhtWjJxip6d3/X+Qj+btj3FZFcH5mJ xxznp+FNQN9n2idEw
+  /mAMcnd9aTyn+VA6yMUJXC4GM/1p25IzIGUPwMAKMj1GfrW/Qv4qvLPX1e3 3f5j4lX7PIjsFI
+  +YHsVHJH1z3ptuPJkjmmYSRfdcDtnlQfrVYqk88Kq+QB7/AHh0H4014pXVCySh nYn7uACDiny
+  30b3POxMruUYz+9/18jp3kWaQ/uQVlQunlrjgde3rWlayzskUslxAjMykJswSB6eg IqtLHHK9
+  vktE0RaMoAQV3cih0t0tmWfzEkldCDnG04PFeVPllGx6VdqtS5Ze75dfxJkETass0roY mV3XZ
+  kEgdKyrhrhdhIJBHO4Z5z2+uanlh8uKOMSNiFSu7P8AF1P+FQMJpLZJ4Y3eNADKSc4brW1N Ja
+  nqWi6XPUfvPRfIdsdpynmbHXKBCuSR3H1q9A1zBC2yIRr56nzZFyAx+6DkemaoiOfyY7mRQjOr
+  FVz8ynoM/XNWIA94nlSloZhIBMjnqQPvD09KKjutdjDFV6dSmrbLrq/vWxoTrDBDgx7xvZMgYK
+  rk bgff09qdJa2ksRgtGkdDKzJls5GB830ArLuiJSpy6w7Mx7j6kA5Pc1UlX/iYeTE2ZtxETBs
+  Ar+VZ wpNpPm1M40aipKftPhd/I27RYY7RRJeWse+YOx2+33fxFOknnmMETp9ms2jMmXTOecVi
+  JDGzhWmK oZBtAPL89R7dqvTSlNTkbdhHVwFbnZjjbSlSXNfcy9lBt8z5uv8Aw3n6oqIr5VmKM
+  ypuUFOw789a dHalfKMTu5MiuGTPK5wfxz3p4SP7AjXKSlAzKhVsZA4qRUCusdufs6Bm3PK2QC
+  P4a1c9NDrjiadS m7QSW1/6epYuZo2S980pNtIUYGCSTnIPYDGKyreIJeJCvmnLsGXd0P8A9f8
+  ApVhJlurdZLiLaytl BnGFB6H1Oe9OAtpNRaZo5WJjYqySEAnHXpSinFNWJWHrSpX5bX22X4bl
+  WNZliVJZ7dJI5mjGxMbg er8DnkY9a0Vim2F5C8sgmwdvRsr0/ACqkMP+grMkQniU4Y4ycn39j
+  WqlmWkRWDJIyu0gPQkHAYeg 5pVJpdTWhenFKUv1/KxtRW9td6OptldoU4J3ZJ29TUEbyS2Bhi
+  tDCrsCrsBlcD196itre7+zkNHJ FEiBNq8ZOcdvzPrU8kMcUbeRFchoWKySM+V3g+noc15+ibV
+  76nbQahNJT5ne68isZ5Xs4RHCIwRg hhn5egPTtzUjqs9vEk+54oodsUsZxnnjOeuenNW72QDS
+  kSRV4DIrhQOMjp9c1T3I1hDE1vMdn7p1 U4KkHdz9P61UZXiml1PRjiYSoxaWrbX9aXJ2s3WGd
+  mmEN4swxGTzgDmmxTyfZ5P9GkRJJUYEcc9j 9Kie4me/kliUsJJo3IbkqT1X8RUV0MEpLDO1vG
+  +yMqSBg9BnoTSUW9JHn4mPM7TSdtbu1/0uWzbM YLVrhiiuT5ise+7GParwjtbeK8tkSa4/eM5
+  eJsFsEYCnqOKy7V76MbPIllPyZZuQmeD/AJ9auvDL l7dbG68hA2FVhv4Hc1lNO9mzyXRl7ZRb
+  09bfk0XYY1kndyrWSzoWQXHzcD/OPxqxaTPPLLLG0TSM p3QquPLOMH/GqkTWsVis12ZI2mIZQ
+  7Z2jGDj271E8lsmnLcRCSRVYKGRsbgep9655Rcm18v6/wCH NIRlJtX301S/Pf8AE25ZrhHjLy
+  ReSkflEeXyCRgkn071UgLSXiFZ7e42RnYyJ26H8T2qH5o79ZIp cIVLBJMtkqeP0Jq1CWMjfab
+  SZUT5AUO3JJ4P0HesuVRiarDuCklZ6b6L8P8AJkl5cTWkFvtMe2OB hEWGdxyMk+vXFZsswXT3
+  BYyTp/AD8xQnJb6DvWxcQkaYzNDJhm3BmPygAjcPxqjPeCYrHHCsscpY 741ALAfxA+nalRaaV
+  kOjOm1GSXXfb+vvI4HRdTu2u1WO389Qvy4OcZUZ9Kx3u72K/QosbGYlinlg hevBrVMv76DzZE
+  jQJudnHAboD+VdT4i8Q3/jHxZFf65FpkDWkEVsiWFqtuAiLtUMF6uepbvWvtHC fw3TWvy2sut
+  /U6pXpzc3FO6/q3Q5ZYYpkg3zxC5dAzBeFceoFRm6d52QGI4beFC9SOp/CiSTN3bC GeBh5Xyq
+  F+bAyM59M1lXE9wCkV7GHSSMuTF8pY9Mj29q1hTcnqKnH2tS1r+vT00RYvPtKRxRTKQX OfNQb
+  Qy9Tj9Kou58yYxszTAmXy2+bb/eHPGO9RwSzRs7NBJOGkOHHRQvWrEV3K6TsQsgKbnVUAJY 8D
+  nHT1FdcYOPY9DD0eXmcEml1/q41ne4t7jyImn2sE3x8A7un4cVUihvVgkjXC+SwCRtyWz/ABD2
+  FRJNtj2tIYycq2DjeR0I9u1NEi3VmrfaPNmGAMKRsAzkHjk1uoNLyM6vNLRNfd/X6DSN0kj7wF
+  En lgs2cf5FV/LfyJICSqGUMDJnjAOF+prWjb7bvgMPkOGErsehIHyjpxmobYeZdxy3qhJGd2Z
+  TwMBf T2NV7Syd+hhXxajG01drorFQrAzRqqSR7o2KKxyeT096favJaR2+4qrYZmMq5Bx/Dj19
+  6cwEssrN Iql3jKEcAEjoPSrMk0T2MisFndZMsmOVPcZ/CiTurWuY1o3tTjC73+/8DOMwMTK6R
+  75QpUouGDZ6 UjtAjHG4uEIY5wc5ois0KxP52+Jst5YPz4ByRz3xVi2U/bk/1ckksLMAy5GTn+
+  VaOUVex20MQkpK Mdkt1/X5BbRPFYyyfIdj+WqkZ4I5I/OtGW38rT4EM8TQZDK654H8Q/HikUN
+  NeQ/aIZJ4kQKwh+X5 m4H5ir0KhLQ2UUbW0gJJW4+Yrg5rkqVHf+v6/A46mLrJRpu1k79NP1/A
+  pz20K2MhR3Yh94TdkqM9 D/OrFxaSqUEUcgdonkJz6c8+9Mis4LnJM26V1ZwVzjGM8ipYo5PtE
+  RacwMSI1aQkgKycnFZuduuw 1VlCEWp6pt9fyX+QqW8k9jbF5FWCaIOnHzAjgjOO9VntIAj7YJ
+  jJJJGQWIJGc8D8qswGaJbYSq0s PkeZGUGPlQnj8aVryZmjOwQQvDvLuOxPUVKlO+g3iK7Tell
+  20X3LVsUrFcakIyyRyRMqoD1MZHzH 6jjmnXJtZroQNbS27RyHZuf5mAXqfWoY3ni1aQCNLiJZ
+  FQyIv3jj5QD71cYvaaotzdYTcjK+R0cE dPzrN6NWfTQ4akFzys29LpJ316kUEUD2UefJWZURW
+  ULyCW6H3NVTbPDeX108JeIuUVcdeOMe1aSL Msk5a3Z9rEZUj5eOAffNZ8mmKloUYXMLljuWR8
+  5I5/rThNXd3uc9GvGDak7J6ar/AIYZDJCIgJrV R86bNqgEjPzfrWxPBFdWoCoVMgxkHqWOAw9
+  sDpWRAbgXkm6LzJMqWCjox4WtG2tLpZwv2e4gVNzT lzkK2CcD04qa2jvc5selTmnB3fS3+TGS
+  abc2czxwSpJlXZyR95h8u76YzTbe4RYLQ/ZpHMdpIEYH mRT/ABVPHLcLZRSeW7RKilNxyQAck
+  E9zin3+pNIs8lvp06ySI3kEKCAhwCQB2qFKTdpK/nsZQ5qr UatrPrdL7zNigjFtbsxuHC8RMs
+  hHmLtzke2f5VCkc6G3uSYVE0Bk+5gADjH+HpWjZyRWFnE7I00a ryueSR6eg56Ut3uvltjKVSF
+  yrIqDBeP+LFaKo1Jp7EwlKDkm/del97+SMCGR0vRFAx5ZWjEhJ/dj P6mrQjhmljt3uVHmxMYz
+  k/KobOD6k9M1oLPDDq4RZLUwsrC1cr98YyD9M1E1sIrEzPC0sjgeYU4w cY49BWjqXfYqTU6qi
+  pWb2/qyKCW0RvJoWuVt4ZYw+4k4hbsh9yaty2c62P7uRVLSbpQc5VwBx+Iq 2sYVSuxduSjhgN
+  28Abfz5IolmllK20REUTYkkLjJ3LgYz71DqybVjSpKTaUXtve3+VyKG1uPtVxk E+bOoOP72Ol
+  STW8htPL86JDtZWZkz83p+NSPqFxFDMUMcryOXLBeFY8AflSCzuHjSOdxK3zBgowX C87x7Vm5
+  SveVkcVab5lUmkn007eRTeWCNHcsk8oYIkcYwX5yD+GKzJ7K+utQW4mKwS3SvJhsjDDs evJxx
+  Vy4LzXtwi+Tb5dE8wrwWYcEelQz6XcSPKnnSSXUXMYVjhwp+Yiuqm1CzvZs6G1F25kpPurq 3z
+  0RhanA8X9nv5ixNJC7sHGfbH1rP87zYlXz43iUlSdvQngZrqbv7LfwurFYplAMG9v+WZ5YfXjO
+  awo9OMO1YQZxKPMBjUk7R3PFd9CquS0tx4NX0qaPXW23muhRnja38t/LDtuKsATwfy9KSCzU3q
+  RK 0WcsWJXvjIH1xSvL5IgguonVnmXMnQRqMHJFW7iJjdA28UwV5XZmAzkge3TAro5mlbv1MJ1
+  5TrOm 9+70/DZjBaxMiFpViATDE5Jk75X2xS21rbPKZJX2RYKgZ/iI4FXbWKGfT0kuHCTFcbW4
+  yp6EfhSC ezhv4yIGdFjbzCG4Jx8p/DpWLqSd0ric6klJQk5SW+iRHaWVwdQWUxlNwI2OM5OOR
+  +FULjS33Isc MrIEz+NaDwzyNCAjhERUc9dhbqTn9aIiI/tQWUyCNwkTA8eWec/Wmqkk+ZMuTk
+  m+eV21tbRfO5if vE+0bFaMNMp5PTGcD6VDsZ7uNFDlmfn25rTktRJM8M0iwsiliTnsQapSNBL
+  f4EjxIkvX269q6ozT 2KxKp001De26RIYdt5ttiJWCMQ4HDL3I+lQLDLuj4LKEIGO4PNSnzRLN
+  dW6uI5C4HooPUfrTreZx ClrIishkDBioyBjpReSRzVJzilJw5l1t/Vjoo2WFZRNDLPIpJZkOO
+  x6/n+lKl0o05UDI7FSU3ruK jo2ffuKWJZYZYxZzQ3SKGQsFz19c9T6VGVZd7CDJztI9Mfxf7o
+  7+tedaLev9f5HqNUue+7Xz/P8A yKH2OST7ivLtycqDg46896kTYSoEcqpySmeT7j8K1bKFbV5
+  TvYxk5jyc8Y+99O1QxNH5m8XEO7lm JQ/MMcAenvVus232O7629Woad7bfqjPe4VYY1w6AnCZP
+  3u/6VE5jlvYIbeKaWQuwMitx0yrU/Y82 9vK+8+MkfdOMkD3p6Ru/ARP3hVSduBnoBWqskP2F6
+  XxKPX/h9dRi21wumZEE6qhA+fPOTkkflQyc tIJI2ZgXUqvKhuMVrS6eVmERkJEX7uUjpuHIx9
+  adDHYpFlGIlSI7AxzvB5NZe3W61MqfIkuV3+Vv zbKAhaWz852RCZTuGOd2PlUemetNRFmkPnS
+  eSzjIdhwQBz071opa7cyAbWaQDY38Bxnn37/SorSI PMkcEYkVshsjdgnkip9qrPUKUlaT9orL
+  5fduZMgMssWJgwALAAHAz1qytuHcRI5MahpA56YyOTV4 2U6sFW1Ko7MyuBwoA5H41FBE8a7wj
+  hFAzuPO09vzq/aprRmsOSUHyavpbo+4WvmQC58qS2mgaQfv dgKg54HIyAfStBLRDqrH7JOCJN
+  rMCNoUjP8An61YOnXMt0IRAURiTKw4UPj5f0qUtFbW0c3mkSCN g6s2S2RgfpXHOtzP3d2cc5q
+  o/Zp+8+3+dyGOO1kvmtYLaZI2/eKQ/G0DJNPjt3EZuVlDSvEDAjdR HnBz60skUz2hiSRAY22L
+  tHKpjJU+pzTxCstrD5LFInjY8nJTkArn261m5ab/ANf8E6acvZpWdk9G nr+ZYjdbaOCNmkkR1
+  aRcN+hPtUd3fGaZY7VsNu3SHqrYHBotrNBqcoecRbJESOR+VAPUEepp89s0 GsRfKixO+zdjv2
+  /Lv9az9zn8zVKhGo03dpXSWiKpu4pLCWafMkjMCyLxliOCPQVG0s0dpiQGCdDg hhnb7H1LZ6+
+  lTysgvxE1v5hRC6hfp0I71nG5ma8C26b5ePkZd3AGST64reEU+nmelCFoNyVra2bW v+ROZLgX
+  MjxvEpEnOV4YnoR7CmXksL3scEXmLsbYCzZDZHB/nzVy2e4XEpiDo0ZwwUYXJyQff0qm BBMI5
+  VIgwmArDJOD14pxaUttjlnyufa+3LqWx9qh2Rf6xHKEt/eCnPH4VoSXCxXUoEc6QOzNlnyR 3H
+  Ppms4AgQRxP5mCrq/93HQH69fpV6G2mbUpI3zKjNwR/Gx4JHsPSueoo7s55UotynK2i9H+JLNa
+  pcS6fiWNoxExkBHEpxwV9ADWnDZW4s08vdFIIh5ivyAMYPHrWMj6p9sWWONYUkUvGjqDtUHbnp
+  07 1agsbydSJxPGwIThsBsd/wAa56iaSTnoJRcbRdXTfT+kTSQQ/YvJjkKW6jaJHOccZHP51It
+  y7Jh8 iSR02g9G3DHH0qF90sgtzNFCHbeA4zn2H5VdunumUvBHGjRnKhkBJPHSs30TO2jKo4KE
+  bX6Xdvns UpftaxwWqymQRwlmQ5JXB+YGsZjapcQyvDcRxyFWQq/3B3B+tbRe2iCefcDznYZwc
+  bs/0NVJoLFp mDzbreL5AFODnGR+Z6V0UpW0aOuE1GMoyjb0X5bDnezyrBXdVVmZN/K4OFz9Ot
+  VJnH2yIMJ5AsJM rI+N7HoPxHSp5bsKVMcaNM8ZkPHB4/lWU6B7kvLdxRK4UHk8Ajn8q0pQ6s5
+  KdKKg5u67Xd39yHyQ TxQWrW8MytJ/rGLD5WB4PsKkiu7d7j7Net5tugJEkfB3AfLg+h9Kg+e2
+  tt73a3MI+RyufmZjxj8K bPEILxEGyVYlAbaMZx3/ABrdJS0ZFKopR/ePbZrf7/8AMkSSBUR4h
+  Ise07gWyQT3NSiaTbAITBLN 5LD5ExvP/wCqiO7T+0vLlEUapDLtAQABiOM+tQRW3kadHcQybn
+  ZR8vfjHI/Ok0upM5xhpb06/wCQ k0MyMjq0EXG5mKZx/wDXOahkVSWhsYMSiTMRx95F9fU5zQR
+  fQSCCQ+YVBK/KD781FDZ33l+aI5F4 YhvYDkVqkrXbRqqa5b1JJN7Wf5rqbNhCqvLdX0yW4zsb
+  K4y38B47VlmW1ktlaRvtjq5wYiU5z93n 1q8siR6DA6p5zDHnBju+Y42j8R0qoklsbM5gWJ496
+  IwGA248Hp1FZQT5m3f+vxOWjGpXq3ld306J fduR3MsX2TDhI5pGDbMYKEev17UksU/lykmLNw
+  N5A6qV5btxioZUNvPChKOpQhWxncM849/etdl0 2f7NbJM6LIS5YsSenC/U1rJ8iVtjtrupQgu
+  TVd0k7HNSNE3lFElBYMwYNwoPH9K2IYX/ALOuUlgA Pmr+8AGY/UZ9D1xWhaWtlFp6+XPFG+4M
+  vmnJPoKjF9b272UkmXBiYTYPDMCeMetTOu5aRW3/AA55 31icpJxhd+e/4/8ABKcDPBOtz5c21
+  GA68NjIP5549K0Q7XVwqySLC0swXBHzFtuV5/n61Etw7okk M0LRoNgiK5O7OT+dRRy+bqMcty
+  6w7ZVZVxjnJx0qJJyu7am1bmqRlVWkkun5WLkENpcyRI/mwZfM yq2CWPQD0HtVojT1iW3Nyok
+  3CRy55BXp+GOKQ3FnJLJvI+0vJn5eNvzfrxmq8UUMetXW+VIVWb5f MG7dwSPwrmd3e91Y5XBy
+  fM1LTp1v/XkP1I28sLNB5iDEjDL8YYA/l/jVeBUazjEV5BGpj3kOu7y2 B4Qn3OaWK1tWhQzzG
+  QeWNwVsdG4/StV4Izdp5Nvtt0RhIMDOTz+lN1FGKj2O5csaai09Nf6v/wAA iLw3FjKZ5IiJSZ
+  AkY2lZOyfXjis1YruCWFJZo4z5DPJ567xGQeQffpWnHst7ASwQm8uVKxIV+64b knHr6HtUBkj
+  eKJ79SvkwmNU7shbkn1IpU5NXSWn9dDKivdajDT5X+4UTi4sXjSTzD5uAyt94dd34 d6zgt3Nc
+  h1n2xuoYGQk84p4FtHE7RhobJpdsTMeoPv8A561KtzYTT7lL7zJsgTfjKnqfwx+VWly3 5UcU5
+  uCbjt3texEYI59IneJnL5CmTccOc/LUstrLb2zol64mllJKsxJ4A/TtVuRryN2jIidAyliq YB
+  brkegqbzWuZ5VIQs0jYbb1XjJHoKzdSS9DzcRiK0ElGWi1/r/IQDybKaVCzsx2qM5CqRjp61mN
+  p1uWiX7Yw8iFlPzngjBA/Gt2K3DObUP5bZZstzuCjlh7VB58clgrHy8SHc5C92GP5VnTrSi7xZ
+  zx xtWPurV/13RTSVDv3xtgkGfn+NuRj0AHUVox28kkcSYUMbeXqPu7e/0PpWWl0rytawotxuc
+  bivVS B1+mBUYtGW9h86eSOCQFkBcguvRiD6VU4edhzhzTT+Hl1tvclhsYJ7a2aZWYKoZGU42B
+  hgA/zqGC 2uLTyh88ttvKyEnOWIOOvr2psFxC0zRafcCNgQEEmWAC5wD+taSu11bRpcN5CMQQR
+  xs7Kp9STVzn OOj27Fz54NX+5p3/AAI547f+ybOQ2t0ZGg37RJySvC/iKiu0uX0qNoF2zDBkBG
+  S+D1HoKmNy6acI GUCXcpYMOjE4yPRfatW4kWRIA6BmS3dJzHwNxIxWPtJRa0vqTCdWHI3G+vm
+  9PO/QxozJHcySXMO1 SWymANxGMEegFQzJLN+9SO5S4L7Qd/G36dsmpQWuPMjmkWRWxKu3g4x/
+  I4qyVuis0ovbZAW4Taej Dkf19q05uV36luElNyej/roVioe2AUrbXBlDCKUZMij+MY7AVGsar
+  MxDvEsyFkmckrg9h6Z/rWi2 nXCabJFHjyovlSRlyVJwApPqajiihMslpdB43J3MWPAK9VHpx2
+  qVUVtHc5rRgubmv5X/AKv6HOrD LHJGd0UNsnlj51yfmBHWqsFkvleXH50siy7XMb4A2tyPoRX
+  YyW1iZxGzGVZlDhA3Me052H3rDl0u 7N5G9gsjrL+/YZztC/wn3NdNPFKV9bf1/wAObTxUHzJe
+  7deiOXuYrd9akZFZrUBypJyW9D9M1XIj /wBHdTLcKIyJdrEbH/HsRWx5MCXMUTXUeAGEJVfvL
+  yT/AFFLbRRR3RWK6gLcuQUzgqO/rXo+2SR1 1J03CLT97qulvWxVtQG0iGWCWMx+YyTlueB9xR
+  6HBp94k8iaYosZWLQFGMQxnH8Rq4Z0h0qNE+zy 5bMaIuCFPUH1J5we1QRXFu8Rmj89XTPlKZN
+  2E7/lWalK/Nb+vwOLD08To46a9X/TQQWs8GnpdR3S 8qUdG/vZ5FZptoVuIlMjmRVDPg8eZn5R
+  +XatWJ9Rlt0DeT9mYhg4QYw3JX/ePrVOeCC1MMyMW3vv VS3JX1zVwk+ZpvXyOiKqTj7ObXM9r
+  fkyvI9xBqVx5iDzBMFbeuduO35dqpMVcvLtYj+FUAB29d3I qwytOymabLq6KBnllPLE/QdzU6
+  pYvcXBG7yon2RsW4K//XrdNRW2prVcabsrp9bLf+vIwS0gjVd6 hd7sx7EEDA/SrME86P8AIgV
+  iMfdB2fXIpzAb3aOARxsC2G56f/XqCU2yGMs7NyG2huWbNdN1JWsY xpclOdlpvZu977nV+ey/
+  vjCbeRWw644OeCMeuMU1LZpXUyXUaoke1m59eB9TULGaZTLJDIY2kfJH GOBkH9KsW2nwskZkn
+  8tZIi4BP3mB4/AV5ztGN9vxPYp07U+fl+e6foxxt44Y7m3hguUkeUMUkfJQ D7yn86pysn2ZY0
+  2sm3ggdw3StMhRCnmXQllizuYA4Oefx9KgaQKqzC1ISZc4OOcnGR6AH9amEn1/ r7yqClF+/dJ
+  bX/4IkbCOe5SaNoUaQMofqBnn9KdcRRhVS3vIZ0dtyRqp3DBqWG1Z7g26ROAhMcnm HJDt/hir
+  C2lmtr5UxaN1kwWz+QqJTipXKqOmqi017KzX9fMyY5JItzqsqSyTfNubgHnK49T2qdLC Z75o/
+  KcquUAAGV2jp+HetARRRlEuNsaBC0bHtzxn1I9aoXSXcCQkM6IhaOVgf9YzcnH6VUajb93Q Kc
+  p0pXste/6alo+QLCOOC6UOpG5mBwM/1NPtxE5lMit5MV0kYKNg57UyO032FqwiZ1TdgL1ODzk9
+  8fpTpd0V2LqFGW3Me5Q3Ic4xn8DWWjukxQhOrTkqctXfdobPdXIubl1LCUXB8tQM7lI+bA9OlN
+  gQ vBgAh1mX5Tz8uM5PtxSCCK50yCaORmuIyqRop5zz19STz9BVh/OsQXiCh5lBfK5OQcZHsc0
+  9EuVb m1Kh7vJ7ql6b+r7F+Nbi41WKQMxWWIzvsOArAYIP86ovGZLWFEdJCrRh37A9vzqSNY44
+  5LjModJF hChsde35VcNoq6vMbe0n2Rt5TEtkMSRj8RXPzKL/AK/rqcdVRUpS5tvS1/1KUpeyu
+  yLcGbe7Z74P /wBfNbUy202gBEXaXdXjQH5sAgH86yJYb2wV38vlZdh3DOxs4Cn3INXY72eG8u
+  FdoGaORYV+ToG/ z+dRUi5JSjrb8S3S5oxmkm12/UqTacvn3X2iYv8AvWYIpweB15plrF51t5K
+  M9u0jCZTM24v6sPYY 5q1LcGaSaORhJGjlXCDBBPyqCf1qhJPqfkJbJCryoTHKQnOTyNvoMdq1
+  i5yjZs6I1JWSlq/w/wAi pJBKwlVJBJhhIze3QCh42guy8wZZWUqxBxtbGT/hTZYHHm+ZIseJA
+  oUn+8Pl/LmtNU+xOIgyzMbZ pGLc5YHtmt5Tsl1O6eIaS5NfJ9f0sZ6xyywRP88Q2qPLz90dCx
+  /GpJY8vFbBg/kxhCyj7xJzxx3F IuoTDMgjX5Gx0zjcclT71bSaI3SGSJ5ZDKCY1O1gV6H6c0p
+  Oad2jnnOtGSlOndLVa9flYpKftF4E UNHC2CyA8kdCwOOgrZP2y0nKQSJPHEjCNgPvA/eaoolH
+  9qecAsK7hjI/hwcj86jkWGSBHlEySu2A Q/ypkYAP161jKXM0un3i5ozq6r3X87f18x7yyT6dJ
+  P8APGhQqi7uQuRmtWG6jjtXlR3ZhIDMNxON y8/TFZsyvYwxqtxGfs4MWCvXuDj9KjjSaWSONf
+  3jyxSSlV4C4HQ/SspQjKPkdMXGVNc2iTeu34WN CyDfbE8ueKdI7faqheQTnGc9TQ2o3NvJyVe
+  FU+UEfxH/AArIgSTfZyEOiOo3Nnjdnj/CpXmtE8xL mKcPJligbBjOc4P1qnRi5a6m8nQdVqKU
+  o2XTX8CXD3jW80sLzgR+WqxnbuOSQ49h0qC6n8prmJRv 81g+7HA9f/rVZN9eRNE9vD5cQVsbl
+  BwT1qv9pVFtpHmguyImXCJjGDwDnv8A4VUVK+qFOrONT3o8 3az/AKRTgvIxdsXBEewqrE/dUD
+  gH60eXNN5TwvHc4wr7V+6cdDS20PmyvEGgjRW34K9R1Y/QCpFu YY4XELBVBJVQOSc8HP0rd6P
+  3VqYShOVW1Na9nt+S/MSOdfsxhnaM7ZVdV2YLHPX8K0poyUkaYBv3 oRGXjI75/GqqzPvmmkjg
+  lRZUCzKoAX0B/Oo2e7acLHmSSFWDei/XNYyjd6aHLLDSjV9yKsvMfOYD BOwifzFbYwJ5XjnPv
+  UVn5n2CadEcohAiOeCpzkfnV20t02r5sggjnchRIc+YMYz+dKYfsf7h0e4D KC5jOApXp+FL2k
+  UuVGk8VFRcEua3m/8AhiB7G4uGt55LS7CheTu6nufoPSnyQTiITRyB/wB2+zaT gjcAPzodVkn
+  MdxfGNXDSSDJ+V+m3jpx2oQWbWlsod32QjIVyNuW6H370Xlp/kKMKianKPytp+JGI 0uJriJoJ
+  UeNkVgGwN3/1qrhHt4UTyGdJXDsx5Abv+FWJIrtpXhhglUxSBBzySfU9zT3tmguJUnlM NzGxG
+  1+ijAzke1UpdL/I2s2/ea9N7fdr+BC93JLFBJ5EbLFCyA7BkE0QiOSGPcnl8rIHHADDoPp7 Va
+  kM0wjQRo+JMII1xuU9G9wMc1Qe5khnYh4nzvIULkegFOOqskHKnDljDX5/1+BKJIJZDhZJpZ0M
+  gCHG0joPxqOFQlrEZZraIxtsCyLkgNyW/lSB4Y9OUKcpImFVRhg4PBz2HtV57KFrSBZ8xyMuAM
+  85 B5B/CiTS3Oeoocq5m/69V/kZlvagsY3u4Y5Fk+TKn5gvJNMVDPdIFni8uQfIzDgnnA/pWt/
+  Y9tIF WJLkKX3I2/kDPANE1rJBptzIwV44rkFNgxz7HtzT+sRb0e4lmE3VXK7y87aencdHCEtT
+  E0LtKG38 /eIX/CqcTyvdxzzRtsZQ5PYFsjn61Izz2YBk3G7hLQ5J4Ibkkj2BrRSAzpOks0f2b
+  7pVRg8DK8/h mseblTb6mkW2nKfXrbUpLsZo7Jka2BwJC55YL6Gp7lk8mFIZjEs4MsgZsnOMCr
+  o09pNl07G5GcAx 8csOn14qxHYSpdwiJ4bciMszzpu2kgjb9fSsXWhe9yY1YSd3K/L5/wDDlBY
+  Z5EMsz/Z4QAPMHCo/ YfWopJFYsH2tkEmU8qB0K4+vFb9pbpILVJp4ii2jq/oDnIz7+9ZDrbWt
+  v9r86J0kZXRPRQMAfXPN RGqpSsTUnRrVLNO/TRpf16/cZgR28xoGGwrmONlzgjgjHrUFsY/7N
+  kuJNqyLdRgR4wyZ6j8e1XpV sp51tjMyTPIrK4PGf7v49qfEl8k7JPBGQWbJ8sAZ7fjXVz+7qZ
+  1qXuNSirrdbO3r1BHaW4vGTckS TKrlj91ieAamFmWBhRnm2EqZIzjeq8kj2rOkR7cXEW9UIkB
+  mLDhmA4H1NadtfTJAgkKJbylWJK8s xB4B7D2rKpGSV4nJisNVjS54Ky83+Xcc9pmMsPPAbLJ8
+  5zt7nPpVtLCOK4ciOQxhWIQn7uB/jWDN qL+WI/MHmJMgGD/B/EKtz36Q3HnQrM6P5jBi+R24/
+  nUSpVdF3OCWHxEYqM+o3Z9jVZoE8xvLAfaP vs3BI9uaikSSDT9tk27a5WPzDuKqpA7+pJofU4
+  /tEJnRijQvt2nAG3kVWW7spktWZpOYnaQq2Aec n9a2jCejaOinh5qpFqN/lv5b9CNobiz1CNV
+  8mK7lTKoU546j61dg1NEMSbo5C0LhyE4V88Hn0p01 yktqZhBJ5o5GW6ggZx9KC9v5VswtARty
+  ygANuJ+UE05e8vfWpo6ejq1Y3vp9lbEskkUmlWyR3UT3 L7V3f7p+9+Nab291Lbunn273QmLsE
+  TGOO4+nNc59lmmSRmhdZy4VEHHGecfStbF9BNEjTxSRxrIu FHzEHGGzWNSCVuVmVWF4Rakm09
+  tPxtuiMM8QSQGKVUX5nVcLg5H5YrTtoLY2hQ3UZZXAZM84Xr+h qlC8Zhj80jAAXbjG4A4Y/QG
+  iKAPBcNBcxSqCUBC8t3Y1nU1XY5q9GlK17p9+j8tjbu/LNpJFbziJ GcMN+Tz/AAA+/FZs91cC
+  8t0uvIjmdW3Hy8Yz1/Gie12QIzXAOeAvcn1psTyGYABGZEKjeufcn9Kx pwio9zNYei6Gmthqx
+  piV2Yu7OigIeQDkVjX00sNsypKyyvIrlgxAGO49j6Vsz3Eql3sgiYmViWXI A25C/U1lSi5ur8
+  XEkW1JIWE8W0ZDEcY9MZrqoXveWxjTjUvepJKPbS/3GM6tJPdLCiSThw0WxfzI 9s9qdYi6m1B
+  xcRiJ3bcCUABBBBH41pW9w4s4EneDEEZjDogBfnKnPfNKLiKW3ISeBJHkWSMMOUDD kH6YrslU
+  dmrfM66s+ePJGHz3t9yMq6hiHlGJlA2oIVJ5jXP3W9W96Y6TQ3U8s9p5UXnbAdvABBz+ FX3m0
+  8RP50Es4QbY2jbAZM/epXjmuYJjbRud02/LjI+7/wDWqoza328y+WomubVeen4mbDOFtTEo aS
+  OOZDHGAQxABA/WqEhY3dvAdsPyoR5o3K2CTke1PFjPHpcUhWWJ3XchkP3gOv5npTyInu5XEcoU
+  YMbHoExyPc11pRV2tTR4ai6TdPr89fLZfgWzY2cjG6muI3UxsrpH8pDdmHtisqxjmitlkZ4VZD
+  99 l+V/7px+OPxq3cLHLcwJHMGhZiY0UEHGMDJqrKhjmInSVUQgZDEA4HH50oJ2s3uedChUkn7
+  XV9v+ BomRMn/E4jKTxiZgcRkE7F53A+9Ytu8ciQvHE7jzwgfqI25xnIrTiEswiAbzp9rLiNcE
+  c8A+5qqE tnvIFVGUq3HO0BvU8V207K6ZXs27yjNbbNar+vQ69EfzZYLp1QkhpCeAp5yMe4xVi
+  ya5tdMtQiCN VG4TTKGDDJHH51npzJN5jGSYjDKOCzHPTg9Bip1lkiSKOOTcN6J5bjJ39Mfj1x
+  Xl1INqx7+Nw0ZR Ttp2dkvlrf70X4tPeaGUzMke1xHwuO+cH396jha5SCWMrEVEm1Y9nIBHJHt
+  irV0oIZZ1mSASssrq 2Ar4ABqnpSRrrM0qrJP5J8vy92S+c/N9MVgpNwcmccZynCUpu6jsv+CR
+  LbRxX8TefldpDndyCDgZ PrWhDJG2oIrRmSMMYwndSeASe9JdPY2/2aZVdlH7tOcgqR9/3wahQ
+  tbWyRQ5kKnLS4+8V5yPqDTl JzVy515VlzRur6a2K5Rft4hc8xxYG8ZGz+L8ff2rVMFt5KpEzT
+  7QykhuCeAD+PaqZilvEku4k8mP 7sRPO5cevvVtZLRtJFrLOsCYZgp+8nQqCe5qakr2t8zsrVF
+  OUeWWmzSW36lM3VzDFJGzRl4pkUoF xuGeSPqcVaRAbO4cQSRS5KoHOQxzxgfnUbXVtDBGZDHL
+  NMpaQIOS/wDCR6D2qMzyRQXNuJ1GJV3b 1yV2j/E03FtaKxU6Kk06Ufwf6Fa1NoJVd4J4xjBw+
+  MZ+7+INWrXylupH3M4EmxtxyN204qrc+fKF lDxOFdRIij7wAzxWhYvZrqs2Z0jjdsszAkZZTj
+  FVU+FsiULU5Sjfz3YBGmsxFFGRKoy4bklh3/Kq lrhmPlvOTsBY7+CTnn8K0YI0tIZU+1RyylQ
+  5IJGQvJx9QarxSSSXAmcIttgKsaKASDkdfY4qFLR2 2No3hBqMdPuf/BFFxIsHlqrXYOG9c4HX
+  655zSwm5+02pWEP5sJldccsQfvfQUR+ZNcSolpMm3AVw funHQ8VbAgaye6j8yO4OWUbs4QcEf
+  iOaiTUdLbmNblppQS1l6P8A4Yql2aaOVraRYgmM5A8wHv8A X0NZ6ht8ckU32dZWEhMhJ2gkgZ
+  qxDZJFELh55JogNqKrY+Ugn9M0yRVNtI6sBHHLGiuBkFdvWtYt XsjalSja0Ov3fjdDoLJo3Ju
+  cs+4GIZxuVev9Kp/ZpzPsiLNJtKBGOSB1wfrmrsptiLUO0rJEjRFg /wB71Ye3OKkaWM26xsCY
+  9oLqp+ckcDn3pqc9+4QjiXUb5r/LT/L8iKa1aO4txBG880oO1E7KMbif 6fSnbNt6ELpEspacF
+  uSdowpB9D6VPE98LZpoyEk8xioIyVzxj6E0qRX0EOJo1doMxu5XhQecfmah yfVr+v6Rk+aMru
+  af9ea/IhguS9r5k1zC2844XiNccofc9qpxCB3iaRmuNvEcMZIJHPJ9/wDCtCIx PbbruB2DMPM
+  WIhdj7cAH3pbfy4rRHjtZCIEMZbgnceMf1p8yV7IrDyTbtdv5L/L9B8eoqsTuY1XK kuHAYhxw
+  p+mO1SBVlaO4BaOQx4HOCwK/MfwPNNksLh4YmBhdUQJt2ffIGfzNOlsmlljPlTRYjJkB fgEnh
+  R+FZN0+jsdsoUo2Stfv/Vv1Ik02OBY5ILpZYo/unJwSejfT0pLxmaWMyQ4LfIjEffPQn8Kl W1
+  thPKheSSKOVUCq5BGf8K0oEjkmEZUYRmMbE/d9vqTSlVcXzPU6lUnQleyb726fLr6mCtrMyqsQ
+  eU8MNp4BHX9KtpaRSWTGJlbegJA7Ed/xFbHmmSENGyW8jlclxnkA46etVZZCqzg3EH3FZgq4Mb
+  Lx il7ectNjSOIqVpXpzSt5P8d/zMtkkRI4/IZnijCFl43buao2sSTamsbQecPLO9F6lgD0rqk
+  W2mtA 0kolU4VVj4OV6knuBWBMr7A0A2mSTcSBhk7Ek+hrSlV5rrY4qVf2kpQuvx3/AD/L1Egh
+  zo0weRYw XXMZHzFiM7voKkja1SBPKLzuRhirdTmrtvb2cd5cySyM3ly7FGeNnf8AHnrUdxDap
+  GVs9ysMxg7s 8kZFT7ROVtTGEoqpy6272svw1IkPm3O7y28hXEUS55TOf1pz3rxXS+aUAGWOR9
+  5upH5VFbxyR2qP JMjE7QmB1ycZpJLCaO/YFhiIFCjDJJxwPrVe45WZvHkm3GaWv9biPKsN3I0
+  USSR5OI3AZjkZ3fQC po2m+TMSyW7EPHsXBZQPvA+grQ0+1EdxELi1fyXXMsh7seQo9Owqw1tG
+  JriQxyRuJgjjOAoK/dA7 VjOtFO1jCq4RnyqOvfT9HuVoLW7ju8bTPskUbh3Pb+dPniie9Jt5Y
+  3O10eRzuBHUfj1qS1g1VILw wzRySLPGxbZwxx1HtWc6Bb9JJLmORRG0aCMYABPGfc+tZq8pt3
+  +70MvZSlVclJJ22V7/AD6DLd2n v2mbKR4WIdssehHsMVWubX7E3mN5cx3FmOMhW6FTn6062Js
+  J4mgcMUXZKrDIVycg1PYRwPqEg1CK dwiFDhsDPr9ea6m+VuXT8zun7WLdSWqS7a/iVZl86UeV
+  EwjbcoYDAUgYqxbRytZ28iyLIDEVbIyQ xOB+NbEVrZ2sNvHCZFghDIzu+4OzHO76Crwi8nS4V
+  YxgSHzCVXAADYK/WuWeJVkkjzp43nSjb/P7 ys8LQso3llBEaheoJYYH1zTJGnk1G8hRUbdMpY
+  be685HtxViaS0MkqxOZo/MX5VbB3bsq2fSmXZE TXEiONwlIRh2HXJ9ec1zxb67/wDDHGkoSs3
+  q9romhMEs8clx5LSujOflGMMcAY9T2qL7MhWGAjaV gKMo4IZgc596lgaRdUW5uBHIkgDKqJjH
+  GPy5rQjTzLhUMkYDDCjHzEDOTmspTcXoJy9nJyh0676/ cUxMILRIYInQLtyXwQWA4P4c1TSdn
+  uZlvsw/MH3dAxAPT2reW9AXHkp5DBWUlR05GTWRc284ijkK faFjxG5jGPmB6fiDSpyV2mrXNa
+  Lgm+aOr/r+tTLgk8/U3CuIVnj3srf3iDwPapFht0RYmQzyZTcB 068ED0HeqlwCJhOrxxkxsEX
+  HKDODmtSE+QUJuIEcQsWZlz05/rXbU0Sa/qx7mKpS5VKOqatbX8dD NvYkF+QRGJWJkV1GAxXj
+  j0z2q5bWtzC0zTl/KaYNGp+9gDJ59s1dxc3tspXyc5CBwn8JGWA9+ODU +IntAsjOojhIOW+5u
+  HAPvWUqz5eU86opOHJHfr1+4zZk09beViVaMMI3JOS5bgOPpmufuIpEkFrb q89tAyJKQejD3r
+  pryWb+yUeIQyyFMyAJwxGOnpxXNrLfy226J02GRdwVeTzjmujC3te/3sinBql7 z083/wAAr3q
+  LNJA0YVNx3NIPUnCj9Kr3UTK6W6brkmRzII+MHjcPalNptsRERJ56SFZELZJJbqPQ YotYHi1f
+  E2YmCPh26Adya71ZLR7XM1ShUp817OOvX/h/wEaCGKFraOGZ388s6k7iowMYx2NU4rgQ X6FoR
+  sUsNu3p8v8ALmrhCSQtEiyxyFQDK0hOG6lTxRDp10IZ5FgZQrAAyDPXqPrirjKKi+ZmkaEY 0+
+  Wa19df8v1FsxbmCBmdxJIFI+bgbc7l+pqxIheyjiEgLIMiQdAo5Off0qC0tJFnxBPC7biEGM8H
+  gHp9abIJbYykMrEOEx6gL1+lQ0nPRm1PDSlP3anvdE7/AOQsZ/cxXKzu2Y933iCoJwc1ofaobP
+  TG kjkaR7h96MxyNq8HHsc1TMN6lmit5UhZy8qqmCAOgHoO9KhLwnzot6rGAAF4+Y/LgenHPrU
+  SSlq9 UZKjOrK+um+yv5FqbUvKMvm27FpLhREQQMKACTTrO+ZLy4le1kjJfDLkYGec/wAqz53k
+  dJFmgYkY AYcckZqeAieNPOlVQz49MBeGB9zxSlSgoaowxFKnTu59e2/4HUrMJLWUSeXI8kgMJ
+  UdVC4JHtWRd XHkRRRxlmJj25zzu6Dn8zWBc7AxigklZ41P2dt/ATGSD6k1fS2mGirctMAc7lR
+  uSuR3/ADrCOHjC zb3POU4UeV8y1e1v8i5hcRRAkSpGomdj8rYOCR6VkXEzwaxJJ9oBO5kEXOe
+  V4oK3f9ns81xHOIX2 yGJcbjj19OlVwjw2dtJPIszOm4qB83Odx/DiumnTSb1uY02nPnWt9LXu
+  EyW6WFgziaGRbfy5AzE4 cZPb61bszbyaX5kxEUrsj72+78vBwPyoSa3TS2McJuYVkTzi3zb27
+  EZ6D2qCKySa7bbLHKMsXSMY J56j2q27xak2v6/rc7aNKUqbjUbVu35f8PoX7+8SeKZIkU2yvh
+  3QDqcHA9KLGV5IZZ5C4tyQwI7F eMceo5q1cxzGxePfBsZg2FTB7Y/E4qaGT7FZ3LXSKsTT5ZA
+  uNu7jH4Cubmj7O0V/WhrUajh+SENP W/3eZi3TtLaLNENivbFo0bnCg4/rmqE1shgQXE6IgdeB
+  wSMYrduWG2BbOBpPJ3QM55Vh1Ax71gTw PNPapPN5anBG7sD/AIdK6qErrt+YkvaUbN2t56/dr
+  +YnmpDHIyPDlWZYjt+4meR9c1VJkltfnEig HcxxnlakuIooNyAhgh27gcjcecVSkhlSOWRUkC
+  LN5ZQnkMR3rrhGO5FfDUaLjOc4y9dikHmEbCNA 0gl3NlR3GT+tRiUDaTbuzseCGwE9AfXip1J
+  YyseGBAPbIHBH1qooMbqgyoeYAE89sYrsVjilNU0k r8t32t8kdUlsVuoTFIrsVMgwSSCp4z7V
+  rLJd7fNktlU+YJTlRyQfmI9MVNGIYZVkluLfzIFZBIq4 VgecY9TniqjLci3h8y5iTbuEAI+8u
+  fmz647140p871Or2sK1W84Kz6vS/wB3+RrjbdXL3AIOSef4 BuPcepHQ1mTR/ZEEkUiiV14I6A
+  c8mnGd2sWZZY0CzqsZUYyjHv8A0pzzXCNeQmBrmB5cK6DoD2H4 1jCLT8jehDVtPTte35kqzQX
+  GlqLgxyRYUKFXBHOP1pkd1DaxxIwM7S7nB7Haxz+B6VVd9Si0+YS2 pikZ/nwoAU/T8qkdYY5P
+  MmXIVGTav8DZBCn3NPkXXbyMuSHKnJ6Xen/DbEs89y/lSIvlJIG3KBxu x0Hpjj86gjtjbT2kV
+  yBI6RO0idw/UqffpVNbuGW5SZkkdBjzQrdJD938u9aE8SmCKaVi/l7gxHVn zgNn0yelaOLhaL
+  0Npy5XGHL6f1djkXT8xyurhpommyTxnH8gaqbrIyQId8L7P3nmtnJ681cgtylk rzj513QyA9m
+  J4x6Ais+aeaeaWHy0iRiWO5Bk9Mc/hRDWTs9vM2oSUnzRbSXX+ty3Pp7SqsxVzIso jmVCOWPO
+  R6ACoBZzRSLcW+4mKfayOufM54K+2KlijvWe6ltyZMlgWB43Y5wPX0psEdzdaLApdonY EgN1I
+  U9aalJLWSsYutPmSdVcr6FgzzedGtzayMLdtrquAy5Ock+gFWpbotcyLA0LpEJN4Vfv4+bc PQ
+  Cqcs7z2NwVkUtJMrbMfNntzWmjKkJk3RGV2Ln5OhPG0/XmsJxSSbRdWlTjFT3v0u1+g2W5uJ7M
+  +eq2cciBmlIAVm4wRjt2/GsqQSR3LIgkdWbEqqeYx6VekkkUvHLPAkQO2NWTqfUe2eKpWN3Mmb
+  kj lNsUgYZwxyOfenTi1FtImkmqbjHTy/4O5enjJZ45InMaodgQ42AkZU+pqaC2jjm2CN2gydk
+  TEEhT 0z+NZzieHUVXLeWu5CzfxIR1+ue9ajR3I05fLXdK7AiMctEq8EN6+tRO6SV9zplSbik9
+  O3b79RiI 091ZRmHASB1AI68/MTSzwwErLaRSxXEKlnWQ5BLDjj9auLcTW86yybIpnZkJdchi3
+  TA7ZrR+1JHY sWWMTMMsuAMkYCiueVWSaaRlKq1OL5fknb7+5k2EktqC0xTy3ljxuHscD+tRTX
+  d08hjSNsl0EvHD FuSfyrQkiZ70htsbZLbSPvFeQw9h0rHnE0kUEsQa2kYiYmQ/eG7g/QVUOWc
+  rtDTjOUpWV+/b9S1e 2rnT5vtUUkSrJ+67ZQYzn1PvVV3IsZBHG7xKwOFPOAQeT61YlZ/LnLah
+  FLILjLA52gDoMe5p6wRz 2Db5lilcgynsrD+H6mqjKyXN+peFa5V7V6X81+DLVvHFDdTXCOVDh
+  xtds8tjBH0pzXsLWht5JoVu o8KzEYDH+8PbFTzuscSQMgcxp1AwAvGQfU+9VGa3m1a6FsiCNp
+  N29hnA24Fc8fe1ZpTpSm+aSbt9 35CNawy6a5jcKZpRJI+eFI4K/U0lrFFDujTeu5SGLN/Gen6
+  UsM/kwKqWsv2lwHZnOUGD8xxVu7jR 1JVJJELFS0ZwNxI5/pVuT+F7Hou7ioN2+f6f8H5EFvCp
+  W2XzQsW0Nk99ucVaWN5pJDJCiGRlO8qM L8pyD6msiWXy2ZYZAkkZA2NyV7MPrVyOWZfPLMSIp
+  ECj0OO9KcJPUzqwrOL2+4pzz2tvIVKSIykK hDYByOf8aRgrDG4FdodGUcNtHH5mtNprI7S0Ik
+  kdPMjPXCg4OfU1H5cCXieQVEKIwjY8hkB4P5mq VTTZnEppL4NvPcrxNehIpZXt4WX92WaPhj1
+  z/SoGs70xx3KyxS4AyEX7rBsAH3NWJnuY7xZCnmoI 3RgBwG4yfwFSRqbgHdDLBChKMd+A/fIp
+  8zjroOFWUW2rfd+BTdCxuJLf5JxcFIg/TB4PHsc1OGuI IQ1yBNtc5KjmQgfKwPpTzaW7Lvkkd
+  pXBkYK2CGX0/PFNvINwaVp/KcltwOSFJHTAo5lJpHVGPtOW Mne/lr6dyT7QPsCXE1vPGZXRkZ
+  m+UBe+KmeSO43TAMZCxdQD9854bHpnis37ZdR2Sh4lmIdCrhfk K9CMe5qSVriWNo2spIY/NVV
+  wRkDOSKHSs/8AghUw8IRTqaa9/wBN/wATShvN9rcK6skr3AcDONoz jafcnpVRUabUlSeAr958
+  jgAZ+Zfr3HpUEccaXL/aIp4l3n5WfkAg4P4VU+yqpRFuDCxUMA75Jx94 frmiNOKvZi+r05J8r
+  aXn+mv5izI04k27VgKAmQD7zZ+U/j0odbopbSy3lujPCxddmDt3YYdOvvUr xRpZKFl8obtrK5
+  z8x+6PwrTginuojJdQlvIfYuBjjow+prR1FFX6HVGr+6TWy6dfxLtoQ8c8X2eT 7OkyeSx9O4P
+  qfeprmNpLxljjkjWM7cOchQT90+5I602SZ4QzbGti6kFW/hHcmrlu9r5EihyAWDBm OThTwf61
+  5km0+ZI82cb+9GNr+v8AwSsjW7xKrPGyhz5aIMMFByQfU+h7VBHpc8eru0dxHLZyOZEB GTtUe
+  vv/AErQC3GLmQJEYmkykioMHI5xUNtdS24hi8yJkRSACvKg+tSpySfKcSlVgpcuvf8A4HZ/ Iz
+  3sofNtpGlYxmDLqGOck4XHpWqQ8MUv2dhIS5IBGSoA6VmNcSPJKlvbmWEbTkAdc9KlN1eTSNLH
+  H5cu04GODu4PFaTjN2uzqrUas3FSlp6qxpwSvI4Wd4drRMyoqYPHU/TNF2sc9rCXmEbOvmSkHA
+  3q OMCoYpDECJ4jE8b+XHuPJTbV+Kc3S26SCPCxkAhcY9j7nFcsouL5lscqoTTvHZHNCKMooME
+  ql2LO XIO1gM49quNaQTWSTw/61lO5Sc85A/KpL+5jgjiT7LNcJMPMZozjcemR7Y4pbeGO2mmd
+  ILhTFMUj DyZGGGTn8a6nKXKpHe4SVPnWj/P8b/gEsSy21xAW8opdGPepwDwCuKx3EFpcPDNI6
+  CYhiznofT8q 1kHnRndIibdo6cZHc1HJN9uecS+SUaQFDtGFGOR9cinSk1p0NMLGFNuC1XXXVG
+  UjTzvPBaspjiKq X25Bb2qi9p5UYt0mCxKrHaoOTg7uv6VrhJoZWeFQ2VLsij72Bzj6ViPE8ln
+  5aiTMLADD8vH1Jrsp yu9NEOrDnqe7FL03+exERCZVu1czcFGjB5y3IJ+hqfylluXdgyxRYTzT
+  yGJ5J+lQ2lpH5zXMtzGk RdQuc/Nuzg/hSSSxQ3JktbpWicFgCchmXjj866Hq7RZHs4qbhGV2+
+  rv/AF+ZJILf7SjxSLKm1o3d eiEDof8Aa96iS4nuZbdSxiTYAE/vAn5vxz3q3BePFgxRRu543b
+  fllJOCwHpiohcyGVEW2zFuCJtA znnHNSr9UDpTjFxdr921+H/DlGCxVbi6E0rwx27vlsnnnGK
+  c88aSRTiHzI1j2qpGdxI7+pAqw0N0 IpYxbSZZjLIzHIBTsfrUbpujhuVjK+Y6FUx93J+Wtea7
+  u2b/AFeDSd/x0/r5kSNBLbRx/Z7rzEQq H8zO7v3FSxzTK9qIpYtz25Z0KZbIGdv5cita68spc
+  KIDGiSq0kx4USDgL7fSpY7ONIZLiUokxLsr Y+Xb04H6Vi68bar9Tjq4hNezcbLp1/H/ACOUMk
+  LF0McksKjEJDc7cZyT3piQGGO2mjk/cmPzJmxk K5zgfiK2IraBr6KQvHHaNAAueoPZT7ms9ra
+  K1tJo23AvMHkjLcgg/d/CuqNWL0RTdNz9lDVf1tcv wS21vZtLIY8TyLhyPuqO361QnfL2Yhcl
+  fIkJzyCVOB/Om3TRXcSBW3K8YCp6MTx/I1SVolZISxQC N1aUscHBzwO1KnS+11PIjl6hLmb6/
+  wBdmagF3KgiSMQxMS0e5c547/Ssk3l4t5AMQSBLchMx5BBH NaNtcJ9lZ3nWSE4MajIOAefzrO
+  iWWK4SR4XKgMoyc/N/hz0q6Ss2mkY4WlBVXF2t06a/PU1bCVfs QlaSJi0iK7BPlj/ugj1P9K1
+  ZYIbi4SNJovNicmIxjb5qLyT7jPrWJBNFFZkeZG7W7hCQuAQPUdzz 1rpDOv2bzYZYJFiKoSq8
+  juR+NcuITUrouvRlF7t32v8A52KsmosWeQwqULBlPGNp6j61UWSG5SdJ pUw5fqc8Yzj68VOLy
+  T7S7xpDGrqzr5iZDJ3I9MHg0wWc19bQrcyQRzCMsDGu3GDkqfU4xUxUYrXQ 6aapQa5vdfe//A
+  /UZAtumnyyW7MqvIsgBbOzj7p9zUEtrbzxu2HkllK7kDcqWPT24zUsL28jQlIJ STGfM+bhSTk
+  DHrxVi7zbTlkhIjfDD5uQFP8A9c1fM1PTccm1L3b3/MxbpbizkzDHHIj3BJUqDtbG Bn+dUpIJ
+  YLaWQyorMweTcM/MOnY8nrWkZJbWZZfNRhjzkjcZJAJH8jVR57O709AxaAq2JWz94kHa foK6o
+  OStpocEsP7KSm43T3dr/gc9crbgL8yKwQDk9fr7mqgjWBZlIyBNk4PtzjjiraqstpLK6bTH KA
+  QcEk9M9OlV1imS7BWFikZy4c/qa9WLsmrl4qUXeUUrLa17/wDAOhS3idrkROWiVxtYEkYx9PWr
+  X76ZImeVJV2NIVHDe/bip5kWSKFrGSKS3ZGxtTOecdxTzb21rpjSeaY7xNqKrHOVyNxxXmOpdL
+  v/ AEj2VOM6NN21u/Pv5XXqT/Z4r3S4HRXiR2yuT1wMAfnTIEjWRPtNpd206Sqkm+T5ckHAx65
+  rVilS a5dFjK2aTO7ODgZHQD0HtWJqCi5htsSMJJdshwenYA+/vXJTk5Plei/I8K85N05PS+//
+  AAb/AJml HLMk91ujIlR9knmjI3EZHHtVFoVluoVkP7lwfPfP3ZAMqPxqovlSPKDK9xlT5SKfm
+  LN6+uKvwyQW liqG1uHR49wkL4Dkdx+NaOHJtuegsBOLUqT956WVv1ZVaW28i38qLY+3bMM9WP
+  U/hxTGtpDFKEn2 Iku0yMcqVxk8fWpzfxNaR+TAsOSnzNzkscGnPLK32y3NnO7gtsVT/D0JP0q
+  1zJ7EUlUhO0rr5jJm uLZpJAj3KSyqUYH5Qdveqtuw3Ol+6KisAQPvMcEjB7dKfarC58ss8LjD
+  RSO+VwMgnHua0T5uIoo7 ZZJYsDO0cHHOfU9vxoclHTr9xq26TcW/efy/HYLWZrlZhBiLePMVs
+  cLkfMD6nikiGYo7gMDH5REc SnDEHjP5k1HFfvLeQSyRi0gVDHtK/wB8kfpTbuxmmkhCo7xJhE
+  MZxwO9ZctpWehjFxpNptL+u/8A wRIbG6MqxG1mhEaFXY/xMORV7SxdW0EqSRF98yy+WR8xJBy
+  QfQelJDPMFkR4pogTtDs+QST1/lVK ZRbXFtLcSSblRl++QGcHkfkaG5TvF/1+JT/2jSdr9P6u
+  aEnzTQ7UCyJC8RmZco2DuLY/SqSQKsfm WQaSVnJXccjGe47kc06K5dZhcX0TvG5DeWnGMcMPy
+  waksJY5dQwgEEQlLgscjOCFH09qVpRi/L7j aFOvCLVk4rd9PTq2W4kuPtL74SY3wVfHAIOAP8
+  afLcXsQe4VPNLviUovRs4A9qtQzvDb2gkIklEB 2qB1HJb8fenpqdrJFbspCRSr5nPYqcCuVyl
+  e/Lf+v+AU61Tm5uXTyJD54M6ytC3lybfucqQQSfwz RPLK80uQgWONozIVGCx5zj6Uyb7MZLlo
+  JDKJL1fM+bOHHQfjVq5vHgtC/wBncu7FipUcnpWPVWWv 3DpzfMmrO/TRW+8pvJE+nRvKHcfZ8
+  4DbWfoAQewqhNNcxDy4YTIF+UbhnIzyw9h3qaS6uy7pGIuQ BsMeTgc8ela0cSzzM5jc5YlwP7
+  uM8egz1rS6p6tXHUgqfxK9+n9aBaWVv5k7GLzlUlUcdJVB+Vx9 ecVcT7Mt0ITblRJHuweoxnm
+  pEgRYbe4wzDyP3m1sB+eCPQe1WDPFJamKKFt/mNnJyRnGPwrgnUcn 3PLnV5p3SuVv9FaCAvE5
+  +T52zkEnnH9Kom3RXXZbyKBGfMjBG5DnOD9KsraSnWopI5VEPll2BHCl e31q39pkkmW5ZFMTK
+  wIC9WPb8TTUuV+67nVGXsp2i2/M5mC2aW+mwWI3bkTPO3vWza27PNOyzKFM 65UjO4kdR7AVY8
+  6B7u0aTbDstnjePock8k/Sr08scWnOPNgMaDYsqDAYsBz+PQVVWvOVlbc66let Oy2v3X5abmN
+  NCdrPMiXAlPm5iXBCrwxPt3pqxzM8pBjWJ5FBbb98spwR+lIryqsizXturyEMkeOY x0Kmo4Hu
+  ZvLjjZRIqcKRnvyPqAK0s7FTjUS7tddkhs2ltBpA+0TIo8xVQnIPbP4UwRLE26FWuIw2 GZTxk
+  HgfjUbSPNeQFXaZvL3pk5UYORkGnia7hiWVpYHdgIwipw248vWiU7avUuMJxjac1d/L9P1L hJ
+  DpI0J80Psk5+UsevH0qA3EM4ZEtppoQwSNo3xjjv6mrsYSUtCt1DJIkiqzgcO2cAj8MiojLEru
+  LNDglh6hecY+uM1imr2tqYuUIVLPf56GRskMikSBlBRDgc4P3m+lW5IofPWNLa4uY5d8keHyCA
+  cL +dF2YUQhd8RiiKKCfugjjPqafGzC2i8wNskw0YU4yE9Pqa6HJtJ/1/XU76kuSKneze3T8v8
+  AMy4I HjkmsGhneUKTjd90jt/WiyEkK7r5njVxvJYnHcZ/lTb67vWuDdeQ8UUoYqcgHAAB5+tW
+  bcSJbtdS zQsPLKiNhk5BFdEnLku+v5ndOpJ0730fTe79dbfcLFfLIzSxhXjT5UDc/eHU+vNVj
+  cSmfa0lvMI1 2nan8RHBHHTtSKY7uSeGYpA7EkMi7VA69B6GrL2MCWMsjh0MjAvLu+VSeSMe+O
+  Km1OL1WpjH2EZJ TXvPyX/A/AzUWzaxjlh3mdm3bGbOzPAU8dfetlPMjs1DXCgRnbK2ThzwQR/
+  KjTbRYZWmdR5ckqSJ nogAPyn3FRy2NwLxXhmAKYJLDKvzzx9DSqVIyly3HXrqS9k3dLW7tf0v
+  5fMvvtvrq6kmnVVSTZGT 0Klen51Zt43kntRNGwLxlpQBjaQpB/AHH51lzSWa/wDLUSyqGEZQ4
+  G0EZB9W9DWnAtvA8k0XnhGy V3Pu9MD/ABrkqK0fyPOxMWqVrvXb19ehBsd47Yq0hRYQow2Bub
+  jNPQl7x4nXyGVRtZ+c461LcP5d vKDHJGuVeJ88bVOSPeny6fmae9SUSjcVDA8epI9qjnVtTOD
+  hHSW729fkTzf6FaFlCFuhIHTnJX64 70W4jw0pwskThGTPIJ6H8jSP5aWocXMUgB29M7icfNz2
+  p7wbtQQgNcbFIdYuCGHTPrxzWKempnCE akL7W666+WqJhMSssZ2OQ6lGPPANSPKRbuPMiHmfO
+  WC42lT938azYS0gQnbEDAWkdhw2T1FXH+w/ YZD5hKyN50eG+8FHy4+pqZQSZ1SioOLStf8Ar0
+  K017KJPO8geVEAhXH3R1xVhVluWX7RGwjdgz4O CpXufYcZrGaRRbiZriL5tplXHQt1P4VoQXs
+  sUyxxEeVENg387s9K1nSaj7qKrUajheO/4f5El3Dt VHBG9Q0bleFZ2xgj+VZPkJbm4nud+2OQ
+  RuqsRt68fU1dlmil3iSKaF3YSmTf8oAGNuPU+tVry9jF k0qrneA0isc4f0+taUudJR7jpzlC0
+  Hd839b3IDdwR2iraXC+YWG4P8xGRyPyqvEbeJ4zI4RPn2lu SVIwaagt7qEGK3kSSRW3ZP8Aqy
+  TkZ/n9KpRXzs8CPEt4FQjfEMAnJOefpXVGlo7fMyq4eN2438+9 vUW53w2UfmhZYQyD5FwVOOh
+  9+lVbtPPs9oEcLRO3mLjld3JrWVpZURoYtxjDFiVyuT2x6nt6U5Ve ZSssAgUx/vC4+9Ifu1pC
+  py2b6f1sFCm5csnDZ91+Wv4FO0lkP2VXgLb38zCjBYA4wPTHepLu2kij gMsn7ndIHK8YPUH8e
+  gqMTC1dI5UfzfNUO3/PItzj8qY0t3Hqn2lIpJYd4yjcg844p8rcrrQ2UHOq 27Ra1t39GyeOcm
+  2lHzI4uN7BznjHSrEt9bHTohKoDbQItoxxnk/h0qa6aKG4LDYYEYxbx2PUZ9fe sc3MDCRZGjd
+  5JF8jAwGx1I9s9qiMVPWw4uEoqXLZL8zQdI557p0JCGZQqs3bb/MUskz2LGUt50Yj 2FevIxx+
+  tRRPFdGVlina53kgxnClj0IHpxUM7XE0GZrWR5SNshU4G8cjj3AoUbuz2FKLqe7KVl2/ pjEu7
+  c3XmpbTJlsIHbKsDxkD2NYz2TmeYtdR/upk80nPLHIJ+ladzZRTzI0YkjGd+C33EH8P1PrV SS
+  BHtZ3Ln95IHIzjO3Oa66TivhZxR5HUcU3d6bf53IpbB1uYYUBlUgHzE4A2nj8+aoKlu08quJEh
+  MxdVJ5GOhz6Gr/2KYQLMPPMTAMpD9CORz6YrMuMpcyTtufcu4lRwpbtXTSfNpzGcaaqzbdS6Xb
+  Rk DLEsyqN0rvtKqnGWJ/kOla1zqEaSToskasrOrgjq2BjH+e1YQh3XrEllKOwVc4YbR8p/E1a
+  eKSaB ZWCsWctK5GAGx0rapCLauY1pxqVouT08/wCtSxZvBJd2ql0KgFWXH+szncTV1PNKyeQ2
+  2MREyoeS GHGfyrIitXe8hd5UiD5ZXxhffFXY2FrGro/zAb2JORJ82Dj2rOpFX0dzedOTk2nfT
+  b+tP1OgWT/i UZbaJFJjDkcRAkZQ+57U37JCHRI7o70jZGXeST3H6Zo+8t/JJGTbNciJE78/xf
+  nirEXltHKsIC3O 5TtbrgAg/n1rzrtbHBR0jyp63e2y9W0Urf7SQscEDKe4YZyOoNF7Yl5PlMx
+  hRVZZA/AHX9e1axVZ obHZPH81u0jEcEkfKD9MdqghQG4Ehv7cRFSAMZBwNuKSrO/MtDojimov
+  m/BfqYd3HFFdrcRuHiki YZf5s5xjHtUVppsrW0bQTWzlSSVCfe54P4VpzW9tcae9ujDeg3I2e
+  EC9Aapz+abBIVYBpR58hiO0 oRyV9s10xqPlST18zgxbVSmrXu9/T+vMyXu0uXmkcxQRhXQpsx
+  8x6E/iKyklZZnmEgEj4Vwy5Bz1 NaV6sKziRrZ1idcnkDg9jx1B71nypHJYRSLHIGUlQikbj7n
+  6V6NJRS20YKFKlBK10/JfodK8UElq 6wyOJlYsoUlRjAHSmmOK3voBJvS6nQvG8pBQZ4xjvnmp
+  FW2jluZEY3OHOxIzjC7evv1o+0tJZWbM Ix5UJGxly5HUHNcSb2W3/APUbcY+yp3t0vv8/wDgj
+  jBciM2rEq4bZtBwQBySfXHeljHmyC4Ks6j+ FTjfk8EegB7VKfKnh/1jtO5G1QeSp7/l1ojTTp
+  kSKJblZTGH3+ZwNp6EeprNy01M4RjKDTbv2tt5 7leO5jZoJriKOO5j+Rwq7QGJINXL+xiCQfu
+  p4lUlGLP8vr+HFNheT7agMlvGFVivmx5xu9fUj1q9 JmJGMVwkzW6FW3DIJx1qJzcZqxnKFWlU
+  XItfV/5f5mNcwSWlmiwtDPBvyzBejLyBTS+5JJgksDOn yFnPzBjy30BqxMsdxYqROFgMy/N65
+  HLfSmzSrbPFALZ4oWdkidzkBDjJ/A/zrWMrpLqdtFJRV1eV 9/8ALTVjEV5LgrZKtxNHIVXAyG
+  Xb1H41YRJ2iImtrlZpVBQA/fUHnHvUaJNCzSQQyOJmDxlDjIGf 5HrVxms4LaLzXlW5KsQ5ckL
+  xyuPfPFROTvpr+ZH72FW9PW/rf8HsVknji2IkLRQXLh4Wl5A4OB+d TNpkku1pPNG2LaUV+WOM
+  7h7DvTIHeS1SV7dzBkLGpxlM/dJPsev1qKWVxdXXnmSBvMEMkjMdqk9T jsP8aWvN7un4mNOb5
+  2ua3fS//BIY3ubuztrfyZHZoWmQr/EVPX6DHNSyxOGEs9tJciU+d8hwMjg4 9BTJT5AFta7yYV
+  MW4HkEkHH86da29691GtuGh+R4nMnIUsen171b0V9EjWtRhThzSklH539X1RXc i41S282KWNm
+  fIUtwTjnj04rahtrIW7SSwyxRJOGbEmMjHB+maptZXH2qO3hbNwsR/fNysijhmHpU TxXVtEoE
+  c1zBIygOG+VQDgZ/Opm1OyUrEyp05zT52vJP8f61NcLHBYxRQM0beUwfzTuKv1xn6Vbh UCyt4
+  /s3nTGAhGQAKQei49TT9gF0yNxcrIFkJHEjjqwHYAcVPPNGixi2t5S4Qyrls7AOgPvXnym3 ZI
+  1dRNxhCN7df8+5UNusskccLranIEofna2MDPv71PJbX9sSrJ9sXaQAg5C45bmkE/2lIPIZEO8j
+  btyQgOSSe5689q0rkiJLnEFzK81wJoiH6xgcge1RKclJImNZKdlq/P8ARsWxs/tItjHGViWHy3
+  kb uwPyjPvXQWunTRzKzRs+IWLIvWIgcq3qcHNWbQRCVLiCVIkGY0BGQQ38WPY55qaEiHEby+e
+  w+UFC RkdyfWvIrYmUmzzMbj5Oo4rbtZmb8jWcSRQMsbIrKzHO7HIx7VOkAmWSVmiEm8A7RjKk
+  8/4UkxtV sA2HTceDu4UHinXSmYmJZo3hDEsYhgnAGOai76aEwckk07fL+v0GSR2ttdTPIrJbq
+  pC5bqvr+vNV AlqYQouYWCgcgdxnJ/rUpWEW6y3SycKQkecce/8AOqRikayUzyQ7eMKi7TKAeW
+  B9K1grrVnXCPNr KTv3/wAt9fmVWgjlulmKtMoXbD5Zx5ikE7v05qpLdwtHG8bKWmgEkkY7t0y
+  PQD0rRt1s3uTvuDkb hEVYgHGaq3AspLm0El1axRtCdyqMEljkYPpkdK64SXNZp6HRCvFN8zbt
+  6/1+RmKYpWCHKMw2+Y/Q DNWJLSJLhHt5wylJPMKk8be31NQrLIbtX2KiKv8ApBYZCzHoPb6e9
+  ayXcxiJuFhWODdHIQgGGYdD W1SU4tWLq4mtTa5Ze6+l3/X4mNbllTdEolmli85IQPmAX39B3r
+  TTyzbNdSyQJCZBIGK99vAH41Ri vrWOGJmjaFzHtyT90McYNS3rF9IurePbI0UwChR0A5A/GnN
+  SlKzVh+0nWnGLjp3f/DWNKSSBHtnt kVSYnMuRnbJkEA+9c7LdzvqLRbNiDIdlGMs2MVPdS3U1
+  kB9imYyHO2PghiOW+g71KksNskSzTRNM snlv/wBNGYfeHpkdKdOCgr2uzeKjSjdLmf32K/8AZ
+  m7U0Ed0paNvLlDZbkHvTZmSOW7V5N/lyHy9 v8ORlR+dXLW5Es/kraTlMA4Vvm3DI5PepBOss0
+  YjeAyLCQYyuW57n3xzVOc+b3tbG9T2/teaetku 2n3f15nOzsGjh86YMnkgTqB9yQcge2atW5d
+  pJPNVZGbcz4XhG6Yx71VdbaNykkcjR7Fw4PDHPytV a6kuBEQJBIZZczsgxsJ5H0yK7VDmSS/r
+  +v8AI6qlLnSSW/loWIzNHqJ2wGOMxHzN3Pl+x9yeadvm VY4btWMUICMc8FuWH4ntS3n2iF5Xt
+  juLzEqMZyu0ZP4UtpPGIkjvJY95ZDAccMncn1PvQ7tcyX+Y qk5L94o3W3mrFkjcYE+2Rwqw3r
+  GwOSMZakvZE+wLIkrPsXaWV+BuIIH1xmrS3cRnjMUSFER9pwCT u7D8OazJZbCKyYW0oeR1IjD
+  cgLjv754zWNNNyV0zPD0n7SN4vXorfj1RPbJFNqiyFCsETPGiHqAw 4z6mty2sc2v2eAtNswy4
+  J5VRkj6msA3jtpaRRQP9pRhuUdcFeSfpVmKaeyty32pIs/KuQSX28hhj tg4NTWp1GtHYmthcR
+  J6aNuyTV/yNCJctcrIkquVLDzGyAD7VMZWOivbPcwxokiguQcOeuR7VSGo3 E1lFODEkyREOpX
+  +8ePy71NLeyyQwwBrc+c5ZW8vggCsJU5XV1/SM5UZtxbVrP8fmhyJPsYRlZZnQ sPl+UnOOBWm
+  rSxyTysrsqvh0XggheTWa0837mfckcaRNhQvLBiMVXlCxz3iolzJL5n7tRJ95QvJ/ M1Dg5bly
+  oycXG6Sfb/M2J5IDY2j7WMZhdWRTgglhg/TFZsdzG7/Z0Xc0LbAfUPzx9MVFDNdyafIk 15av8
+  6gFUxwetVJLuK2uCgdBKpKSP/tbTkfUVdOha63ZFPDxUOTVy+f9Mv3cy29rEWaCaRxtjVVx lS
+  Rk8+lQ3c5js5oyvy+ZmCQHG+IEEsPoaxnhiFnBPHeASBBhXJJx6j9aZDe3YuUjhQ3LDOwlcgIP
+  Ue9dMcNomtbfI0WEcEpJN27ppFm4u9QDgxbPlOCSuQSev5dajeW+NySINyZVJSRkEZxkVHNKUn
+  ka WGVYGLgNu4JIGPxzUFpBcRFC8u6dkYFM/c55B9z2rdQSjeyOmc3Gm5NJX7f5pluVVW9lSW9
+  S3jjk ZIgVOcDkZx19KjkK+UrmaKIgESEJwHOCBj6VTa7K3EMXlIUCfMsq7jke/wBOKszzpc2i
+  iXbbAOzy REfMxXoM+pFVySTVzmk5qUVKL+VvyaL9rLEhWZYpzFMTKxD8DA4B/DmonKzhV81ow
+  ULIxbhj2/Md KwBeh3jyWWIRsAo7Z9cVde33WzSrcoYUKqNvcgcfzpvD8ru3Zm8KPLJOctXt1/
+  ployKlt5nkTIZH BMjtlY8cBT6sfWozvkgaSAkSmQgJnOwD5Sp9z1zSERGSzHnfNtj3J1yDnKn
+  /AGj2NTbYrbVCZCwb DeWOnGO/qQTnNLb1Oec5xk1zNW/ru2KLq7j1CO3kUJbxKxl3LnJXpz69
+  Kr3ZkS7t5YIQ7EBmXbwp H8HtU0rl7WFVJDxoVaU8qWHr9etTKXWwgxLHGOfNaQZw/b86Sai07
+  A6iSulZy0tr+lyzZpm+keSG W2mlckZbAVQOePY1SVIoLiKeQzGMx71ctlW4NWR9rNgkp+aSWQ
+  TFwOEwPmX8ahlkQ2wQxyIrI24u 2QpzlR7VlG/M/PQ5YSnKTUldS00/4JRk2/YfMS6XmMKUYnO
+  abb28jwWrNPHejawW3hPzEfxdqc8g WGJnUGT742jhR2BHqRmpJlu/sYLIsXmHMRjXaVx2/Gum
+  7tY0rKrLljJJLpzW/TVfeijbxzPcJaZk gRUfaHyfcCqSR+feO9427LLuVBjIPf8ADFW1t1Qkp
+  cExfKZctzkjgA9uajuZJEt5YJAg2kCQFeck DC5roi9dOovYTm3GLS032t5ruZpk2agZIysqI5
+  8wj+LnsaS6jl+yvMsqxQs4XY5OX78fSpIYmkae IzRQqCMswIAI6DP51AiRlBGHMs/mFozngdu
+  hrp0T9DmxKUPdWr6v+tCS4kYmyWORXYKWCqMbue38 6sCRTpxcTRyTEkyYHA5xgenrVaRvLuFO
+  1WAUJuC8Zx0+tOjjWR1jlkS1cgMA3Yj1pOKshQpRp8s+ ezWun+a/yNpLactG9pL5yR7lcjoWx
+  xx6/wCFbKb5Iw8vyXQAPyjHTr+lchBJKJZwJFjZiX9MgdD/ AFrRg1OOGGAtKX2hg8hP3nPQj2
+  HpXFWoze2py42VZNe957a/MkDpPBFvv7dEKFIwoPILdP61cgtJ bdblYrqCRIpFiHy5+90NZNq
+  VnhdECl1faJAOCO5A9hWr5klhbpOgEqJ+7wf4snhj6nHSlVTXur7t DpqYia5Y3u+istya6S4h
+  uJIIjGY0QgELyWQDjPuaw5lultvlcCbzQsh28JuXhTWq2pG8SVLUqzRz FI8jqrcA/hg1hNNIy
+  l4b2CaKI4AUHLkHntRQhPqtTzpRlN++kpf18iCeRxbrHNbSkhRGxLdz0OPp WSscsUJaUvEQAO
+  mMcnj6966S7ufOhVZbZ1kSQkkHGQCOPqKwl2/bY/OLtCGLvHnk7T/9evQoSfLs JSbV+W1ul7/
+  dZnTNeNFbzKluqBZgWyvP+zz6ZzUzXl3EZHkaBi2cBYxxhgM9OlPuILq3PlWuyfzX V5AV3E7T
+  x/OrOWnlMV1ZOJtkjuVwNhB4Brz24WTsmvxO6nUpQs4QvHu7N/dYoSTiRproJlWlYAqc BvlwC
+  PQe1W7KSWOKNTPaW7RgQMkiZbkZzn3qNXtLWeASq0YaFlkLHKrJgkcf0pVFxENNkhCSzqg8 9C
+  ufMGeo+lTKzjawSq3hpv6f5k5trmCCI3SiS3SIq4UYYMTlVJ9apmK8DqL0eSGQxsQuOpzg/wC1
+  itIkuJiyyMokJJ3cSFRww9u2KYriW6HmyJmJVKKe27hgfU1EakrHbhq1SVJpfr+C7+ZS+y2jzz
+  /Z pGa383aELdFI4P50XKQpaRBVcDzfLAdsjkjj61ZkjhuLF2bFuMt5YPGUUjB/GpLSzmVXKXd
+  pHhw6 iRMjAHT61TqWV29iKtSUaampu/8AX9bGfHebrgxIDIomC8H/AGuMflV+5lFrqsTAQfZ9
+  hDiVd2/J +8PQZqjdtb3Ea4i2Y3LG6KAJAT19znimxRQPdRLdPJBbDCLFK3zg+59iPyqnGL1a+
+  RlUnT9m5TXy 6l26kIt5S6mTMhwsfy4KAY/Dn9Kz1xc2yKjCOUxESLLyGJIy34fnS3ZjEc6rIZ
+  WlYPIQeFJI/wAK fHbbyfsuN2HQhuSpyDg04JRhcKMKdPDqUtLvT/gq2w24triSC0bG4lmMmwY
+  MhHHFWLWKMX8qXCXE McUy/MZMbDgkbv5VVNtqNvbRtLudFfIC9cdx9TWtJcTXFlKgVI3aQCRS
+  vzO4GQ4/2QO1TOT5bJ3X f5hTs6Tho0+q6fn+Rbwz7rqBiwdhtQHDE4wCPRR3Hesclor17e6lY
+  pEpRsNgNkZU/hUTRPa2L7pD LEZsIQxAIPP61baGCS9SRrqG1x/BMCSejDP1NRGKj1ujehTVKi
+  6knfpp5en+RHAL2c2+JhPkFW2A 7sHhjn2rZjuXs794IrW4DJKGVnbIwOpPtjtSabBDLJLMZ0d
+  pJgz+WdoBHOAPT2rZM3kKZLsxxq6E oxXJA6nPrziuWvWTly2uc7xidRwUbp7b3v6LcoXzSyvu
+  tzEkqnZGqr/A2PzPfNbCQusUJuIZv3YZ XkLfKp4AOPSs2RJrmzhbg74S5KDBBBBI/lWpDIjrZ
+  ySCXzZo3Yqz5BI6ce1cdV+4l2v/AF+ZtWqR 9lBJdXtv8/8AhjchV7e0iRNqiMlWZhnacZwaUs
+  32i3DK5LwuGkXgZ7H6VB5T3Jji83ZuIwSevHU1 qOYIwY9rI4BIDH7hIHJ9h/WvLbs/M8rSMny
+  u79CnbQw3CwbW2ybD9/kA+mPWpt9qrojq4DIZDhsH J4FV4TCb4EMZMHcGXjtwKfI+6RGCjzNm
+  WyM4A7USu3qQ8M3O8l+NipaBJJYi+XdI2JBOeQDz9Kbc CCaCJF3uJ4TchlOAAABge2avwm2ba
+  pV03LyxPXNUL0R201vFFHJEOdjPyAgH8s1alep5ik4Kr1uZ MMNs7uQdkisNvPG053Gsm+2iJl
+  FjcSSkB7eRWGAg6j61o3UN1FDHPbzwgxPHHuZMg7vX6Zrnr2S5 knMRt7hzbuYo5I3wFHUqfUm
+  vVw8OaV0z1aVPmqJ328/+G/MezQeZcl5hHvl2xqxwWyAA34VKY4Ss gSSaeRkIWNZOZFAxn3Oe
+  c1VeaRlt5tsUMMhxh1yVBGAM+oq/9hhiiWKKC5M7hN0m8fIwPK/1P1rp k+VK7N6mJcY+9Udvl
+  /wH912YTxQp5dsjyRfu8yLI2SzAcEe3t71bt7jVPskKRRrMWPmSAL82BzV5 NPsY7tzFvlAZzE
+  5bOAex9cnpTo7M3F48ipM67P3hjbblm9PbtVyrQa1/E7KTjVpvW6Wuv9Ir3N5e nT55HcRbrg7
+  WxjAwOntUomhvrJ3MLN8xMu3jL8BcelTNbuLefbNCJBMPvrkNxyAPyq6ZIksWa4tZ Cjy4Pl4X
+  aeOPqOKxcopKyHQdKnytxW/R2/D/AIJSVL0pPH50UUyFTKoTlcct+XeiXYtpcNBPDeP9 oBzCu
+  0le34A1ckslkW8uFuCG83ZKcn5sjjHpVBfszaYvlH7ZuTa6wHaykEYpKSlqvyN6kaU7TSe+ 1v
+  zf/BMp7l2ZCQqqI5I5BjqScZHsDT7UyyaUImjMilt7FcAkL8tWWaKW/eNDDEFQhXddyk5BNaIk
+  Z55YkeJEO5lkC8bSMhfrXROVopWOuvg5WjHlt1/4bb82ZVnbWkNva25lmKMkjuWk+6SeM1cY2t
+  vJ bhZYXuVRvlIzjjj+tWo44IUiZ9sJWHGJOcfWnC4tLnTZVKpcZUHMQwxAOKwlNt31aOL6s9I
+  3fL/X l+phxahJmOQRoFVc4C/eOeSPapkurZp43lFs5ZWCgRj92PQ/U9KW9dxO0Nv5SBZEEa4z
+  letTXTXD q5MMIlEu8ARhcjv+VbtRdtLXOmWEp3Vla/oVbeKK3vspI12PIcuqNyNo4qHLJZW2Z
+  o5IjGu4Hkrk jI6dauXCEkTQARylW3EdtoHB+tWYy7i2nmmtWUl96rFgYI6/hQ6ltdxTxnsVzL
+  3umu+nbQozFRP5 CwSiMtu3bzztH3enc+9RLcyQPFdXGxp5l3sgGAOcDA7Ag4+tTCVmW9uIyAk
+  ZEa7udw/vCoDBMzQz KyS7omH1PqB6CrSj9o1UKUvebsn66mo95b3GnNFsks13lSJD9zJ4U+9U
+  Li2eOxMyieLy28pizZJG ck1Tfz3t4g0DhRuG89HJHWlW3mYuIZfN+UERHJyD+nFEKShsycLQk
+  knGdopu63X3/wCZZ85/s58p WKvIuQOoyeB+VV5rKb7YZXhkjZZywRuScEZzV1rfzLGdVtp7aS
+  OT+Ju4HyilslmUu80wxIoMu/kr Ic7R/wDWpKpZNomcpcjkpK35kN4Ynu3+zxqdoKy8fKWzkAD
+  sKqo0RDeZvtJScE5OBxnGBWtcRySF EaAzziEtOIgV3MtZEu0DYCIzNiY7xnHHNVSacbGkJNwU
+  NYrq9391/wA0WYXuHhiijMcjIoA3LkZJ ycjvVhLWV7QR+Yq3CFwXPTg96o/ZX3maDe8Xlneqn
+  5iD0IqOWOSG2th5joDAXkVmJJyfvfShxTfu s8+vh0q0ZU/h9F+P/BQyWKKDy4/OWSeYbSpwQT
+  jqOOlUXiuIxEzxtDuGQHGenWtT7Cjo0rFmiDKE cH7q5xj61WvYTFfqJGJXy3CIWySO9dFOor2
+  vc2dSU1yOrdLo/wBGU5FMke7yEy2Wyi4BPXjj0q2b HzLT7XGXSFkZgm77uOn41LBaXEcW1yLh
+  iFK+WMcdhVwQNLa2xt0ljlEJhCO2Qx3HPH0zUzrWtZnN KrGEocsklfW7v99zDmgkRIudkkij5
+  Ccscen4VLHHJjO8SERs8Z6/JnB/OrVxZRqrRs5meINgjPX/ APVTrG2tklijIfa0JIJbqcHd+G
+  KuVVclzrrynGl7WesfSxBCyy2DkoyoB+5Qnk5P6mtQWUV1b2TW rneEIcFsjcTwf51Xdke1DR7
+  PLVAFA6/L1Oakyy20zRo1u0rBuT0GPvD0ArCbb1WhhONSdOEovlV9 P+CiFI5YrxYyzyuoDqgY
+  /dU85+tNMspke8iZZirmJQBlSCOuParUVs0aoySiNVh2O7jOT1BB7Crm l2s728s1zA0ZNwGQb
+  cZC5zUTqxinJkYiqlK8mn36X/Az0s5fPgYSJJCIXjOOrEdDTjAJtJjledky N7ZY/K68Bf8AGr
+  Lia/ZER08txuEqDCqc/d+vGKz47e7lmkS4heIOCyjphicD9KFJvVuzRwztOfvS St+RkYJExI+
+  8CWAOPmHQ/Spr0rLpY8weY7Yw68BwMfN9B0q08KxQxpG6MxXLA88/wj8cGql9HOsc DiLYjuzs
+  uOIyB8qn9eK7FNSkjoxOIpzUPXt/wCO2kaC4fy/LZgjbWIyGz/Fisvyyk8RdigjHQjBb vitq3
+  eb7RGSkNvmNkMjr8vzdO3aqlzHi6toHcOIoz5jr0c47foK1hK0noYctJtpU7d3e7/y+5me2 Gd
+  S2A75YA+vYVMd0YlKxsZAWBJ5wMDI+oqJ4gm0eW8cQbcN/LD2zjnipXWUopiZtpGVXGSFzk545
+  rdu9ipSc2o29F3II7hpNNWRVUMoCEEc4Jxk1ahGx3DvCPKk2hSuc+/4VTZ7hRbCEoEU5YLH97B
+  z6 e9PRJGkcDLbe/qDzQ46M58PSqzvSl8tv8jQNw22NBH5MRjZ2XHJyMf8A16FuLeK1iAlaVlK
+  sNxO1 yO/sMcYrMaHyzCil2HIXqTgj+vNLBFLhEdssdwVyhC9Pu8jrUOlCxDp007S0t0v/AMM/
+  uRpxXUUc MhiURL9xR1Lbjy34VD+5O6CCPyZTIIyWOQxPcflVR40WLdMjxSGQbEJxlRye3rUQz
+  O0g2NGjOZWy cnIHBz6UKkt1/X+ZhUoJtSp38u33atl/UY2F3taC4G1Ww27jkDBNNhndrUySRR
+  25B2EPHk4POfxq oEu2AnLGVGiJEnOMHqPrmrMMUd0lsJJGjRQURievPQ+9DSUEn0OWrKmoJz+
+  9f8E6UxXE3l7bedoo Ytow2GLlsg/QVNCJtt4tzOvmtdRsXAwAuPm/Cp/PaZYVEMqp5Z3OrYG4
+  N7e1XJbaJrFgQxdSowDz IMckewrx5VLaNf1c2q16Sdp6Lytp+hmtBFc3F7AoMkTzMysOvAGBQ
+  v2iK1Vtiyl90nAwV2/0NF3b CJZJRcK1uJsEJkEKRj9etMtEktLW4lcPOgk8uJgeCh/qau946P
+  8Ar+rFqKqJO910TWj+b2JPtZgW 3lLqsc0bSlWXO8njK+gHpStcQtbyOQFdJApI4LKAAW+maGK
+  TRM4TyYgrIyyc43dh6VWNjcCKOYwT LE6AYJ+/2BH9aIqHXRnRTjh1G7XK3+I9s3F55iSI0RBw
+  gHQHj9DSQx3dtYBQ2wAgNuGTG2eAT7jm pWiVdNVYg29JgiAHkqTwPc5zzWlFKsOoSQzxO/mMz
+  uc/xDp+VTKpZaL+kEsReKainbo7GMtpIyTm R0PlyKvTGQOQR7VFciS4lYW0BSNW3NK/ONw4Br
+  d+zQX7xzoZCZox8itjBIwB+VVFO3S9szxrBEBG iDhmBOOT3Poaca93fqbOvGpO6d5Lps16W3+
+  ZV+yeZJEv2CdyYyqMrAAjPBPr606KL7NfSzStl4Hz IRwCcenv2rWtdPEVn/rzAys+0OxO0AYG
+  fpnmsxnKpJi6t7oIEEjKuQzE4FKNXnuk9PmTShHERlfV duv46Ed1IL21jMKzRSlfkQv91Sec+
+  uAKvxQwCNwjklgWiycliThWB9B3FV5UmRprdpIpFJLkIuGX aMbc++aUxI3kq04Bh3KQDjaxxg
+  H8KUrcqSehM6UYQjGKsr/1qPkVlsV88xvIG+RwvyFc4JA/l6Vn K0kdxLJHcW0zF8RnZn5AOv5
+  fyqNvtMbIRHI8Rk+6ecqOv6mtC7aJrSKWKMwSJLznkMuMHj9K0UeX Te50wpSppQjeSl9xHLbk
+  WqZmMhAOHh+UMBgk/jV6CeXUbeaO2uIQN4ctKmQSOoGc4Bx0qOwDzqkM 3yQkboF7gLk7Se5NJ
+  FaOz27xK8McsGWI6Bycjp9Kyk1qpbo1pzpRTjUa5lt/ka0dzcW+l3Ml2wLz TiTKrgRZxiP6mt
+  VbyZYonkEKeXmNFKcjJ4B+tYqW7pbK0sivMXHynoDnOCPUVqPb5uLpnmVZPMCz AjgucEMPQAV
+  w1YwbIlhFOC1vr2NNHZoFa5ilTZlTtON7Zzx+FTvcNJ5hjOzfIo3OM/Ke/wCFMAkT Jdh94EKe
+  oPTH1q1LHILci6XY7ZI2jHAIHFec2rnAk1Llf4f5Nk8sFjPaMWYjDARbGI3ds/nUcUip qEIR1
+  /1LLIWGdrZ6H8KUbftJhgjM0ZZCSO2OlRSyY1DzY0CiQFkz/wAtE6Eisopv3SIPSVOW3mST Qp
+  NIB5ylSMDbkHJ+6ao6pc2lvpUYnmywATGeQ+4YFMlu7qJbiDMWxv3ocJjaF4xXOajOGtZTc28z
+  FZVWPBweO/vg11YfDuU1d6BDCSUlzPT+vQdPcSyambWK4hDh2ZlK+nP6npWXqbCNEkAkjuJYsl
+  85 UKfvZH973plyrTtJFbRskrMm9mPPXrn071HsL3jQteW7qp8v5kJ3qf4h7V69Kmo2fY9CjRV
+  NuprZ bq2vrdfqadnHC1pbOZBcBg2xR3Cjj8upqKNorS682ZLqOB5QzSOxI3c4/A1lCO8SaOKF
+  gsRxJESP 4AeT9McGtC7R73KJBLNC77wUOMeg/Kh0/e1ejMYYHnquMp3Uvlp+RaE4lkjlALIpO
+  FXghugz9OtM mj8qAyMJhsmVXkSTCuCOmPfPFMjt5Q0TXFrPFFP2VsF2z1B7DpWhb2dsk0xlt7
+  gMAymKR+hH9fSo coQPXpLD0tnt26/NP9Bkd3FcXC/ZoHGwFULHIx7+p4PNVo3luY5gbG5a0mv
+  FcuDjbt64NXVhSyWW 4t4mcB1Aj79OT+RNOmeWOCOa3jeG1JLxs3IKkYH61Cav7q+/7zsjGLl7
+  iTvs3vf8wNvEJsySPty2 3BwHXru/A1nTW1ta2VvLHvjZ0LNh+M9h9TWxG9vskSW4jJU5KnqpC
+  5C/iahYWz2StOrRuQSu88Ed ScexpQnJNbipynGcd7X/AK8yOGFpIbQ23kRz+UUdXQEq56Z49K
+  rPZyQSSz3cbsiKqkRnG5/4fwqe SK6WxthCyuoIMkyDhH9D9at/vCtyslrPKfNPmANx04FPnce
+  v9XNqdWcNJWcX9/36Ff7d/aMqI6xx Ha4nDJnLHjj0HFNWzhs7a3fyZplERiIjfGSx6/hmpbZL
+  f7GrGNuSiZXgknIzVhp0s9UcypIY1RlT PIYAcmocrPlgtOxhKUVJxpJ+hlXFjaNZNEs2y4jkA
+  XLHI+v1pk1sJGH2i1u3kTduCuBuyOo9s1YX zJ0haaCUwsyGIjALjPJz6dKgiW4bVnZSY1eQzO
+  JDncBwAPTJNbRlJLfYHOUYtb8uu/4aFk6cIo/k LQsQQpc5BzjNZ0rRkqr4uWCEEQnbgnp+gpz
+  zyxXxDzeQ0S+WEk5yxOc/hVW6uSZZJfL2TedtMqj5 VOOBj3rSnCd9XcIKUXy1Hfqv+C9x0890
+  LIOFjhSVWcIUGSOBTIZQIWnSVZOARCv3s+o9sVOJbkWp a4jWCWP+KRMhezKR61ltcWjxOkI3l
+  2+RhxtQdsetbQjeNrfcOKiocj0T6rVP+vvNW0uGOXcZw+UX HDKf4h7VSniura/iMMqyuh2r5Q
+  655GRiq0RlvrjMeLeJAUXPYYxj61K5uI7a3jvE3iL/AFbr8u4A 8/0pqHLLp6GdKmqc7Rad+mn
+  6lqS5eCIqm9Z/OIfec5J5B/KofNEeoQXdxKHBRjtUYBJ4H4gc1CJR 9rldv9YV83cem4f05qae
+  9lURxrai5TecPtBCjjFPktolua4mCp+4o6Pf/h+xctLi485jHukVv9WB 1I6daoPGkljcmV1W4
+  dlZFIOVA/xqSGWyaISeY0MiuqMNx46+nvUM4uCqS7oJnRwsgROq/wD1+lKC tPsZ0ptVG7NL5K
+  /zehfhaWOAXqlXckiRVHHIwuB29asQWt6tslwjQzPFGsWGTPzMT+lQTLd3d0Y1 hMMsbkwxqMf
+  u9vcdyPWtQXUwt7dIEUJIhlcgc5BAX8u9c1SUkla2p59R1U+aMVq9U7PT/IhW2P2O WCS2mLx3
+  CK2DjJHJ/DNNkFzJDPdYt4WSRY0MkYPyH7xq/JBcurJ5ojmy2+T+FiMEn8R09KrKUl32 7N5kY
+  K4wedzdF+tYxm3qRGnOMG1bzVtv0Bbq5jKIYEmiThVjQBip5Xn1qvIkLWUN1tktVwNqueRz jd
+  +BPNaW4HV5tP8ALbfEjh8dcheCKzLiS7fQ1iUwlJY1kjhKfvCF4PPoDzRBaq2hn9Wipxjbl792
+  n96+9L1Mq5EVru8yZLptjJhDjuM/jzVKRo2ztjltyIyAWbPTA7CtWYJNLa+SVknjhbY2PlZj7d
+  yR mqK2zz3rIwChFIOD3xx+dejSmrXZ6mBr0uWUZy+Hpf8ANbFhPLngi+0yI7mItiJduARjB9y
+  cVHm0 t9Mtfs63DTq3lyo752ueRx6Y7U6GMtEv2sqilMxKowSAc/0qKXbPMbyaQQeajOIuc5PG
+  cAfjSS1t fQ5Hg4ydoS07a6/gX7v99by3ShirLkBTgY9R+VRxXBh8u6inJEh2APyEyfun3NZst
+  5NFDH5U6yDf ukbb8o4+7j3qAyXLmIACNkBYqVwFx1z7jrTjQbjZ7GijWlScJfj1Ld5DLNOjQw
+  zR4J3Kp/j9MD0H Wm7oRAkrPNFPGpHzvkE9j9M1JYSlQ1yrgySEsqHnPBGfw71EsKC6j8qaNZl
+  gK7XUkEEdfQ1Sdvdf Q56dSq4OLWi8nr5PfQqWgh8xEnma4d4z5jJwEY5wOR1prsoji8xAHKAY
+  6FCTwD71opFFZwRtJLEW GSQqcleq/wD66wiEvLyWSeQRsvzI3bHpj16Yrem+aTfQywmIxKlKS
+  b5f72qfyJyRb24QRPK8bkTf NkcjPSq2+MW0iKj+VIysjM2T8vUZ/GhomEy7ZlmcHJRM5bA69O
+  g71JCY2aS3kPEkm9SOMe341tZJ XM1FRnOa+etvwK89074aJkFukwBjYbmORyM055fMkSOVUIi
+  JBKttBU9v0qwqqmpxFoluJGjO9FAG DzyffHNRm1EUfnTwyiAnCKGwX4659BxmnzQ0Oe2G5k38
+  kna/4lKJRNI5iljK5wWJ4XP8PTrTBFho ZGE7gMcBWwpI7HjmtRYZora4jUW48pwrIke0u7dDU
+  OEtmiWVHklti0b7ThXB6kD15xVe17GUqtSS tKN/JP8AXT9SsYrhhHEyPnaNmFx8uc5+nvUQjZ
+  ZJHaKRDuyqsfu5NaE92ViEETboADs7sAeSCfao Y4ITbs84mCK642MAWHeqU3a7R0bU+aUfkv6
+  RQnVLvUINs3kRQlgTIxPHpx3rQEMDoLhJVMCtkr3y ein+dVldBcylFDJvLAMoOSO/0pI5YpXi
+  neKRlX/WbDtDHJ5pyUraHD7OpG9uutl/Wn3sluJIhZok Uu/BBTbwozyRj606NHmvpTHZy7PMC
+  yIpGUB7fWnWEjW2n3Ea+RLiUOPMjDYAppdUu47i78xi+XUx nYGJ7cdajVXS/r8jKNOpGjJJW+
+  e/l/wzOus7MJp0ixeZIxw2N2c7W5I+g6094SmomeKO5Z5cyQLv yNgGCMfjUtlfMdyOm0ySAEg
+  YwRk4q5JeSyzK3lqfMR3aNBgxkAjb7fhXiTnUU3dBPFTVZwcLrvff 8TMgVQkRW3uJo3UEgtkf
+  72D2FLDFcG6SVpo48IymNh95ugOOmMc1FC1wLHTGETRNFblgG5yc4H4Y qUGVbUywXEE+2WMBA
+  uTjNaSvqd8ZVG37y107/wDDF17aRNPYD5jHLHGXPRuOWqjZ4F6Uk86YEAjD /Ko54+uOaciP/w
+  AJOss8jtAyOxKkhVODwaWFbC1tLAQ+chlx5ckj5HOQT74zU7Raet/6/Q5atWUF yuV79uhKkd3
+  DY4DoEaPzPNZcjjgfzojSN5Lee6V43+6zbsAuQRinmS2geO3aVihVgYyclcj/AB5q m9kiWFqs
+  cdwU8wOCzk5H8X45NJa76XNIu6UZOz7liwSYR+T8wuQVBUdQQcEfXFab2rpGXkEca4yy uuSpz
+  gfzzVd5UmuoYoYpPtETFpCD1Yj730xmoJrqYwRea4QIyhdw6oTzn1PpWUlOUr7FShN1NF+r Jp
+  p3j84SsjvGGWRlGAC2B09+tZkkVrEGRZ42iwpi29SByM+vNaBhvjqaNGiz2sm4ZC5x2GfpUaW8
+  1u4t5/JjWKMIJ2ThiOeK0g4xWjO2lRp0k0nr6r8inOQs0ki3CCTC+aSONwPI/GrD31o09y80DO
+  BI AXj4AJGQD78Yqd7WO5lXzHQs0ZZ9gxtIOdp96fmHy41Elqm8MQGT7xxwf8KfNFpXX9f0jpl
+  UozUV JO/zX4lL7SbnSwY1VZjggY+6CckH3GM07yppEl/d+aZW8yNsfKB3OPSpbO4MUojiaDbI
+  A6ErnAHH 8+Knm1HyVuEmAkmjuFAEY24GOR+FD5lK0UQo1qErU43e+u5Xgto7j7T5k4VlZY9y8
+  cdTj69KsJdP taG2lWN3kEr71z5RXov4+1QEW7SuIHKTbTkseGwMjj3qSWayfSYEMiJclFM+OC
+  xJz+lTK8nrr+hb nKrK87y+W3quxNb+fPPubduddxQ9UI/qauJJILyN5FcxFOWz39D74qOGIqZ
+  5/MQr5pVUxyMjufQC rd1ZgWg3b9kUgSMqeCMZJNc85x5rEPGUvhlK99Lf5GiRJFEHO8RxsIyz
+  HPLEc/hT5biYWz+a+6Np C59QAfX0rBgRZFZVeUxqwBLOcepH196vXepNDYZihKo8wLhgCYicf
+  Kf51zOi+ZLc5IR1Vtdf61LI uWEe8kyRvESfLOCrA8DPvQ1zcvaxSJJHFLsZY2dcqysRkgemeK
+  r3NwEucMyy797L5Y2jAwMfnyKy p75lSBDGxZVXcB/snk+w9qqnR5rNI6KaVVXiti3dXl9BaTe
+  W8IAk2zl484YjhR6Vg3OqXQCRsUUR 7UbcuSSfm/pinRxFtWcPDdSRyKzyrv754qrezRwxsHgb
+  Nwu/LH7pz0r0aNGCaVrs74UKDajKF330 X3EjMY2vLmOZJy7bmRDyoPb8BVZd8k1zHGYSyzHyW
+  VeXXHHNPvmlNkPKeApLlnVV5LDAxn61Xtp5 I7+2id41jcHeuMEBff1rphH3L9f8hKpTnTc73c
+  f62tr+BMHYWsAuG3ThTDGqccnn9D1roEuZEt5g GR8yIr4XuOCR6CuVjn23IaaJg2QSM8hs8H8
+  jWw7izM0VtOl0FOAByQf4s5/Gs69O7SK9hUbUJNWe 2/X7zciYT3TW85aOBJcqSeSo6Ee2aVWt
+  767ZEaWUTK8uxW+Yds5/DNY0d3NJbiLz4IkMTOSV5+U8 Vr2N5i1imgt1ZlQwhlAGd3f8O9cVS
+  lKGqNlhPYvnvbsr9e+xJa3VqscEUayxlIvLZ5GyMnpn9aYt y0dqq3iCZVUoNgwGz90j6d6zry
+  3lS8cqymAMhyv8ZQYGPqTVqG+hEszzrtd3aSNW/hA6j8T0pulG 3NHU9OGHp814av1Kaox0mRn
+  iaB0b5pHOQCOin3NaOnqbiPZclYFQSZDDJOB29BSkrd2cdoqlWkTe M9VwclT6sexq7a2lrKsm
+  Le5Sdd3yGT+A8mlUqrld9wdSMYrm3T+75sq2e7YjGOS7gG1EihOC5x97 PtSzRCKSVkjui7Ocn
+  zPvYxVcLcWgVYklKgBh/vZzU8duLi7tnuZJYTKhlPzkdKUtG5X0DESnTi56 Wfa/5Ji2V3BPMI
+  Zyo3nzC6cKOwH48UOIJLZxIXgUPuxM2SATg/rTJI445MleH2EsvHQ/4moL82ro jSvJI6kRNEh
+  wST3pKMXK6urnH7OCkpRbjf8Ar+rlt4cBjch4LaIeWjbsD5iMkfTp+NZv2WRvOXz/ ACssuS2f
+  kAP3fqeKsPfLaXEttK4CKxYGQ7s4GSP5VHDBazQGdpn8zeCYtxyR3P4VcXKCuzGvOpQe qtf7v
+  +H730KitZSah5kqTTu7F9obG054X3PFVpjE07QpulM8hdwvTPYj2qW5igi1jz/NCwgnao6q D6
+  +vNZ7Yhu9rSiMb0eRWHzAjrz2rrgk9U3sdNS2s03ey8/yBp7pdyPE5hYNnfyckcc/WqSw7ba0d
+  WQlCBIoGCAT09yTWhPHKYJ3U+egnJUqOpPU/SqmJkZQmwNghht6HHGfc9q3g01oRS5J0vaXs/L
+  b5 ixTyQFY0yrSMRhgMBwOn5U15JXaLfDNIWwqEdMHqf0oFpN5ClsfvVEir1OORmpGuStpbRtE
+  2Y4yu fY1Wl7rVlTV7ONm32Vv1uS3VwpupYo4xGjlipI/h45z6cUttJI9kZ3eNwrrGCBwPUH3x
+  UMZZYoXh ULGCysZOS/HQelSxvCmnMu4LKrKGP8Jbk5xUOK5bJGVanOpRUUrR+/8AHUZLCU1MO
+  kTGMneExkbj 0rQF8ZtqzoE2D96AuPmBqratb7fnMpCsEDbuOeoq99h8lk8mRG2IRISMh/8Aa/
+  Cs6jW0txLbklHX +uupYslukl8zaSjLvZmPKkZyP1rQkgMttcJgTbXC7Ivlbpk49qpOk76SjzT
+  o1tlVAQYOWPr+ArQl jdLuY4csx+cqcAHHI+vSuGb9699TKVL3lNNL0/pMtfYJobBLhJ1DFirh
+  8nlsc/QiqzWrjUvK3Kxj VsKg5bA6/hTysUVpBteRnO6VAXJAjyAQfU56HtSCG/uppoXUpIJGU
+  soxhSMkfXgVzxbV22cULwTb ehVMV5aTwlXCMIWR55BnzM85/XFUJIrtQC0TBYSpAPUKOo/Dqf
+  WtcXCo8AkuEEJXEYfnIJ6/nUrg LctJI32gq58x1OFZivAA9/6VqqjT1RFWFnfkun/X9bnOIsc
+  tvJE48uVbhRFKpwoUjJbHoT0rQnlk jlgVogMRMLiRVADknt6YqSWXyNLjhuERhtJfYuCrZG3P
+  tVCETSaw0jW8qouYrhmOVVz0I9Oa2Xva vZXKo4eLhzVFbsuv4W/UzJIXFhiKGeMpIoRpG3bQP
+  vmoY/PjVwqLcxSAyoSmc+4z/CO4roGit/IX zEuA7OFdA+MsOCP8aqXPlKoVIpo4gQjvu4zt4U
+  enp9a6IV76WNKdWHa/9etzHlillZVkaGKNxmRg mBkjOPqccUSpcXNnKRKmPNC4C8gEev4Vdku
+  VuJYoUs5mEcWwLu5B9/cUW81tbARTho9y7gCfvAHj 8a255Jba/IVSLuuZO/bRmR9knc/ZiyyS
+  JOdskXyqNw+7+OOPxq3mYSgQtA1ycsqrH83pz7HrioZ5 Yo5YZYSWjdd2QTkEHjPvV2CVGigaQ
+  iHzLd3DDrkdOfQf1rSpKXKm0YYmPLG3R9H363Kd40kl0luB 5rIrAbR/rF4JH4VSWCVr+OK2KK
+  fKI+cZ9+aleORbRriWbaBKFGTyCw5/Co5TJDcsWzsiOzKnB3AZ 61rBWVkJq9JxhZeWv6/8Agk
+  tLqIK5xbrkKSy53H0zUohQxmGIlZJGeTy2OSMdFz7c1CgmMqmOQ3B CFQoBbGeSPrzTrbZEVkl
+  Ei8mSJMndx0z7Hn64rR3tuYqNorRc62t19VroSuVjsxM7h53OH2nAIYc EenSpnmigEEkEwbYC
+  HVxuCk9OPfmkuUMaedG8UsLhWKMOwPaq5uHfUd8USH5SChQEE54P4ZqEuZX I9i3T5pLRd3+A2
+  6+zo8ioZZxLhwVboF9c/jU8zW0ljcS27FZzeY8p/mYll/lSxp5qzJPPBGkaurD HzBvwHY4rPF
+  qJJYXWVERoy5kOcHtkfjVJJ7vY568Iyak29Oiu0/68iK42SSQQRskrSMDEE4YDGCD U8Zht7i4
+  dYpCY5Qq7myNuDuOP1p0Fi/2xZJIzOIQXPl8EMDwM/rUEjxXOJkV4mE4WQZ4cv3Hp0rW 6fu9D
+  n9qr8s/h8tH+hoW8yNZQIixAxzb5cr97j+XqKylkS2kRoVDJjnI3BOTwQR19Kmc2YnIBkiQ Ag
+  nOdxzgEVItudnkNJD5O8sHxznPTNTFRje/UyhGjScoq+u9+3luQQSzTzRTsUZI8/ukGGc9PSod
+  86XCB1TeBu+5yKsvCrahKWmhgbdljzznkkADtVXyUklt40kMjSDaoxyK1TidTlTo8yUtGtdHb/
+  I7 KZI547YWjkwqMSuD1Zj8vP6VqssEZkbf/pEM8aICezDBB96rGVjbt5NuU3uHRRjlFIJ/HOa
+  v3M8F xePKAoUPuIA+9/tfTH614NSUnZdP+G/4YyVGTgktUZDy3lvEyBoikbrsi25dlB55rUik
+  jCTT3Dww xGQqMLjBPOPqOKcl/G92y5g2RnahKjIVuv1qaeJCdiFCigRxjH3+cg/XrzWc530kr
+  ET1fK4cluv9 aL7jlLm5S7uYYhFNbzSFjIxfgfQf561pWdur2ygyK3l7vLQ9Rg5/+vVtklmgdU
+  SF/MPmsVj5DA4V fx9KuyRoLxpruJg4Zjsj+UISANp9zW1SuuVRWh11Z3UYr7r3f4laOeFGKFo
+  /PJSR3dcgk5zj2Aqv K6vEXjkB/csS+flBBw2BRbxo8Kuq/vQ6DLc9G6Vca1Z5JVCMJHVpI48d
+  FH3gazvGMgoctOreKt5M SGOCCB5oFmuX34wrcnjrVULMQoEDrGQrOz8jr0/GrouEWJhCY7cyg
+  bUc5JOflYe3WoXt1huZpJbp ZgCA0anbkjuPbmlGTTd9zspTVHmc939/+RLHte4QhbgpbP5ZVZ
+  MHJ+YD8qkd3muZmnkiWFrhDFkf e4zx9TxVa2nsYplZ5Tl0aSbLcb/uj9KfLfu11JHB5aiJ9ql
+  lztGOfxFS4S5tEZulJ81S11br/wAE ZPO0VmXWe2WSb94yleVx/CffmpUngkgkhupreLbIGzsw
+  QF6dOxFQu8UsqxMEMbByCR6Y2/map3Uq q15mMTM+C0gHy5A4wPQGrhTUtOp00cPTqxUZNprtb
+  82NvD9rmYwosaTFpom6AAYG3j35qaxsgbjy bpjJ8xbcDyMDkH1NVILtUjt51uIXlj6psyD13c
+  e/9Kn8u8igF7HIGjbaJABz+H1zW8lJR5b2NJTn y8rdl0et2/X/AIIs80iG1Ft5bM0Ls5xnDA/
+  4VDEWkjjnZUe6Y4UqoAC5yVI7nHOatXRaVGgkUK0b 4XYuNw7j8arXFvKliHmuYgQp8tQuCyn7
+  x/DilBqyT3MaTb5eeyu/X77FporWZlkM8kaTuHjw5G0e /v71Y811uZUguhNK0mDzkDHbB71QV
+  7a3sxAtzFlMpIWyfnOMMPRcdqLS2E2ni4hV5JM+XuRuGG77 w+gqXHS72N/rLUZKUrJ6K60/H/
+  M6hZokuZ5GBjbzAjg9M9z7cVkzTTG4bfE00bzqGCfwtjgH3Ip1 iuHZ5SfJeQsrOcjJyqt7ioD
+  BcQIJGuEH2dgrErkEjJJ+uDXNCEYyauYYeb55LnUn/W3/AA5o+RaR MbgmVEyFBd87M8BT7nrW
+  bOBb6nKxbdDHEYyzfxluSRWtJJbixiIuIn8yBiFxn8frXP8AmWsixRMZ ZJJXIR1fhlH17mqoX
+  d27mlGvDmcpN9n/AMC9i1a3cK3ELtdwmMpuYY5JGdoz7is+/cSyF7lHDK6t C2OMY5U+tS3Rs4
+  7SWRFEOZo2jBHVfT9KT7R5UjSXEBmiJdkQf8sznO1vXiuiEUpcyQoxXtPacvNf rez+RWtRFKy
+  DbJ++yevTaOR9TSSSZ1CzxbEMSXKHG4Dvz+tEaXDndbRkRtC24d1YZ49s1C08Spa2 13L5bm3K
+  tJtJ2nsDjn0re15aGtShGad3p+RJC1xNdD7MhdlyGOAck529R37UguDKVdnCSrDhjjGS D396l
+  ikf7T/elUMrRRjaQ2MZpUS0hJN8rJlVAUHG3jofc9Qad0m9DuwmJ5INJbeWvz20FgvYjEkU hS
+  N9gUOVz17VpSanvtTGhWAqVUKV/M/gapR3EEeoRLPaFoSCxAwGjYdMn24p0NvE8p3zwymZfMYI
+  MMp7jPbNZSUL3aO2lXjGV5wemqe5pRW0kgtgk3mykM5wTjAHJ+n9akspLeJgJ7WWQzIph5GfLz
+  zn 1571DCzxLFGiMo3GMHP+rycgH602YyQ3JuJmyrZxt4AxjIHpXO05XTM4znKMrt67Lr+hqwN
+  ZHUyq szNIXkLhuBt6fQZq2Gu4YYrqcrmTKoFGPkP3vxrLfybkrboCrAFlKnoO4P4c04vdy5Mi
+  P9nCnafQ AYx9SK55U0/+CH7ru0/Pf5O5dMd1O8ciypDGqeXECueD3P0x1oiZIJJTOTGXIKSOM
+  hF6EY96qRSx iHbIk8ULKJYSX7AY5+hpbxvJtt3nJcs0m/avB4ABH454qeVt8pz1qslU5H17b/
+  ft95JK0FvctdLI s9s4+RAeSBwKquwhQoDG8sC53EZ3Y5NV7ET/AG2KBIzHald8fm/NgLk5zU8
+  TyOQ0jxtMclDt4ZM/ NxW3Jyuz1/r+rluEVU/eavvf9CvLdQ3YcztDHayMGj3J8z47g+gI5qGF
+  trSMkiyF2ZnYDAVgPu49 6Y9xJdwLGYY0hd8CQKMIP4R7dPxouFke1eNXTz0ONsa4IBHIPqfet
+  1FJcux0xUJRUUku/axXnuVk ESKhMrruK4BPrSpCZ7NwGjSWOVVYyjlmJ4P0x2qaJg9xEqWssj
+  jJj56L/Hn1xUweOGRMGOFGVvsz PyCv94+p96qUuXRI86vWqQl7NW022f8AmV5oJFkaSSaPD58
+  xEJGJM8L0709IEYebHHI0k0pkMW/5 htPAqqxLQTfMzNDKkYPbB6k+p96jYXsWqF7csF8tgoIz
+  jPT86qzatcVSM6q3Sf3J/cWb5MRSbZMS +bhuf4c549BVSR7a51aJVyokzI5J4G0fdqeNp3soy
+  0ZaeBypXH3R2VvUnmqPkSAqpw87/vQiA5AH UdKunGys2PD0Zyp8t7W8/wCtPUlS4ZbpVaEMjp
+  yhAwM9D+FT3FrEqiFGEczOCzNyvTAH49qjsY4L a/cSrLHIM7GdshB3BHer32d5PJjUiVUdR5i
+  nqxBK0VJ8stNDpxVepGom042+4SzUEm3l8s+YwcAD mPYOAfU1ftIzdXjLBHKWUKuc8EfeIx6n
+  FNgWRLyJHjUGFCPNxw4P8X4Go7gSxW8zo5BadTGyErx0 I+tc0nzNruFJybdOT1e39I6BIlJub
+  tQCDL8kJ5wCMdOmRUtzMgmUM6nETngYOeBz+FZCSx/aLcNP 5bksdjE9M4zVzcxtmKOk6LMFDq
+  OoJ5FcLhZ3f9dDmp3Tbldtd9B0U88jLiNFcIyoWXIZepNMaG4Y ySxziNhGq7T/AA56598UrCK
+  e9LDfDJggLu6gHJxU8ySeT58Q8yNiHVR/CvRgfU0XszWUnG/Ilr36 /mZdvZOuoxfb9vlvE7Dj
+  AXb/AI8VoiH91lg7tsbeUOB5mMgflVSZ786nEsSh45EaRl25MfGAtNju Jfs0iwQT+e7jYpbqC
+  OW/LNXNTlZtnJX9p7Vc7V/69BkEjfabXeyBWtyGjcZJJycn6VWNsIz+6Z4A sTKpkbIcdd9WFi
+  e3817WGSdZHBU5zs2jhST65NUDai5kYsLi2JkJIZ84X0raNrt30/roazVOMuZS Vvl+RWllRZP
+  OkjuLkH5nMb4CNtxj696ogyxtCyyrMY1I8vB+b+6/5n9KdsiiDAiQqSXjJfjaOCp9 +nNNWVUn
+  82AEgoIkHXO/rz7Gu+KSR0XpuDcv+A/68rFzy7i6W33lfMRTE5jG3eW/i/DGKjaOOMGT 5WkaN
+  izMMgMo4WqlwsNo+ySdhKn7r5SfvDp+eagtUje1it0huXKE+YQ4O4nPIz09KFD3b30PPhQc bS
+  T930/zewNfK0tsrpDbyCArHlOCfTGOtNAmNrHvVtm5ZFZRgKBwQfr1qOaAbkIUIxUuC2DjA4H6
+  YqRbe9aw3q5YqygAfwg84Pvn9K6LQS0NsRyxaVl6/wBXGxXckKF2jMtqhyAQDuJOFP0qnPbM08
+  qN unkSYybl6Px6fWti4lmuIJYvLWRjJgbFABI5LD2GKxpwGkLhZPNYA/KegPXNOi7u9rM4IYe
+  lNup9 rtr/AMALW48m+juSpbKFnRRjk8U27lE01tIytEFQg543sT2pk8KJeNHFL+7VtqM2Tkev
+  SogigQ5Y HzBlVzynOOa3UY3Uh1qVJT5ndPvqa1kpTVP9JglEP3QT90D3/GqbzzPPIpeKBTMok
+  O3G1vwp8t3d W0vU8t8rEZUnufoav2yzyyS3EqwlPNAeMIMlwPlNYy918zOLEy9lJyk07rT+rW
+  uYk0YnC7i6OOcZ 5PYk0rW9w9rbkZELuSVXqDnGB6ZqeRnNjFIIJo5GygyB68dB1NTIZ5bm23I
+  YiJAr8cFsHpWrm0gx FWlGmnTd/wAH+enyJLCUPOsAhmGYpHUh+vv9OKrXLNAIEIiETkOp28se
+  uP50LiOWKVRJDtTZknIA yd6n3NQm633yTImzy1ICvyApG38+9Qoe9dLT9TgeFhKo5Rimrde/r
+  f8AzK8zN/aC7kxFtwF287Ty OfX3qL95b3Ufl7jGgwyvyTkfzq55LTJBGJEdURotwGd+Odw9qh
+  AhFo6sxxkGPI5IPfPtW8ZLY7Ka hiXZ9PXQquqGSMsrFFX5RvwSPfikdlR5iwG4t0A6e1NYmYR
+  naoDFV3dOe3SpQzBJ5JCFuCSWTHIP Q1tsbQnC/Lf5/wDDWPQtNDQlYLWKXylYB2kO4tk9R6Ad
+  x3rb33H294hYFkXJOFGcA4J+mKxgbZb2 FJXeJMPvKvjLdVxT4b2W9iNul7EkjOhdyCPn54+hx
+  0r5irBzblb8zzoqUqsny2XXfT5rcubYRcma KEJAq4hUgFtpHf1NZ++ZgqxywSLCrLhRzkdT+G
+  a1riNra1ZFUCVz5sYIyFC4JU/zqo88F156GLb5 7faCyHG3HBH49amnK6va6/r/AIJ6VGEpWqc
+  l0UJZILazjiO8LLGB9/DbgflxS2cErXU7TSnypDgu zEgtggH+lTy6YssMlymfs/nI6knOBnDC
+  l+yLBb4iEgj83awZskFWyPzBrb2kOWyerN6+LjCm1Cbu /wCvkKCLO0VjazRjMZUOwyoB6n15q
+  S7iM32gxXaRusx8pv8AZ7j8802WK9bVhNdwvJFGxSIKMBcn Jz69qe2pKjzR2yAsvzK7AEZ4rN
+  KV046s5uXmanC0pddV+pUWy1C6uLV0ubYqi7Rtj5UN/wDqpjWl u8CxPL5kxQtgNzIecYH4VDc
+  G8eXz5IZgsk2+NYjtIAGMce9aDSRC38+9gkhEcq74hw4kxwM+ntWz lNW1+6w1iakJxnBL0Vr3
+  69v0CygP9iWgQRxOsLKzSpu5Lc1bW2CIskUOUVdsink7jwvP86iHnGAF 3RcI/nLjmN+oBx3NU
+  bYXLQxtvMathgWzxt6KfU+9YNOV3cqLlUp80ZW1ZYuvOit7uFogjpMqxEgc 4A4/GoTA10s8Mc
+  sOxTmIbeWXtz35zzTxeTx/vXw5kbeARnZ25qKO9LTRyHyxb28PlNIoxlyev0rW Kmloj0KM5uE
+  Ukrp/1pZjzBJ5EFuq2wkWJfMJiB2ZOSD74qERySySwRyBY8jyvbI3EH344q1DOzNM YADdGaME
+  HngfepbYfZ7mZLd0mkE+5Vxksq5/xx+FLmaucs5TTajFX/J9zNmayiaCRorrMkL7C0nQ DkE/j
+  Tmkku4FgmAiIi2O56bsdvQdK0J4DNBZXGxGtxbuT8o4w2R+fSs+7lLXUd3FC4Z13bD/AA7h jn
+  1xWsJKVu+pNGfPCL5m2m+vUhtrCK3Yfa5I5WkbCL64U5P51qQzx/2WqxAKrIXmVTgq44X6A88V
+  m232OWbfcSMAjhFfdgBcYx9T61qmN/7OjgjMewhg42/NkH5eaVd3l724YhRVZObb+Vv8/wBCs6
+  Sr aq33wyAxBe69WP4ULeGd5EmxBHu8x2boTjgfjWcv22O+EKAxyLgx7+flHOKt3PmTBJg8cjr
+  KMIgx 17Y/M1TppOzOmeEhF2l/l9//AA5PeQt5ANy6QKyqAcYHOOn0pzzwwIYIoCZokdY5cArg
+  9eKpedNI k0V66FTmVTt4baeMexoSWeVJEysU81wJUV/4F/uml7N2V+n3Cq0nHlUlon8v+CRl4
+  Lixjtw6QgBU TzBkkA/e/OiK18rzkeQl/MVJPrg81B9kEUJuZgS8c6rJ6Nn09B0pJzLJffuxKt
+  wsmwc9d3St0t1F 6Fx55XUJWj30sSym3+yjyLqPzWUZUoQc56A4qnA4aIJfeUjiYSRlkyVC9Qe
+  O9aCwyrBcI5gLLIDK ioA4IOOtQzwILzDkukAxlemDzVRkmrXO2tCpiaVpzbt979NP67hmS4uJ
+  Ag8t5nMjjGCGHIGfpVq5 hurpbePy/nkVn3kcA44H1qta7JIp3kjlFj5u4SbueOBzVRoZnvJBC
+  0rqn3WBPODU8t5drHDTpt1P ity7f1c1Y5lijRZ0KlEVVZhnC5+YN6k+taFq9hE91LGytD5yhS
+  edqkcg1myxrMZWuI5IkL+YjE9F /iz684FJm3NkrQspkMgIXscA4zWMoqS9fuOudpRs01f7vvL
+  E1wV1F/IlWOAY2Mwzu4xmnCRbaG3g m3TuylMH+L/a57DvVaWeKWztCWWIhAhDDgZ6/liqkhMU
+  bbMkn7ztz5ZAOV+pqowTSWx0SlGq4x5e Xz/qxsxh2mhJuoZpY0kQtGMKfQ4/SlElwmnOZ3Bif
+  MhbJG2QDCj6e1Z6uY7KVWR5LffGx28MB3Gf rUlw9nM6WtqJsFt+CxI47fiah09fL+mRKnOE1z
+  q6XWy/P/gFiG4VGj89JI5Ik27mORjuMfka0yk1 ywNuoYzQl2kxxuzjgduK5q4tpLYw7pcyL0U
+  88dyc8d6Yst1FexRp5imNCEQHkoeT0qpUFL3os2lR U17alv8A10f+Zr3LzWiCORxvCMin0BA4
+  +ppbGNomVJd6rErbHc8EY5H49Kjt5XMSNbp+7WE7DMN+ PQH1PXmq0ySXt+Fg37mVfKAbA2dST
+  9DUKN04vQ4788XGbStu+33afmTz20i+Q8cy20Dw73LgkDIw F+tPsfmt4y6NJLbxnyiD99e+fX
+  iq0Ya5uLnzpFCPIpB7fL2FadonmzS+ZC6sgZTsOAxxzj9KVSXL Gz/r+tjKrVpxj7O7l+X6GXF
+  dtbFJYSPPRCE3Lu/dk8n6+9SlxLqb23mL50T+XE+Pl2kcnH+eabbx SJbbXspkneHCBjn7vapo
+  WZLuFvKEcrRfMxGQ5PQj8eKqXLq1v/X/AARfu+VuH+f3+RVhEMazr9si hMlwPlcE7doIzVu4S
+  WKyuHkYTvK6lTGcbRioRDIrmL7N9rZVIbAH3yOKmaJJYYQ9yiTouyVDn5nb GMegxQ37ydyqUX
+  CanLbraz0+SuvvIALdLJEkS4h2cFi/3t3J+tW7KIygxIphlEZ3O4ySRyPoMdqs XMFuJooIMTS
+  7GZxn0xgc96rRSlJSzK0MJYu5Y8pg8KazcueN0VUvODcE0/P/AC6ilY0uIYbgxyXM kTYwPv5H
+  9BSQSRyXSwqkkCkBgzHgqOAaXyJbh7meK0nBkcPHMSMDsQPaksILS2keVpQpBZJfMOcc cEego
+  fLyvuKpD922n926ZJcBQyKS6hx2PRV7Z9ScUk63tzbJFJiEGJn2FeQV5ozaMIEVJpEIBLb+ GG
+  ecfTvT4ikmpXSxXUJiklGwk5JYggY9AaSulfsdOGlKnG838Ouv/Duw2GLzkgnZQjSqzlj0PPb0
+  FbVhKLiOZIU4SVURfXJyT+GKyXVo7aSCSVY5Y/nXjpt/hqGzvIZXd4IZ1Vn4G/7g5JB9T71FSD
+  nB sdXnq0m5v0N4+fBfSm0MboQxAKZLDuR7daswXH2q1+cCCLpF/uHqDjqeM5rPuJUZ5tysu2N
+  mhKNg NHgZ/P1qrC15HcI0Vu7RNjcuQeTxx9BXN7LmjfqZypJ0+a2q79S+IWUyXErFmRkRdvAO
+  7v8ATpVG 5+1W9uzLG4uEUur4+UADBFXJdR8qcrHPbkKwieMpksW6H8KxLu4dLkW8CzNOA5Ys2
+  QMHpj6VpRhJ y1RhSd6n7yK+f9f5kVvcI2mvcKZDFhQED87wOufQHrVS6uk+3w3CSGSXb5rYYh
+  Wzxj2oN3HGFgO2 IKu5Dt4Hcg+pPaormUyfNF5CIkbIfl6Z5Cn3r0IU/eu1v+R0RhJv3oavzaV
+  umyIFluHmCTL5jfMw 2rjGeB+Ga0IZoY7QSEok6tzuGVwODgfXisqERxQC5lkMUZGI8knecgkU
+  24cyzTJG28FywAX7o6kH 8a3nTUnboLEUlVkoQXurdp/g+5LudmVmRWmADMXAPzA8/jzVkm3ht
+  9sTb5Em2ttODg84/ClimvZp raNbJmjJy+EwWY98+nSorSzmXUWh3KLjd5u9h8uB14+hqXbW+l
+  jJrn5rWXKujNJYtMnjucM87glN qtyMfdI9qzpLZbS9S1j8yVmJWRN3L8ZBHsKmit4JrkySLII
+  pHdwUbGVH3f1rOWVRfRpJcpBM3yq7 g+vHOKmmnrZszw1T2kJv2jtHv+WhZmQRyhHt52i4ICth
+  gSM4zWbds7XBJ2xkk5xxu9CPatAGVLto rpzKZyJW2HG3BxVUN9sugYouJ/3sYPITaSCPxxW9N
+  2d2RCrBTd1bu9l/XyM+OMz3TDcI1MmAWbIG egpGheOY4jkyDgHHtk1NPbBbsxllhUL1IJ5xnt
+  TEWKMDZIXVycY6jiupSvszeUZOqoJ6Punb7+pJ iOS2VmnRmlYs/GQp4qOB54xLItwjHzQWIzg
+  56mo0RQpViqvuUbj93vS20pkHlsm0s5bKjA+v0qXH Rnm16UYKSvr1029CT7XGZz5qkQBWSMd8
+  Z4b+tRzJAjKgL7h0O7/WN/eX2q1Z3Hm2Hl+RbTMWd8CE E9evTp2qCZ1ubeGW4jcqke0NHgc5y
+  Ov+eKS0la1jOlUmoqXLZdrp/hoVSgQBlYyMflAB6MTyPrUv 2S4juLeaKRUUbyxkGQ6j7uPqeK
+  gR4Ycy25MTCQkrL82/PHHHFajRwz71cTu0TbFUP98+306mrnKS 9DlqxqT+J2X3fLcmtYilmFE
+  0SXCAqVZehYZAPuTWVe2zxpG8jAttw2BhYyB9wj1NWLkmBZZTFIpk kVgWPDAjqP6VHDPHc3ck
+  jyxrub5S/Krnggj1x0NZwUk+dbGMaNWm3KPw7+b+4I1t28OrcJiN1+Xa ecnsfwNCiK4t41Y4l
+  Q7ZHHb3NV/NNtGIoplaPaUT5AQyk8t07VLaC1tS0aSAKJAGJGS5PvVyTSb+ 4wdOpBXb130f/A
+  O3SYS7kkMcjvKrKir83XB/SnXOnW0MwktXJLuWiYE4yOgPrnnFQJYPaRj7Mr3E xJbg52heo/E
+  VoWotpWLBzGFb9zG5ySjjgn8e9eHKXL70XodSw7ilOL0fl+auNjtLxftF1Mzpifyi jc7c9R+P
+  amy2cKSBFD2uI28wyN905xt/KtOewEOlpHHM8p2qJFLkkvkYNVkMWnT6isgZ4HuDjzDu JLL1y
+  e2axhWctUz0cPiZS1Tu/JW/AgtcW2/96zRsVAJ+7kHnimSTvJq4ZUZY4YjG2ejMT6etPWKO 7g
+  tFwZRCFiOw4yCcn8eOtWPswgaRgJFtpJCyhjksOm4H0zVOUVK73/r9CcRXvJ8+rfyGi982J2EU
+  qsZcgscjjqPqcU6W2jmeQNC7JM5lRo8DZgfcPvWlbWyAKEiaSZtzKvqBwf8AGsgwy2kUKO7TAq
+  37 xTgEg8YHvWMZRcvd0/r/AIB5PtXKWyVvPX8xEmuI4osFI0kw48xc9PT0qnDPOV3Xmy6dgSx
+  QYC/N 0Of4j61atf7QLgTRpPEJFV0VPm5zgD0pYhHDPIhUhmOTnu/PT29q3ulfRHZTcY3en9fi
+  XJZ7p2jl toMSOrtsKA7SDgFvXvWB5sxkt4TIslzKn7wpwMDPIHarcz3kH2RIoncmMLuB++M8s
+  PakNrN5zOWR cuSzY++o6lfQZwKqkowXQ1w0uTeS8rNX+fclhgmeN47iaGNdwEYZOSAPvfTpUV
+  tGkhfDRS7pFEgA +UkfeYD0rRguopCyi3kJchuSDtGMMKqSRxLbWazRSoiJhirbcgHDZ98Y5qV
+  N3aehcHUTcWt/QuGG 3tpnuhIhQOCBnqB3z+NRyR7buN5HRYyhDOo6Eds+9WltpJtQMVo0f2YA
+  iJXXcdnfPrgipXsJFsmc zxOElRRJt+ViTnOPTtWHtEnqw9ooz5ebXz/4bT8ShIphW3lkjcRxw
+  PG0Stg7mP8ATNZ627QrNErq JIpFgjD87lP3j+tauLhLy6NxIpE7eeCV4XZ979cUtncyXNi8iG
+  F2adJXOwcH0/CtVOUVf+v63G+e m3ZJ/l+RWNnZ2zRpIyPBGHCjHTHUn1rOWST7JEu9ApXcDjn
+  g5K/XpzW20UEVtc3UqSSsbgKGDfKF /u49e1VEht57ny1tJ4N5LqjPzHgcqfcmqhUurvUinOct
+  WtPkVLS1uJrSRVYCZZifmGSo67frirRX zdNhBtZY7cqPm4zyeDn9KmtYEigE0UrK77N0ZJJjY
+  kjafcip57rar2m9GiPIXHOV6HPpUzqNz0CL lOquSS06tbFeKI/ZrdIYQgjiZAZQGO3PP45rPu
+  bV7qS33W8vkAEPIuBhyOp9geMVcZnmado4ZCBK jKqtymBkg1o+bbta3UARyrSFzGG+bpkEH0H
+  enzyg7nb7GVFJ3u+/b8TjlW4ntxDdZzGcFQMc9GJ+ nFSk21tKSgaVgOG3d14P+NakLpe6fKGM
+  cSK6oJSPvlgefpxUU2mQi1DCTyXRWWUsfauv2yvaWnkd tOvFQ5Z7Pok7Ge/mm8WOOYC6Zd0gI
+  4zj5s/hTn0ib7FC63cd0ybo4xHn5xnvnrxT5YrdWeZZ1WRN qkkk/e4x+Wa0Y9Pia2dcywBJ/L
+  ALHK5IwT+FOVbkSa0+RlicQqNSMrWXmv8AgFFNMmbR5I3lSE7w QjA8flWlZ2FxHfPJDLE8aFE
+  zt/1h67h7VoBoI3eJ23xGXKt3kI4BB9M8VIXAWMrhZEDeaF6GTPHH pjtXFPEzkmu559XF1Jxd
+  J7P7kVX08yPi7ljERgYQsoxlAcs341TklhlQojwW+xgRleqgc/4VqQXH 2tbszJseWRWGT90EY
+  AHoMjpVC5itRbGRbaZJiMEFumeP07/WlCTvaRp7SpdQm9trWsUJo4ktg4lg Ikw5i2/MCOKha4
+  thdRNbRSSzONzKTkAZxtI9cZ5q8F32IhWAu8bCIMOwY5yfyqOZLmS+uVEaQt5g fbs5TjlT/nv
+  XTGS6m0KkXB3k387fgihFcHbJ5kkUayT8K4zuUcAj2qmziI23lxlpMlXkToME/wA6 vxwQtN8s
+  yrLje0TLyAvoen1pzyQzwxRIoTE6hnzwdwyf/rVvzJPYVScoNXu119P1KMwMskCz7wjQ 5MgOA
+  v1/L9adbPbRym483cwUoV7kHqf6VpS2yy3KpyvlMYxnkEt3/Ksi5V45YrZRG6hg6MigblHU 59
+  zVQmprlOjD1VVTipW7pdvuf5lmaBdkjxpMsDSL5ah+ijrTxcBIzNbxNGYN0asxyME8D69aikik
+  uru5itJSys+8nPG7GcD0zUkK3QEayshcy+b5YXpjnB+vak7curKquVve963S/TzJYpI0itJ2kU
+  Oh yy/7Hv7n1q/9uFq8cZSSeSZ/3LIcZTGT+Oe9U47yC4uJDHaySBpzsUEZQY5U+tSNZXKXkct
+  ycxOp AK8YU46elYTjFv39Dj5Kbi5S0t0vv5K2pm3QMmp2WJJlUw7mG45B7CtiGKDb5sRZmjm+
+  QM2SykYG Pxqo8MUGoJmOWaHbuOG7rnaM+9aWmT776WG4iWF5U+0ZPG0pwB7c060m6d10OrE03
+  OlzQTaS110+ etySygvgXNwNgikCYx3YfMT9Ksi4g814Tbqp3rIkhAy4UdPrRb+VPbFi8jM0uL
+  k7/wDloeOPSpXK pbyieJ5I1AijKnBO4evtXDKV5O6MVyTjzaLyWn66kDyWtxfJOkTMkqMylWw
+  VHYfU1BG9vBam7lid 3YBNjHIIzg/j0FAgS3WCOEthEdMHncTzx9T09KUwia1topEe1jYFlMh6
+  Jj5gfcHvV+78gclKacn7 r0a6kMt8qwYCuHQiN0B+65zio4EsjqDRXc6FmQDAyN4wckVKloN0Z
+  8xE+QI7MM/MTwT6kY/WpJzg zSLFHclB+9eNfuMP4Pqa1TjblidKqQUJUqd/vsx6CwMcsoBSKO
+  RU5boWrPCxxXG2RQ/lkEmPjaec GrJhsxp4W6tbizjkZZA5l4fBx0qzdRWdxqJWOUNN8xCIT8w
+  I+99BUxkk+tjNckLc3M777NffqZv2 cSadJcKWKOyqznnLg4H59aWO1Np50MbJuF4CwYcj0X86
+  vfYrZUlDF3Rm3fK5AJAwMU4IXs9rFY5h taRzyFZeuffFU6t1a+hrVqup7t7r+uhnTRNDqD3s8
+  oUqsgCEnrjhalVt9rb5hu5HaP5NkmMDHIPv 71baOS5SQuoQSEypkDjjgH61TFvLa+XeM/WDH0
+  znimp8y1eparua95pNfiVHCw6PG5t5wQw2sWzv To34iqlxK6ySFGY2m47STluRgAn1PWnvJe/
+  Z7VVDBPKwu4A4z1PSneRLN5qmN7gtKAsUWAzKBw49 hXTFKOsjLkpQXNUafdq70+eifzGQBhDI
+  JHt9+5hIXTJDgZX6DHanokU0dskZSVmgKTlTxvPINOlI W6khSN4bcljI7c7iR1FY8cclsDKZk
+  kYTCNdvZSOv1Aqox5tb2Zlypu0ZNPpZJ/kXDa7LuOKArLsT c0bLncc8MKjR/scbmCSF2LlW+U
+  ZjB55z1qjKVM8rQtKNhARg3UDqac9xIzPnZsDEABeSP7x45xXQ qcnvqb+wqOd07p73Wvz0L8s
+  WLRPJuT+7GNoY5ORlj9BVO3jurbTJJon3rkB2HVQTx+BpVtRFEJWk aTYwJ2HA4PT8afcuJGdo
+  pAUmbPljgqegBpK/wrVCoyV7Lb009P8AhyENc3DLATvVWIVVAGcntx0q nKEjuSLkhz5mFI757
+  j0q0izrceW37twu0joVz2PvVdEmuZI12KZNw25UFiSeK3jZPyNK0eWL5bKP 3W82ORi1tFJbEy
+  SqjIwz0Gev5ZpskczS/u0cqJdse3+EY4P41f8ALMF3PCkEsUjPwD6E88e1RXNt JbLA0Eh2SSk
+  ZIztUDnNTGor+pwqs4S5Oa76aXb+7oQbJZWmA3BiN4B5IA6D602GB54gAoXEvDEcK jfeB9we9
+  SAwi6liEu/L/ACEDHy49x+NSXEix2lulnJ54lVlTbwSFOS386bk9kTjsRFqOr5vnp/Xm jKIxO
+  Yyrud3AHU4p6t5yK7MyBVJfA689R7dqt2iwQXQkQTTMqsDznJYZ/wAaRzaw6cEjl8yVwAM9 Nv
+  X065rR1NbWJWJ5pck20n1X6joDHDcK8ToxbLeUB8xA/p7U2Uo0MZWCQg5yQ3B79Paq6wkwqJWE
+  LKArKwySSeufTFNXayNJFHLJHHMFByNrBep/GlZXuRKvClUU5J39N/yX4Mmfa1uqCEMyJy2OM7
+  uv 5VWRJwkpkmjBEu3KJ/CRwat3F+0stwiW+3c+SVwMen4UrXDvE6LAC6j5iQDyB7inHnS2KhG
+  rUd4p xT7P/gFaK7gjt4YtwEhO5POG4DngYx9agke4VVljhh8k5LmOIBeTg/lSn/WCRooZBGhQ
+  nYMDcMj8 fQ0ExbEgYvEsURVhnv1A+uavlSeiPMqYd05W5eu/UjWYRXcUs4jni37cIMH5eAOnG
+  anjluIdQBji VnVsyIY8/Mc4p8c0ZiaGUxqZ5lmZdv8Aq9vY/h6VEzCG5MqRzRu8oaIs2QAOoP
+  qaN7po51rzqS1f f+l+B2llMxska5nRWZlxH0fhjkZx6VfkurSG8imgmjCKrjDDIznAH6mqVpm
+  2vtsiDgsmGHVtpO4e wGKlitpZtKhWNobjYUMhVerDkD8a8OoouV3t+B3ynT9o+bVfcvysi9+8
+  mhlEaygs6uATypXt+Hes 2NZLieYNKDJIWYMQcL/skevFMf7VBPNcTHaIpPKVRxuD85rW2Tr5j
+  w+XuRykI29j97Pqan4Fo1r/ AF/kehFzi/dktdv8imlm/wDZ00bM8W6aJmA788kelaQtZElZtz
+  QmNXKeaxYHPXA/L86rSMRaxTWu RLt2lXO7cv3dw/DJrXtYRFfsTcLMY4WhhB5DL3P19/asKtV
+  2vcynWUkrS+XmVLe2uYzbKrtJhFV2 U42E87fqaTasGp+WY3gzEziSZtyLjtj15oW4aWztkMcu
+  0xlmCNhnx91gao287SSYkV0UDZK0h3Zb B/LtUqMndv8Ar+vQ8xRk5ObXy6/16otG6uTZAPsWR
+  gHyF7rwahW0lnlwd6MCUyfzP+NTPbXLWP2d mWRSo3FRyR3x+HNSxypAojSZQJv30LvyGC8H86
+  fNZe7uPmUIpwfvGch8m7JEU07qAD83BBUkEe1R /aHazhmmikP7t2jIONy9GI9qutcx6jBdNar
+  5cjSxu3sp4K/UU0soysZSaIkiIDn5QcHFaJ91qdSo QlNSqL3u1/1EhW2/szMcheQsNuDyF9a0
+  yktxAqPD8uMFvXJ5P8qW1Jd3tkSLaWywwNxPqPRQOoqZ poUtItsy7fKcKxPUMeP8RXLObcjL2
+  jUlFLW/rYhit0/tFCrFjHHJFhSRjdzk/kaGtvtVsRFMPKEi gqDyw7MPbNUo2voTsFtNcyhfma
+  PgH+9+lXtPaOSWWO1Y2t0doiMx3L5XOePz5pzUormvsdk4VEnP m27W09ev4GZeRyNqgUyGXJw
+  Qh4BI5/AjtStBO4jWBH+zCM/OnHLdKu3wzZxxuQiOpcy+oyMVVj06 5uDJLHOYDgZkfO1Mfw49
+  e9axn7qbdjSlV09pOf36r8BdLtg0iRzOwY7S6s3TGc/jU5tyuqxXUayS wupZWU9z3+nAqZ2WI
+  Nl1Z2cFdowVXHIPvjmq1tatbqDbSM8GCIAWJ4/r3qHNtuV9zKVS6lJytfQb FHkxvMrgSFXBHA
+  JGefz4olt/tN1biWSNopEZpIkGHU9Ov4CrTwxfYZCbpFMkqzhsnCBSPl/GnSSw RXjebbytE0h
+  wUIGTwQfoCeRSU3e6ClGVlNKz6dPzH3HlWlkA43NKnmNsGDkMFz9MVmvbW7I8tpIW aN9i/MT5
+  g3Y3D2HSrSpbSXZkkSWQFGM534GfYdh04pGjUERrYXCFXKffH3CMsfwODTg+U6NIpaO/ olf/A
+  DKEr20MVxFKu1TKuwLxgdh9ahkt5vMuoo8gKdspfnL7f6Zq4+mKkhKsbna2WP8AexySPr2r Su
+  CkVmpjgknadzMVUjcAMdfw5rR1krcutx1cV7N/u1dv8PvOQitVDQxRwzTDaFnYNwzE4BH0rq4o
+  7qOzkjitpGJDEM2Du2HrWbCmL0T2YMX8IWT5twznd9B1rSjjvLvUjsLEhyFK8BxjqPaniKjlu9
+  F3 FjpzqNLmvFau9/8Ahh0Ane5mknWOKBnVk3LypwT/ADqK8RhZWiSxMshJAYcYI9ffmpxAIrc
+  tdSEg Iqtg8ZOQP6VflMk/lwSwPAqRu0hfHLgAcelcbnaSa/rQ82rGTnGS2+77u5gw290JIti4
+  J/iI9ODj 8elRYmiMYlhlMYQqxb+HBxgn1NXltilmwS6BAwXOT8m05/8Ar0gu0lhuXlvLaYm5G
+  7YuByOP0rbn bba1GnUTbi7p76NW+ZFj7dI6wzRxgEeYoHIfOFH5CrjRTg3SECSZpt6uAMHPH5
+  CorkQwz3TpGSxl VZthwGOARj0xUUEp+0XT28zFzJtQuchRg5H1zUvVXW3/AAxpXhBpOCv69zn
+  r6KSPU5PL2xXIJXkc upXkj2qs8e2VjKGESPsUA43YH/6q17oXZEMsUL+aqiMhhuOSOP061SmU
+  RWSApJJdRBUdc9zyDivT p1HZI9GnXqStZJ306XXz6IpPs88AecJyN8ke/lWFDW8lvvljI3tMr
+  R7+T/u/rUTNJNtiCfMsLJLJ jktnI9/arEMkzyyJ5yRZUuodc7eOe3auhqSRs6U5Sdvne70+Qk
+  0kkL3CMPIM0sZ29GVe/Iq5LNGj 71uYpUVHWJFB3HJwDn8abZQGc4klVkEPBIyX5xkH6VNLLFH
+  ZxCGITREEblAymD0J9qwk05JExmva KD/Db/MzbhI83CKrAxEDMZxlsYJrRFx58SPDL5CiUcTf
+  NtPQA/WlaCGJZLYyjYzeZI7dWA6kH0NC GxaXzSJCgIRAp+6G4wfVs96JTUlsVXxMeXl5Wvy/E
+  2r20NxE0DuCylgTGMZxg1BbzW2VkMbvKq7d in5sMfvZ9AKhaZZI7iNJDJNFdxqI14YhRgjP+e
+  lSwXJ3T+TbMsjuphDgHMecGuFRko2f+R50lKKa k9Pu/wCGH3gSW0IidQqEI23jzGJ4cewqmlv
+  DJqMZkM4iVGH3zhCPX3PanG3eC/u13eXEsgEYY5xt HQ+5zWsjzCIAQCR41KthRwDzg+pI6Gm5
+  8itFmksQ6KUISvft/mytJPFJppUPGRuyQv3lBAzz9RxT 4b3zIzFAMRuR5bv8wxnDD9M1Uku5D
+  AqxeRBHJC8gDplowP4T6n3qlNNG9jbnzEgjlXfuHG09hx68 0KjdWaLVOpGEbrr6/pqad9DcXU
+  yW8GxsRM3mAYEhyMEVnb9Ta5ngRYy5U5AXGTjrUrXlyLx5HzEs cwjLEYAJ6UnmK19CFuVEkQX
+  J67ufmP4VpBSirNI0hKdOD0TtqvX+uhVWGCSC3+0Q3abmQqHl4UDI 2/UmtZdRsbK/INvI7YOV
+  UjKY42k+vNUbrL3f2hWE8MTeW5j6B2Py/lSLEftl24mha5QnfxkMRySB VNRmve2+ZdKFOpFqT
+  vfZXdr/AKEkTxXV/NECYRDIApdsgqByappcmaQKsokTaG+TjqeQfUnpSCeS SWWeaL947oSUwo
+  +bgjFUv7MuIZ99vIiIFZH3clGwflPvWsacFe7sXKGHjNqTs7adfuLSX88mqbxG YGLbiWORGAc
+  YIovpHMzwbmLRLhXB4fHPA96itwohjC7pCoViOrMxHHPpntVq4V5I90jxwztb75lY fxeo9ABV
+  +6prQ6aNSLrptWT07/8ABKzvI9raFkdZHVmUnoVPGfpUTzPbSTNbbnmLNGxH/LJemD9a Y8jwX
+  NpK8q3MKpx5foB7+uaq3DSCFSm6Aw/IMjls8kn1+prWELtdjmjSndqOsW35X8thY1cXqecs vk
+  yL8zk9fRh7VXcSRXJZ4+REwYkcEnoRUiTXb3MKwSRHCO7Lt7d1plyxjsEL5BGIwG/iOM4+tbpP
+  msy5VZqraVlfTf8AIqGCSVbeKFHlmwQ6ge/FWo5HAlt5Io1Eis6uVwVHp+YpYGmbe0OItkgXOO
+  Tn nNNFvDILePbI52Oxw3KlTnn61cpdH/XUyq15QldfLun+ViwJbn+ylQIJMDG8LwT3HvVSxjm
+  uZopi 0YhjYCR8dPr71JbzTw2xuBbyO88jAgdFz0OKnt7gjTJlVQhZzn5eGx1NQ7pNJGdKDqLk
+  TV5X8vzu RXweS5jKlIlDHeSMkjsayZJZGnQqjnOckfwntWlPHELCAo7lpOChYkjHf8qjy8cu6
+  Hy4x1CuueM9 +OtbUnaJ00YzVJ8t9Hb7vkLEIyWSdZ43ZS3mGTPOOTUbK8mn28yszRBQshxkKe
+  w9s0wvJHMjocbQ VAbkc05i0NzJGfly43IegK9M0Wd9BVqMo1PcWi1syFTJvWdiFKMVbK9Djp+
+  NV4IxIAzYj8s4BJ6e 3HrV24jaWzhnWGRRhlkOcjORj8ag3MkBiZAJiWMuR159O2MVpGV1ocUJ
+  e0nzWu+t7FhDD5kZIbck Rf5TjkZP49qekME1qtw0iwsVdSrDp6/zqKAv5EskEWGEyx4b5icg8
+  fWo/LdIFikBRhGwIYn5uefy rNq70ZVaUqtoUrLvtf5+Qy2e1iy5jlkhUNH97JJz6/jSS2bRws
+  8BdEjk2ybz9854x+FCBUsQ0S8g 4k+XIGen4moAsko3jayxnbNhccnpWttb3OaVKCirv5f09Pu
+  Hs0RikCh423ERqTy2DwatSQ3FxcW4 hjwrgKTnjpk5rNjiENpI87Kk8c4AXHOTz/Srq+UksCW5
+  lmnCNvCE8EDNEtNjF1alkue1+r6evl5l MxsbyzXcsm7JcIMYHfPvgVanmtnnDph1bIUA4KjHG
+  Tjk1FH5jzwLHJGzuhwqjnGOn1okiS3ZNp83 kF4/4l+Xk/mabtzHHNxjUum5SXTa/wDXctQShb
+  HyktgWcqS5ALMp4bB9OBUoSK1ncEnEPzQyPyrg df51m2sP2G2SdnMi5C/7xJ6j2FXEdJtRBu9
+  +1W/ebc4z34/Lis5x1dtjjjzTlJ04XW/d36qx0mJI YoGaeMSIrZkbnDZ5/TFNit5BNnzcKcbl
+  jcghgcqD+HP0qOyjMEK26AZWNmmZ/mG/cMD8q15khfV7 ieQSTAsQqwttyAACfwrzZz5W0fQU8
+  Q6dGUZW18l/X4jF2m8kmAdojkvuORkDIpDL9ujhMEc0oWIE Kr4MmeMj6HrTpJ4ltQsDpHsmSJ
+  S/IJPr60sVisNxFFv86VS77YTg4B6j256e1Y3ja736GUa9JpTc dfnp93+ZTtIEl01oZZGt5I5
+  VJZ2PzAnqPY9MV1SvNb37TxlPJjJjkyucO/QfhXIrczi5UPNBHHFG FLMmc+g+prZa7uotQKKU
+  MLSYPy/eJ5P49qjE05SZpjaEX7tlZ9/00NGS2mLvsUiQcEjgAL149xUU peKWGCG7tFMgdolaP
+  5iB3J71I01vkZS5QvhnPmfdPv8A1qbT5Z45JIJLUXA3j5gozjknB7A+lcd5 JXfQ4Zzkknbbul
+  /wbk1s9zBbQyzQO6xtGkjADGOf8ap6hdR3MDwQLEUjU7NqgEBSDTbm2vXKokhg GFJDfxc4/wA
+  KiL2kF/JIx+0SwnYY4uCd3b60RhHm5t35Bh8Iub2srN72X+b2EVoy93JeWlwqSTll WIhCwwMk
+  Y7Cqypbx2gjimFsInC4lOTwcn861LkC6WdJswtET8/Reg4/HinpKwtI2je0VppRK5eIH Cgcj8
+  6r2mn9W/UiUpRkua+r26fqZ1pZzzzCeItLNIoIKHAXJwRWvHCY3kiNxbNF5mCSnfsR7e1Zo eW
+  aUEK1uxfEQU4yv8XTvnvV1pIBb5lvIWtw/mMVGCe2c+lKq5SZ1TxMpSUNL+Sv/AF+A27zD9pkc
+  Sk7gpCNjJPYe5HIqGNrGIRbFltTs3J5j5IHVQT+dTXd/E5ZVhkdGmCtIDwW45/HoPeojfSS6i0
+  bw oI43KOpXlc9j9AKIKXJqvxOxVJey5eVu2rs/01Qtx5H2GCSIPdAx5ljVudwOQR6AelWbG7Y
+  RTBri GMySrIXZflyB0x7iq12zI0k/lEIW3bBgFhjGR6AUs8ccKRoJog23OTzuyRzSsnFJ9TJw
+  j7O8ou3f 9LDbi1nlGzzFMoZWdFGCgU5Ofwq28Vo8M4M+N7CcMHICr0P4YqTzJ1WScqLhwHRTG
+  MZ3YFZNpbTR 6qEeVUQBFk3gnaQOh9jzSi+aOrtYmlUc6LlN25dv6szYntYI3gWNHnt1gcsqNy
+  W/h/DHNUYmQ2lt JHDMp8rJ3tuDHPJH1FajTwRASuQ0ZQ/KOoPTH5VHBb2qTrBBLulSNiIy2dg
+  A4B9+9ZRm1HW5WGT9 nZxbXfW3z7fcUbqZSGGxiVyqRqcErjPPqalWSS5szugnSOQI4Jbrg5b8
+  DV+OG2hsoGkIuBIAQVOC +eMj2qNrMCKWBVm+R1ULu5UYyc/zp+0haxvGtSlBJ7973KzRl9ZaE
+  EtGwfeFODG2MgH8KznkWKS3 mnt7pSVww38bm7flVxLEFFnbz5N2HiZXxuHOfrntRLcQtZwyRT
+  RQtNwiT/MUJb5Qffg1rFq6S1Gn T5+Vaxfrv8v8hph+x3TKkMvnuxdSzZVQq5xj3q3a6jK9lDc
+  +TteWMtIqqBh2GOPQY7VWYmdh5kou 5gvzeUcc5pFWCNJ3iLRSk4G85X2IH51MoqStLcirTp1H
+  yPfv/WpNFJILF0wIgkqoWlG7cU5/TvVv CXdzdebeI7ySsSyEgEEdvTpiqsBkSNFZfPUHcSPXt
+  mnRXCrNI0mn3FiPOBeSQggFeQPy/nWbi7tr f5f8OeevaRcrLXuv+DqUTDGdoil2wkAFCST055
+  p7fZjYtE6CJ1kCJ8uCwbGT+HrV3c91fyXG2ImU NMiIuMheCR7UyVtLk0+RlcpsdFikZ8iQP24
+  75q+dtq9yo1Jykk38+3yM+6nl+1xW8LpIgjbLAenQ n3ogeSW1hiht2nkj4LR4G75t276VA8qQ
+  ube3ZEniOHdvmBwDnikt7SRrqO4dnjcxERhGICrg5B9T 71vypR1DEQi6bctLbX6v5WMe6kv7b
+  UWulWWK1Mv71pOQx6jHpUMaNc3MckrEbrZnXDYIKk4B9TUk 8M/2K0CzLPHcASbSCcEHA6+9Ub
+  m3kju4wXYXCMXlT+8wIyB6DHavSp2asnqdtGrCcLxfvbdfyZLI bIWoYmRZnVMpv+7jgj8+agc
+  R+fcxKkkhD7Bg8qR/jS+UJr2Qh1hhdycuc/KD1q0dPu7NIZNpkGHy VH38dG/WtOaMdL6s9GnU
+  jBqCqczl3enpctIxSO0uACoERzg8HdxkewNVY5723sMFAVhbycFOpJya tjbPbZ3iKRYyqs33S
+  oAyMfWs1pgiXWYJA00wcAvwFxg/jWUI811b+v6uVQp80mlDm7+X4luW8vhN bS+XGrCBsZQEbT
+  1J+varcNxZLpyGJtsyKqENznLZz+FZ8TQ2jPm1m81W/d+ZJwVxxnikzeyabZGY 28ysrZZIgu3
+  nIBpzpp20sv6fmXWoLminDlT7WX6M07qWEIsYKNsBLSR8ZyeCT3JFVTf3iyW0cGwk rnbt5AXk
+  /pSBUgsbeVLyFZJfmfcpbkHjseOoqNrW9vLgxrEWQzDa6gDaD94VEYQS128zhdOmoOUt I/3v0
+  0X4M0lvLoC3lleKWFk4VV5yTwSe9W11SFY5JZbuG4KKVPloV/n1J7elcxIJFvAVimhkUgkO ch
+  QeDx7VPMkLRK+1ZkDsIfLG3egHBPrnsfaplhoNq/4GU8JHmSavfayS/H/gpmrJqFg93HEEYxcc
+  nsAemf51RubssLNjbhjP++Cj/lmd2AuMY7Z+hpLVHisprgRqqEkB5ACAeMD8aDqFwyuCkUUp2n
+  DR j5SowR/WtacYQulG/q3o++htLDQi709fV7fcJefa5LqNxMirFceTJleDu5yfpWjBiGZDJAx
+  fa4Zi OAOmPqTUCR2H2eKNoLy7ZNrkxy4z1IP40xHQf6Q9rdLCylipk5JHf6CsZaq1v6+85Kan
+  zcq0Xpa/ 43+8049OS18kwM7jbiWLdkh/U1jLdTwyiXaI5HObgMMliTzj0xxTp5LloI5ITJbxy
+  WxaZ3OQW6Ae 2f61nQzgWke2VRN5ibo35J54H+NXTpSablqdcqUoxu9blppoZLeaS33yFSqZXo
+  CD/PFWEAjvwrPK SY2bhvvAkY/E9KWWGVbmfzWigRpG34XAVl5B/HOKq/ZUEkbXF4kb+WVZDnO
+  7GcVScWtyZVoVIct9 ej1+75E3leY0k8EcsbIdpg3fNGT0U+p71YkW+RkN1F5khhcbcDPUbs/h
+  xWcst4IFdUJdUDTcdW6c ++OafZpK1/se+QSpkRFskMvf9KJRaV3bQVRVFC7Sdvm/vW33Er/Zz
+  pXnW0DxxRNgCU7gw6j9aGKT rELn95ggt5fy4b0NRyTSS23lKCLO5w8TY+6B/CT68VVMrrM5Y4
+  YOgZfU+v41UYO3mP2EfY3bSktd 29yyLdDcR27q8IWErJJnqd2f/rVTt/7PmmRJFljdpCWkd8o
+  GB4bHp2qWS6nn1JLeN49rI+Pl5OOp qqyQ8Qput/kG6RznnqoH1rSMXbV/cYVI88rzbvbp089d
+  /wARC9xbPMDGGiMh8zHHPYZ7U+3e6eXa 0RVW5PGOp6/Sq5t7qdGRElJR13J1Jc//AFqha/ZXJ
+  jY5V/lP93HUH1rfk5lZWubzpp0nHRy9UW3l ZJiY3WZQpB2jjHTNQx2iuin7TGuwFyCTwMfSq2
+  WmkaV1G8tuOOMjPI4p896jo6wwtsJCIVP+rHXk 96vkktEdPNZJ2t935Dw+5U3OoTYwXI6dqml
+  Ahht3ZPNi5A2Hkntk47URCc20E0LwtKWWNY9mTjqT zVOWRXupd8wH7wmPB4Zs54/lSWsrI46u
+  NVSTjTaS6/8ADdCeGa1ICybS0bsOv39vIP41LFsuJdvl MjyZbnk5J6VQ3SpMSIQBv3FSBkZ7U
+  z/VuFG/zFYjBP5VfswhGpBOUrrs+hYkeJd8Rm8gFgAjdSQO vFSAI+9ndGM6E8DlW3CqBVVkia
+  RopOucKTwO9StIZAI/MjjCHKMBgNj/ABzTcexyzrVU2lJvza/4 BbaVob9oYATKZc9M846Y9aq
+  AfJPJJJvZCEAI6ls5qwsTlP38gWRm3JHn5yR05qRLqZmkiEcaNLvI VkGRkfz9PSoTtsZUK7pu
+  1k+7/wAylGpEsSNA3mJEwBBxu5yMjv7ULDJMziCIorANOc8f7P65q2YI 18mZdSi85YwCNhOB3
+  zxyapyzvDelrcq28MuMdBnIzVpuT939TJwTm3Tjb1T3+6wWot7mGETHyiqM rk/xMSdpqCJFVI
+  w6SRtn52Z+p6enAqKC6la5QxxbHXJORkNjkGopZQIozE2fm+bqehz6VsoO7MqL hGc5Nt2Wiff
+  0t+ZIXcruaN4m8xdgHGFH3qr+arTqQ29GkzuJ4C/wk+3rT0EbDDSn5Ry27gHtmqwR zDHuPlfI
+  VII+9zWqSFifaTqXg/Xb/hjaEsks0UciF0CucoMKfVh7VmebF9njdJ0bCMMEcsM/zo8y eW0jj
+  dGLxJsiKHGVPUn+VVYYTJuU7EZpQFVhjrnjpUwpqN7nnSlOF3K0Uv6uejyfZJJIIpmYo6MI 9p
+  xj2PqxPT6Vct2NrBI13wVcqzHjLY7e2Kz3vLR0hlMRmdmyfLOPmzgY9OKuvHLcRXQMUodrrPmM
+  cpgYycV4Ek7JPRHqRvzLndovpdf0vuIWLtfqY/LC9GDAHDgAj8cZpbGS5urzzvvTmUbWUYCxHO
+  f0 5qNbe3ublVknOxVkZo1YhiR0OalhnU6arAqqnl5FOArEcL+lOVuWyWo685JNxuv6+9mpeWh
+  eVWgj VAU5JUENjG2mrJHJMWuJI2Zn80qoxsI4I/EViNeTyXJVplCyDzMAYGewH1xUkdwlwjef
+  PFGzAvKQ uNjY5U/Ws/YTUUpDnQm0lUlv1SuzpZfJuZZGRhErO3LHhRgZP4YpAZZZNyxSxpFk+
+  YDw2ev5Vn2U tsyQSwndMIBEATkbWPJx6irEAuAY4mcnzM4IPBABFcrhy6du5moxpt8zbXS/T7
+  v1Lkkm284aQJuG 5mbhHHRfxpGjiN0UYpkShy6jGVHX8amSz/0LyDIr+UViyf4wxzu+vvReS2i
+  zsJJE2cRIVOMqTz+P vWXMm7I2nONRNpP5Fie5iivBIkR8vyZNxcZCE/dB9T3rCRXtUjkQNPtI
+  jkPbI+Y9auhp7e4JjQsg uVDbxnB6DNTT3jXH2mMrHHH5nyybflzgDH1p0046LYqhTjLRbdWVG
+  d3gd0tZo2mcyFyeEzj8uOfx qzbXMLRRySxKVVWjQYHzB+Fb6Usst0kSxBoCVBibCdAT83446e
+  lUIZY4bySIgyWUWBAw6vj7pz6V XLzRehHs+aGnutee5oT2aiO2i2v50RLMFPDOnH5Hpj1piR/
+  6MwkkWQlgwVRhiCO57kU1r61aWVjI UdF27y2AQwyRj1J71nvcXUjpECkm6RG2IMPtAznPt3pw
+  pza1O2nhpxhzS6fL9bGsZDJA2AGjlILs P+Wew/dP1pl1bImoteSyKY4o3RohwcnHP4UyP7XJq
+  WWQeW7MyqgHy49fXNP+z3kzT2qrucsCHIyD x8xqV7r3sCUef36mnXW2n3Dx9o/eNIwhtkQ5J/
+  iOBluvTpUsECDSRtulupTIrsqfeJ9c+maxWiug yqzMVDAGM85Rj839Oa6ATIkMqJE4hwS7LgE
+  MD8vPp7VNVWSsa1qMHFKK/L/hx7SQz6bKDEYi7M7u 33S38IHoOMYq1bQyujSSSwb/ACyCiphu
+  Rk80NYh4ZzAHebz1CEn5SMA4x+dWI2uFkczTW7qzZASP ByOn5/0rklNcvus5nVdOl7jt5df1t
+  95DD5pie2ZVYeYhYKPunBIwew46VG0VuodpLoxebKjZYn5g RyadKzreTbSFeSQNHkdPlxg++a
+  p3dxM9kC8kMEhIVldc7d3X8qcItvtc0pX57LRPcjnMAEkSzPLb hiUWNsNgjIAP1qO1vLZyslx
+  beTuQuY2xu3Abcj+dEkMu1TbFXlWEnAH+sA44/CqcM13EIyEiEZcq xdAcfL8tdKinHc0lGKpu
+  z/HX/gfcXfOki2Ss0T26NtaONMOx7HPoD1p6X8TuivGiqEGxyBhic5xV ARsJomu33yxwYkiQY
+  PT5j+HBpkTTC4LxXVo4hXy0zFkHd3/HtTdKLRz1IRcHbfvdr8ja+0/ar25g QCQF1wiDDYC/zp
+  ILazR0ncziQpuxJJlW54JH0rNF3II332kiSmRfLZQBgfdJP0PWnOziZ4JJFkRW dJAByz4DAj0
+  A9Kz9m1onZGcnLrK0Xvr/AEie7+1LYvMRzuwxQbQrA5C/T1Hesze0cskjqsTmfId1 +QfL0xRH
+  crJefv5SYXAWQZ4EhBwfxNWnuEhuIES/sFWMMjRyJubdjOD75rZRcfdsFWPKuSKTa1vZ 7fcyB
+  N8bxreSQTQuE2+WmGcnoc+gPX1qlc3kS3Hlos8FyIyCzt8uCeTikuJkuYdOdEf7YlvuZQeu Dx
+  xULDzhNcmIq0xBZDydwHOPQD0rohBXvI3jB815Oza22/DsZFpd3yW8EsBSUq33GXJzu9PQjNNl
+  2SanMJ2dS83mIQ3Rf7v1PWtKVnjtIooQkryQliETkn+8PbGarQQOiG72NbrHlA0w3AA4wenvXc
+  pL WVrXPQoUNHVStJ6aeXe3T5CQ3dtHa22YJSEXEjE/x5JA/KrjI0UJnn87fOm+Nd2Ain74x69
+  KnEyr oqoIlnJk4KqPmGcZ/CoIoLiW9uYoyymLgCQ7jz1rK6d3sXhaPNBt2i+uv9WGTGAWjrBO
+  qKzhmDZJ Vm7Z9B3qF7WeK7/fxuiKjIHPQnHFTBElgMinziHVJEQYyRzSvb28dvBvmkkldwSik
+  AqCcgdOpqoy UdLnZTqUqV4Qk7el/wBVb1ZVUMJYZTIjJEgZo3GS3GPyzTnEcYkllRlGAihWx1
+  H+FRgC1uRI1vMG 7BjkDrgH8ancSmFoDBK7yuG3Y4OBjj9a06m9Kynq7J9bojR1IiWJEMSSCEL
+  IudueQD79asFb+NXj gDxwvNuHru7c1atrhkKR3scccKjcvygFmHfPrV1J7VbdJPOVYiu4gnOx
+  uoU+5rnqVGn8N/xOGs3z 6U1JfejGkl1CDzDdIsuJv3pMYHzN/D/9aiVZ4B5FzEoiR8DauMkHo
+  MfhVy93/ZEklkRJWbZGhXqP 4mPrgd6rTRmTThCkcrB5so7vkMBjafxqoSTs7IwjUp80JOCSbt
+  1t/n9wk96r2cYEZSI5eM9Q3/6j Tra+ubhts9ks6glm8tQp3Hrz/SpHkuEsLj7Q0Fs4dk2vHnG
+  cE44OBVIQSjzZ7hwsAuCWdDjJIx2o UYOL0F7KjKN0km27av8AD/hxZJvOiUbHhTap354BHf6e
+  1SOZ3MsVuDdKhWLCjqxGc/jjpTJbqKGV TMDHIynygRxt245H1qCGFVtgrTtFPsBZuSAQDkYH8
+  XatEtL2NqNWT+z6aX1/P8SaYhLQSMsyyEDY rNkMDwTj0FVZLUz3AhjngkIUvHtXBbaPpV6M/Z
+  NLDAw3O9AyLIu7aAcAc+5pqWVzcC2ZbWXKxYwg A4YkE/rRGfLrcFJOF5JaPdr9P+CVUihnNxJ
+  NKyszKck9C3Y+tXIoB+4M13DNFhi0ajDnBx1NOazM EIjkKQSo6LGX/i56n6Vbit3k1N5LmIqS
+  cB14XeOQMe47VFSqrXTIxUlyOXNb+vT8jPjna1ljGxrd cBQ0vzAqTzn3qcajbokiwRo8oz5Tj
+  HT+79TVi4hsZpLRrmRk35V/mwFc5OfYCqJBjiiaG3J8tWDY 58wEffHoBUrknq1qcMHSqtcid/
+  w+8j81JmkeZWtkVCqA9z2xTNQCsVaO6hcunmEKvJAAx/WrErwT 6Zbopw4X5M/xAkAmsqVTHqg
+  j80SqqvGu0fwj/wCucVvSSbvtboddKpCO6tJdOhDM0nmxu8LnzBhN q/dB45q4xS1gYO8Z8wAb
+  tudu04IP50x0eDTkSJHaRic7jkocfMDTG82cMiSeZlecJnfjnI49ua33 S7FU/rFWjfmio6/P5
+  sgn+3pO8UZcsjkEhCN2cD86rvE1tHKjJyjgMGj+4wPANWhc3KXhmyWcxEBi vr1P1psb3N3HHb
+  yyROpBKOQBkepOOee9aJyS1tYxqUqlKVpRjyv1uvO1jOEk6kyeWcs+FG3r6ipT GjJcNvFuVO1
+  FZfmIGDn8qkMM8Cxx3Mi7hJvV1U4wOuPeniWVYzI1utwZCx2bc8E8GrlLseXVxddx Sadulv8A
+  MzoXdQBAX3MzNweQP4R9etKSq24Blhdh8oOPf+dX3tJtjIqx28LPuRto5x3GO1Z32Znj leWaB
+  nEuGKAgA5z6VpGcWysPWgpOKSbt3bHLc7lMaKJY93oNwI7E4zTkSKe9DeaLcRkMu9uWwelL Fa
+  qkErySpLI0xKiPgYPQ9O/NMaMecEMErMCACvc9qfu62NYxc6EmtL99P0ZYQmS8kbIU/MqLt/TF
+  RMhEm7EixgfddefpnHWmGF0uoVWQu046AYPLdfzqyWuHtpEOGCykDj7wPBb6cUr2ehtCpV51Ra
+  vb tfT5oa1wUu4JW2rNj5MJ3qz5swkWZvLWZZN2Cv3OxUiqXkNOwfZJjGFbrViK2H2+JXuEhyp
+  ZzICQ p9xUTUbGePgqsZTdml836P8A4YrynMxKxkAADA/hBPf6VE63O2N2gbBVuR147/StKaSG
+  SwV4ZUl7 ZVcHOcc1lF3N2+9cBWwwI61dNtoxo1Jzpqzsuy/pNEkc0sEUiKUULIAm5eSp/iz6D
+  +tVpJY47h9x S4TfxtHbp3FTQqbi/tzIFmTkSRIMMwA4GfWmQBPMMUdvJkgklz9zGc5q0kmzj5
+  KMpyXNZd3/AJLX 7ypgeXIY1cLuYEkVJ5RSFBt3ZUsDn17U8m1eCJ5QUkYf6scZwfXHtUCwtLI
+  zGTIV87cfeHXFarUh Jyn7sb2+V/k3+Y+a2kKKYX3KuN+0HpkcVUkjeS4llfchMvmDb2rYciWG
+  WW2Cshk2hUHKk84PrSyJ EYTEbaRZliIdw/3mz1xjgVMarXQmcY15OSp38lrb+vI6d7SHyo7TD
+  QqVaSNyfvYPanXl1bGysBIl 0rlSxRZMYy3zZ9cAVopduW2vash3ncWwfn7Y9FA5I70RWwv3TM
+  0MrxwtyF7d68L2lrOfTzE1FWdR WUddHcjtkt9zeS26N3bAzyRnHX6VWY28jSWcbbYxs8pgfvo
+  OrfhVmHzYjDHFBiKMYDEZ+g+tZslz AZLi4gUJOspiQMPuqR0/PNEE3JmeHrTvJNN9nfb1NdGg
+  +0XEolgl2zbIzjhoyByPpT4rNbS4uT5k RhEnIZeR3APuaxwn2e2kmmjdmFwoVFOC0Y+8RW3DL
+  bSa0pfdNESwi+bgqRkZ9TkcGs6kXFaO6LjK avOL5k/QtLNHLFE1pHGxRdgRQMrnk5+lR2y6gi
+  Ncy42wMEK7e7cZqaxh8+SaKONoWldZd2e2Ocex xirssZiuo2ihuFhaPcdzZCk8BD6t3zXJKai
+  3E6sPZXi0n1d7DnEcdmCgfYsTbBnkg9CT3PWs37Je SSW8fk7EZA+9xnIHBxWxN5l1axWyBo5t
+  jqxIyGwB849FGOlQyJLHa25WUPujyoH8Kk5P+NZU6jS8 y6fM6ajDdt79CF5LhbKVUMagziTLD
+  O3HrTEuT9uBQKfl3jK5DA9xVRzdF3McqyqZhIgUcSKBjcPa taIuJDLKq2bI+N0q/LgdBx/nmr
+  klFG8YU4wtJJt+n/Dlbzrh5SEmgV3lU7imQcDrRdyyBF226TbI 9kbIAA0fBLfnxUjG8luXeGS
+  3kVZVBjWPBTuwPvirYSaO6iuI3iuI1R1RFXkD057nPFRzJNPQy5Ve z+S1/PoZ9wy+TdGPy2Pm
+  lom28AYHBHc9cVRSe5Nx9sS0O853HAxgjnj6dKddW3nwxlnMEsiK75PA Zc0kRwIo4nN5NgyMs
+  fGG7Kf89q6YqKj3PWiqXs1bV9d0RxBjGqrM0EZYFVkyWAB9fep7+SWO8naO 43RrcRuETIbkdM
+  +3f1qO3ZppVkmjeIhB8x6M2SRgelXrmQhbi7URyvK6gbE4TgcH3obtPVf1oaqN p2klrte1vvE
+  txOtzG026XClJSO8h6Y9OO1X7OO43zJ5f2iNAE2qOR8p5PrVK2iaZWil3gSSB4W6f KvJ/H3rS
+  WZIQJwzrK0W488NzjP4A1zVm22kcs6kpScUlf7whup4ESJIpZcRjZtPMvHLD6VYiurh7 ou0kK
+  Z3YLJw3HUfyrPE48kq2YtihWJ6ls/Jg9gfSpAgnnneeVYTJJ5hHQRYGNmPWspQWraJVJJNy SX
+  9eo6dg8MMkrfvYkZJVXghm5/QVDDO7hI4Wh+V1L713EHsPxq/bXNpsaCORGaSMyOG5YN1UZ/Cq
+  DS77dLown7VIELqvAAPXj19PSmuzRM4pq0ovy0/rQtSC9innaW5tZDHcCIrHHgjcM4rNuLq4Mi
+  QR RDlWwSucAHk/X3q3vgneQhJIiZlU7myWPY/gKyLuC98yVFdFjw259v3QDkj8qujFN2f9fcc
+  lOEHP ldr/AHfkZ83kyS284mk8t4H80lzwM8fjV03P2W0E7IArH5iR17ZH0FVXgeWGHLI0Usys
+  MLjKDqR7 dqnuDK13PIAttDubJmXcoZugx/nrXa0nZMIwhPeV7f1p1BbxFO/7QssaZT5f+Wn+2
+  PYfzpEnSVD9 pmQ4kR1C8MFxzk989Kql7h2uFhgUOR8yhRw/YD0759arGS5+zRo9sdyxKJDgA5
+  BJ/SrVFM66WGi1 yvWT81dG5MqRXF00RyGlVkXHRgOB+IrLB3zubsJamaQ4Mg+6wGMH371VbVS
+  u1FjL75hJKxHH1HtT bq7u5lZCglgMzksiemMNn0qqdCa0ZcI4ty5ZRS83v/kTxQzLE8oxLJlS
+  rJ3H8RHtWi87NaSGOWFX UhQdvAz0P41nrMv2WNJw0blHLyA4UsCOAOwPHFSfZvNt7mUSAxyzI
+  Qo4IbGMfn2pSV3eRo5VKslU qO3S7XYcun3P2ppPOjJiby2Kg8secj2xTjp5JnzcKkDIXwe+0c
+  f41FcpPZ26qUuHXd+8YtwGGOtX p4FZzbHcjSTtLhj02gHFDnJWfNv+h6ClXp8slPR6aJFWyhS
+  VTunV5y6yjaCFCD73Hv1okjf+0DLH e25jD4UKCCw6g+9MgvLgXkjwiCKSXlFMY6dx9Kcy3G2K
+  eV4gpXcE24LHHOP51UoyUtX/AF9xpUw7 jUak7X9L/kW7ZFF+u5M+ejSOq8c9AR7d6ou0tjaQr
+  9nlVhhQ7gNjnPNS20aNPbubgAG13uRnjnit BkV7me3Eocq29QfUjpUN8stdTWdBxnd2aS6r+k
+  Z0qbVmBkimnnl3bsfLhe4HpUqs0FnFJFiW4jY+ Vx1TrnHvzVoQRNbwyXPzSJExJU478n6Ypsc
+  0csEiCaIlySkm04CH735Yqea62MlSlV5U1pfXtb03 M2RElTfMHDMf9Hx3TGc/0ppeNLW1RYjb
+  mVC8zy/MpOcAgdsCn+Xd/unaI3VurAKsXoBwc+lV7iDI Vmy+CJCg67F7/SuhWbs2bOz91v4dr
+  flb/NE9xbNEI5rrfLAqMigNjpjHPvVOK0WVVE100crksgLH CAdc1Ok4urtsxTyxGTzUi3fe68
+  /SqQMshlgwI3I3lWGScDt+FXBTtZuzMFGrUnaUrNden3bFqA87 1PnKSN+85ByCB170yUKWidW
+  WcxwiAoP+WjHkMP8APakniWHTrCSK4jEgiI293JORU9nEJraBhhZZ jvZz93cCen4DH40m0lzH
+  FOUYx55yur2/r/gEQQW908+Y51yy/OMkn1GegqOK3jfzhcNLFNGpDHP3 8n5WH4GtWWS3lngSB
+  02yRtKQRnYc4Gfp3qvFbPbLJvjeV9g2yBuGAyTUqr7uujNac3zc2ql9xYEU cUEsVvKkciuBKs
+  vzcjnirqXDSbHS3n8wNmNlOFVemCO55rMhmRmU8FWIKKPvKP8AaPc571ektWUz SrLtnWRfMPR
+  cA88Vzzir+8TUoc8rS/Hq/kZ9w8wjQ7HVo4yCZOcjP9DVy1uZri+dpLSaOUk/Ox+Q DHPHr6H3
+  qyrh7gwzzQMGYuWC+/8AKq1xLN9te6kjZkMZA2fKpB4/Q0X5ly21Eo+3vSsk1/XkZKzx rcx+a
+  CRIQVJbiMkEAn6d/Wr0L3L6itvbR+YsTbJ5Qvyl8cAegPTFWIms5dFUtA/nQMICM8kn+tZs tl
+  Khe3t/OEzSDZ8331HJP9M1reMrpq3qclSEailzaPbXXb5oi+0+TfszW7RLtbcrYO3jCgVbZfPh
+  iVkEzn93P5YwUOAP/wBdVZEtCZJCXWTzdpSRs4BGcH3ppmjmvnyJbYNzIhPIfHB47DFauKeqRN
+  aE J01Kle662/r8yMmC3leIxuJ4wUbe2RJn+ID6VVSFzeRRorSxyQlgyenc81YjgMtteebNEhW
+  aIq56 learyLY29w2VnkiUOqsr8fMOnTtmt4vdLc6FOdOLjF697f5/oTx/ub2ONo9qqDlnAyyn
+  qQfaq32p RdAxWxlzGEAU9Ewcj6571CkFxFbo00UuyMlWOcD/ACeKvwpbSWpkKSCJ0OQDypXqM
+  02orV6nLyp+ 85N303/4O5WMM1vEbgKscRjZAJl3enHSoXnuXcweV5sMbFMImCARnGce1aEUEF
+  zdopnJjERY5PCq BnJqhNBcIlvcvFKF2byduATng59OnFVCScrPc0aVWTUtZR20t92t/wAwtIF
+  lZIvNcrjKtu4UkUGF fteLhTKYh+7CDbvGPmPvg1Iu1pPMLfZpwW3uT8jEjjA7Clhed3sY5XUR
+  SMCxZeWK5yoP5UNvV3PL rx5FKUtu3Vej6/eRFriC3EoWN1kQMxVeAM9qYIo7maJpX8hUT98c9
+  WPTHp2q1ckwtFIssXnoDCyb crzyePYGhrUPZRyJFLL5kRyUYcso4/nQpq19r9TSlWgqS520pa
+  XMv7NcxiNpYmTjHJ5OSeR7VItv LA6wNNHKsrMEAXczlecg4zj2pryO/l7kWInr8vOPX6U62ga
+  e0ZmUiQSkDH3l2jJP8hW8m0rtnTXi qU1epf5NX+5ldIo45l88svTcQMHnn0qz9oimjieSCSaR
+  VfZGBgqBwNx71F88l1byMARMNxyOG5wM VYW0NybgF7cRQO2XRApfAxwfTPalNreTOPHKlKa12
+  6X0/C5meT5LbAGIVG6fxd8/1/Co5pxHcK5M W9n43LkOcD+ea0o5LZDI00ckiEDABwckc8+nbF
+  VSscrAgKqEqEU8sB659q1U9dUViW1C1rfiQxPI kgSC1kkuTI6h1bhOOmMdcVVdQiWYMxilkyZ
+  Cc4Htx61Zni8iVUeJ0Bz8z9x6023k813liSPdG4ZR ImduOo5H8qtbXRwywzmrKV29kv1fYI7V
+  IUKxxzuWJwCclW7Dp25oeR0kVUaFlEyIzbcjHr06Vaju HkeNZ0JN0jyKy8BcAjn8qht7tksYE
+  PkyKExuVB8vPfI5PvSvJ7oVKc6kHTi7fp+f4Iu3NusF28iQ uxjuTsWI7Qigc7vWiEIL1wkQJz
+  x5w3eYOuRntViFx9kObK7kvuszE/LgHOMfStG0vtOvpnikKwRS MW9CvbAP4VxzqSUdr2PPrS5
+  IStd269fuTJ20/wA+KeSMTxgToIwz5x/ez6nvVjyEiuS1is0oQneF f7x7Eegpbszx6a8TSLl5
+  MqyDG0jBP6YqvatcHUg7zxso+VgoxlWPP41x3k43b0Pbmp1KKm5K3bo/ n1LF2LuWFGYMgVWEs
+  YGDvP3RWAkVxJdslwoiRgN4xg7sYX9a2ob6KHU5o2indMjcS2clR8h/Kqtz qNzJayGPyTHs25
+  MfJJ6mro88fdSKoOvCXso2Sev3/eTyxKmmOyrLKN6RFlbo38Q/E1pWV1KqxrFH CrR7UcMuSpJ
+  4NVpJAmlnzEJjWVUYDgqxAxn1NWW8kW8CHcZEUiQo2N+T/hWE3zRs11JiqkqSjP3r N7frc1mu
+  DatI0sLFA21mQ7RyvBHtzSSWxa3ha2nfMcJCB2LYIIwD6k9qzxJpiy3UX2iQAMBGsshO 9O/41
+  pRpZyBZ7aR5FmYlsPwmDjafc9q45Lks7P7i4KOHs1e/mrp+VtSC2trh7xGuRJbupwct2I+b 9c
+  Va81WmjUQTTytEVKo2MkenpxT2uf3UipaXEqyzeai7ucLjP4U2W7hMSyR7djjfIy9ju4APbIrN
+  uUndo6qdec58yX9fmStBsRirJbRmRfKEi5/dnjj8arCZ01Z4J5AYY45Izle55GfpV+WQTrLPIP
+  LP 2gPEn9xc8qfXPUUySdFnmuPKA3K23f3OcbvoOlRGTe6IpVXKUkkn29fxZBbRrDaefI+CWCy
+  N2zjl vwqq0l9bm0W3YyKvAkA4ZTyf06VoKIHtImZxLtUCQKcAMAePxNRyT3H2EC3lhEhG/wCZ
+  M+WccqRV Rk29vvFTlUk0uX79v8xJC7adIyTRNG1xvUleSB3B9B/SqbanapKiBo3kZQymIbd3P
+  P5mponme0Y3 AUxxNsUouAAwxt+vepDbW9vbwwuI47oKCrOuf3ajDfzqlyrSWvoOnXcFyyXXpt
+  8+pBI0bA3KrJvZ wxGeDngkewp8MtxgRPbuYSjMCV9DhR9apmWFbuI/62NcIqqcbtx6/lVp7+P
+  dMlmrsvnjgtnJHce3 bFXKLta1zonUk0oxV/PoMLXSywuY2TeuenAwcfgKngjuGinN9BIio+2N
+  AfuLnlT7niq13dXjXrtI 0UNurHcCnVgM4HpVjcGW3lmvI4oJ4iy7s/N83B/MUmmorbXsNwcIr
+  mas+qv+HmTBJTEVQAhpFYBu u3JLH8KssYPmMUm6AYIyclgenNYJUhGmaZ3y2x0UkY+npVmG6g
+  VwFxIuCSA3TBwv50pU29imnKK5 btfcarG1P2mQwyF5HDbUOChx0pjWcq2R+1SqpAzlRjcR1I9
+  uaha5LzRAhQ7IUkwPvNnJx6cVfuby ONViWGV4JJyXLNnacDCfU1zvnTSRy1qzjaMdb767GRbx
+  Q/apgJt7u+8c/eGPvD09KazfbZlTeYl8 plCk5LHr+dXfsokmmkVTbyqdqo3XCnP9arXVyrzyI
+  1nIyySeYRFhWBPHB+tbqXNLQ7It1GvZ6+b6 f16lcwgWmWtLnfHLkAPjI9BSPpzyXdribZCS7O
+  kmSQR0zUUwmgWKSRZYyrKz7zwSM5P0Gal23J0u RzFOGFwoikJ4HcA+ua1vJK6ZzVoWio3Su9/
+  6uir58pZWR4y+zMhC9Sc4I/AGo4p5g0QHlvHMu9iV BMYA6H3q41tbTN5k0m2RH5CkjJzx+B5G
+  KpfYIbczCOK4TZK6sXkznI6fritE6bViJqjFcqWvf+rD obeGOS28wxGQRHYCox17+vNVGsrae
+  wEkry28u05+bjjq2PSlkKNPaW7tgFi0ZU4MaJ2Y9yTUn2uy 3RII5C85M0gZujA/dH4dRWq507
+  q9/wDhzOnKaqLlb/r1GRRQK0kC3UTIVwHbkMSOMfU1ckV10YQS SxyCPB2qMMSMFufaqjTBY4p
+  ljRAsLZ44LMeo9hxWjA159iX5I2lDqwJXIfAwWHtipqN6Nnqc81yu VrX0vpr62K9xcw3ssCsc
+  Kys2A3RCe/qRjrVQEW+o/LJJPIjkZJJHsamXEksUVttMqxuoO3ONxzTE ZooTaPEJLoRlVOBk9
+  8/lWkUkrL7v1PSwtFtS5tF5vRee2w2V7YeW8cEgmBKyuD8qZ9vpSxNbtqEY 3v5aj52ZuN3t6C
+  nW4lcxSgIZtwwNowQOScfSr9y0BaCVoC8fzNGUwMKByp9T70pSSfLv8x1HTpz9 lBuV+qav+N/
+  zBNQiexfEItnk+b5wMcdMe3tV0XRigt3keCKWORY3kZeGBGc/hWdMI3t13jzYyF8k LxkD+H6m
+  nXMSy6WgKEzISzx5+ZeR/KsHTg7K24pUKdVQppWTe99vX+rDrpJplGyaOUFvnSNcEZ42 fUjms
+  hkMaqsSM4zt3A8Dnp+tW5wy+Z54YwiTflDgseOR7UyNZRfNLGVcIpZhjAYnoR7V00/diejF ul
+  Tave3b/P8A4BYlldknR1aErMFUrwFQevv71nRXNs+2KUmMIpVZCffgH61HFPLcRFpSoKnYWHR8
+  HP8A9anRR2huna5lAD5dgvBVjwBVqCimn+BwVOV+7q+unQjheOS9cM4ijLEvjjHsKgkEKNEY98
+  i4 JQ7sHHTrVtbp44CIYoisbKFdkHfg59SaaGLCeG3VGjLHy3ZeNo+91HarTad7aHPGo/ae+rR
+  Xn/wN y7PPZtYRKE2RFsBm5wx7f1pPsot0jlKSi3gBjdC2CHPfNZawBLRJBlkYI5IPC84596uX
+  OotMfJiR zGjYVs9s/eY96z9m1ZRenUiOFlFpQlo3rpsvJ9yylkY7aKR3E2+MqNgxuGcZHtU3n
+  pD5FqkckZfc FeVtwVD6/wAqoPOHW6PmsrGQIJM/KFPJIHbOK04UErTSxIQ8bmJd43bdw4z9Ky
+  mnvIxqQl9uV332 X9fIdN8gaVFSMwusakjgA8Ekd8VNZ2TpMZDexM8YKcqSoAPRh3JzTYWsprF
+  rf5p5mYeZsfHzAe9V x5hurmNw6AuquF4wxNZauLV7HZRalSdp2a30Wq+ZbmZWJaSF4XWRYpG7
+  euP8+tVZ3sriGeTz2kTz QibDjaD2PqeKs3Jiud0ckv2cRNtLseGYnr+H9agnGLSTyZLeXzyp2
+  Rrj5s4/PFFPS3QmnJRa55Ne m33hKtpHZXDW0qguWkTJJ3Dpmq8Nve/YUWM75HK7GxnpycfhVp
+  ZJhmCX7OwhfayGPBA6Af1rOLzL CD5oL+WV3A4XceBj6irinZo5KkajpuCVne+qvdf16i3VosS
+  tcorRF2O3zGyCuRzU4uIobwOLOS6W eRpMpjlV44qCG1dpopi5ePyismTxyMZHpUgh0+wsVi8q
+  6uFDEFll5QYyT9DVSatyvV/15nnYh6Kn K7b9V+v6lDZDPKfJPkyGMsfMORkN1+mKrTLCDapeI
+  8gt0JOzjfk8H3xUksTS+SLWTyT28w8kHrz6 CqUsc4d2kJkWBvJLjo2eRXXBJvf/ADOmDjWSg3
+  a33r5/8OaD2zGOSSecLbhhtbJwc/1PWnGOFtPV IGEMbFgN7Z3E44/HFVAssis94ssUEkg3OCB
+  8w46VNeQEKypGxRgfNXuHUcfSptqk3/kQqicopVE2 nppp/wAOQxW6+ckUj7ZGQExr97jqPwFI
+  sFzOVti8nl28ZAIb7wOfm+g4pZEjZU8y4ETOpd5Sp+Zi PlIxyAa0obbyoGhKyXC5O90ONvy52
+  /U1U6lle4sRibtSk9Vt0t5roZptI0trEvLmJod7PnIJU9Px 6VDHJpqPIxt7pdzEqjScrkcVPH
+  qTq9mPISTJUKm3O0E9D6k1LMbf+2Z5r0qY4nMaxxjBORyfw4p3 ne0r/JnC5TlL95e/ZP8AQyU
+  KTwJFJgzhVjjPqeevvRBbpcrAju8PykrubhT3yPoM1cWOedA6w/ec EsP4V6Z+uf51VYFIgY1O
+  RJuifPCqOx9Tmt+a90jSpJykowWvS61v+IxoHLPtljkJnTy2A6oQaka0 uo9NmkmkSWdZlVtg4
+  2sOB9asRpLLaSTDEfmOWjGMZUEcj2qpLBsvJIGuGiXcGdmBIJHOcUKbbtfY pTqS9+Mr8u+5ei
+  aOK1jXymd1DpGncDOR/jVaRrdUidQ9wTGfM8tyASep6fh9a1khOx7hpoYopZPM Vip6D0rJ4tg
+  xkuIZFOGARcZGc+lY02m20edRjOabir791fy/pox3tDFYRMjOA5PDnJHqfpTgYvLl NsWlIlGG
+  A6Keg6dfWrtoxt4rm3WFw8rEuZedozk052aAXci2f2aPIHIHGR/n867HUd7Gnve7Buz/ AA9DF
+  IfGzeHYEgMRwQPT9at28ai1t/OubeBZFyDs5AyRzjqanhMFvcpFIiyMFOeenrxioGZHuS3l +V
+  FvK+W3JUVbk3oVWpyu43aa1utfu1f4kZuIJ9UtApKQRK0bNz8p5NXYRb3c9k0UkNyI90bQwxlP
+  MYgnIGO1NWJonUqkQYHKRPGDuUckn1NXw2kO7y5KSGUBvKGxcHqwGOlZTmraJ/1/Xc8tvm6t/e
+  9f zX3lqxMSXFuSGtnW0KM0rZEhwcN/SnW1ktzoUMUM1usYGclPmPrz7VZSOyWSTdcpAJGKIXO
+  d+e6+ wFNjkBeKOWNsRv5YCHGM8lfc9K4XNt3X9feYymlUVSnuv66mtFEP7NdlImj8xPqBu659
+  +n4Vm39t eSSEJGJACSEjGGGTyCafEGTQBEsjHzJVZZB0XB4H51PcKyyN5sc5lkyzhHxt2nkfU
+  iueN4zuenT5 FG8tf0/JL5DYI1huhb+WSHYlZTyMDJAPfmqqRI6CWaKQ/LtkC4ADNkgVHPbm2u
+  Z1ijuCA4VAWyT6 H8M1AJY7h5LRWeBWJkG5snco4z7DBreMW9Uzr9i3PmjL+vk7k8MN1E+5raa
+  QFgGjByZGXnePatSz uY7uVhLEYysnylu67Tmo7QSg4huo3llkVjkZ2gj5j9KZme0vYt0LOQpV
+  FUD68+uKyqPnbXUmVR1p uKav0eq/MtTMnl2xtI43X7MWQsuT14Ge/erfmtHAySMiuyiRlUY4X
+  uP60sc7+cTIq7mGI/lwpB6s PQU+SS2lEiSssksUioqrw3TOPzrlb6NHdFctoy1a3t+pXnXy7d
+  ZYZZDJIhZzvJA56D05q5BbI1zc yvBMTk5Ct8uR1OPYVJFLJCt280tsQs6CVdnfGRj0Ge1W1uZ
+  Jb4s7xDefMZFXGeOR+P8ASsp1JJWR NfGTpwdr8vfuQMi7j9n3tFMyyYB544yPY0k1pIb6eRm8
+  qMy+ZEr84UDG0/jVea5k3oxeNV3CNFC4 JUjJx9e3pVeTU3kDxxwTBWyYtzZwgIHP40owqPY5v
+  bOony9tf6/4Bf8As7BlAilZF5kCtjaw5AP8 6RlRJDO0MyQMgLOW4YscAj2FZllcLI8qxJcsRO
+  cAyZJHp+eav+YRFbpOSIiCzZP3eeAaJQlF2Zk5 Si/eV/K5Dexf6KEVZGdZU37WxuK8n9KgnEi
+  yXZF7AirMwjdxnCsNxXNX7m4Riw8+J2dGdNq4/wA5 7VmTeW2kh3+duQFB54xyfp0NaUr2VzWj
+  OUeW63f9biqYmhmWJC8wKsIgeVU8n8utXBLaN9q8xGdW uFVWi4zkZH51jLOwvDIhUF4+FHVxj
+  HFXvtVstqEit5TKkmWG7+7jH861qQd9jpxNSTmko3Xr+ZLc aj5spgSzmkYksy5B2k+tMedo1i
+  geEhdg8hXGflHf6ZqQPpxvXOJSwASRlbG0k5Gfesl55odTEk2W i6ZI+8RnAX05xxRTgmrJWNI
+  4mM4unGNrd3+Wp0Fiv/LScxqRGcIf4euc+561nPArmNtwaOBEibZw ck5IPue1MkdjF/xMNyKy
+  AyFDtKP3B/SoN1gyXCxXW1XuVl5Y8gUQhJNv8loRh6tVNrX5amwypp8i ThXZfmBRjktzgMD+N
+  ObUo7C3ijSB3ZGbZv5ySMgnPXFWDIJtUuwAGi83ace68Eeg71Nb2CyRKuEm WKMRqCMk5GS3Pp
+  XJzQ3qFUVRc/36bt+KKEeqXJsre5up0YL8pUJg4J+YH3IqWI2ss00q3MSpx5eT zuB+VfqalWw
+  tG+zrIS4dQyYPHTmpZZbVIJUhiVptyeVtwAV6v+PvTcoXtFHVGpS5uSMdX91uhXFw El/0oK4M
+  RMiY5BPBP0Hemxq9vp3lR3KTRcSEHJJYdSD9KsyXCPfzeWqMjLI6naOoxj8O1ZxglujI OI5tu
+  fK6EY6n6dqcVffQqph41FdaFYPM175gMcls08ZVlXqOauNEsl0yAPJEZW3MpPyDqFb1YnvT BJ
+  bCSRUuYflJZVA7AcEVkOb6K4MqeaFypUn7uMcg+/PWt4wc3poP6pUqScnaL/P5D5LKOXUmhhkE
+  Nw0e5/M52Duv1NPMv2ey88rFPAZVYqqjcMcdfQmtHNw/mPbIiMqMAzjJxjr+HNZHl3Bs9skkdw
+  fl 3+WuNoPQfX/CtIy5/iZz1E6sl7X4eu3520+8ry3P2l5GlUhBnMSjBBP9KtrIqJaNb3IjwCw
+  3nOV4 yv1p52R3bCe1ChG8tJccMpB5PrzSTWMccixGZXk8psKowRgZ5rTmg7LY7F9VkuSTcbbb
+  Nfht8yRd qX9zcrnyFzhwehPO0+9RNdxvBJMtu0U42tuf64qujy3Uc7rcwNvwWjCkE8ckfQVfi
+  ljs4YpYl/d5 KAuN4AznB96TVt1dm7lF2Vm326MrXAnlkeaOMZXcJ0Xgp6D8qglWSVLaNZBhY2
+  3nJ5yM8fhUzwwr M0yNLLFcO2Nr8AHGAfU1TiufsrlbYbnUmMhuev3h9T0FaQu1obQxcpxUVun
+  8/Q05lgXTwWWQHyyY WzkEcc/jTIr+CyuXuEPm5UKQ/O7I6j8arm4laSziWIYjhZTG/OFzzn1x
+  1qGCJFspo5IWaNJE78vj OSD6dKSguW0v61D21qLhNd9L7/16jzPKb1ASPO8hlbPQHnIx9KbFJ
+  Hc3QkkZhEYdpCnHIXgfiaqO rQ6hHNKd0bkPkHG49OPxNRRlrd5zEDGsUiqyvzlhmt+RNaDliK
+  krwpu2nf8AUfLHJHbwowaJEALM e7dcVYN1HHHDJbRL5w8xZFkXOAemf1qBWeRvOdWl2xjcM9e
+  aqossT9dyiUZcjjHb88/pV8l9zlq0 Vo6i08tn67kqvcMkk7cmOSPbtX5cDPBHrV2N3RXkI/co
+  5jRRwSXOc0C5EcojZQgRiJkK8sazraQS 2rT3mIyS2yLOC2P/ANdTbmTujKTppPmVr7bu/kX3d
+  50SN7aUhJQFKHAPv9KYDJYXjlkATftcMOrD nH41H5qoYnimJ2ybio/iO3j8B3p0SyXhWSd+do
+  BA6+5oSstdjKlC1+ePuv1/zGRvs0+feNs7SAMj fw47/rWhDewwh5JEkNw7Kw2tgHjb09ealDW
+  cLw7rmDAUq4aMkliMAmqBjS6vYVKmGQJjeDxxxkis 7qd7rQ6aNSNSnJSg+VdX289vwNK0t7dc
+  TwS/vYZChJOQoGQSfU81YbUbq2kh2wLIuQGbb99v/rUk hjjsWihhfIYq7g8Mxxk0kNw7xeVPt
+  RmJbzCML8pwcfhXM/e1auZ0o05q8ld/p0sTeUhuGcoFjKEs pOTnr+dVmCMYQ0qJC2HZFyD7kH
+  tg1PE3laxczFgI1Yj5uQuR0I96alz81rmDcEjO7HYvxj8KWp01 IuS5Vt3Wn5kk+oRQ3IItjIj
+  4dmGMt2Bz7U2OG1ighFy6hZk3Akkb8N2qEQfZvs8T289wAGXKng84 P86tF4L23isjZ3MXlkx7
+  nYE5Az/SpfKkrbdWcFSrSi1ON0utmR3L+XbpNDl7eQCVwvXcpwAD2B4o cmbTkYbWYxu0qY53b
+  h/L0qhPJcWI2grycmNhk5bByPbtWebkkho5gJG3BlAI2ZOOfwzW0KDaTX3k zwqlZv79QZ8osi
+  RyBpCDEM9s81auppUgYxRYRZf3pdcjfVqeSzW4iiimjZEcbWB6gcgj8KzZ7mIt GQHSKeB5CXO
+  QWz8v8q1j7zXumFPkqVUqkNPO4Ti7lKO21YmRn2sOqnrj8amsbwR20Fv5TSyqNif7 QOfm/A1F
+  DJa29pDvZpLlvlJLZCBhzkVJO6GKJbcxstsuNyjBbn19x/KqkrrltoTVXtkqcaeib6NW /wA/v
+  LEEVlsnQt9qKg+YU6A4+XHoBUCyz20dvcWkgLtBmVWGd56BvoO9NLz+VKwCW5UfL8oyytzu /D
+  +tN2Ty6MS0L+bGxRCOAA3LA+tTy93oZum4uKqStF6NN/oE0rCaK4mkhOSGQImAyqOv4npTLgCe
+  yile5gVZmLFtuMMM4/Oo4rO5PlxFPMETbAmMkgckj2GasLbWEsmFkwypnlsq4PO4e1W3GLVnt2
+  MH JKakparsk9CqqqVhRfMk2xlHkjbAZ2OePYdKt2/2lbG5S7twUkuIxKdg/dE8fh24qVI7fa5
+  QgtuU RgHG9eu8ewrOEsyWsmZBmdt2WzjeDz+lK/Pp/XcxqVvaOyaXrv8AInS8SC9uFcgxQ5hZ
+  Qv3u2R6c 1Sku1ujH9oXbtXACjBPH9akltbZ7V5beR2LSMwQnJYAjLfQc5q/PPbwaXNPC1vPKz
+  EgBfugkYH5Z NXeKaaWr0CdSnzx5U3J6dV+X5mUlxPLLJC6OsciHykPO09BWfJt+1qJIZNsJCH
+  J6EdjWje3UjmRB GkAJOH24Z+nzA+gH86zfKcxOqiRzJJ9oXJyQRwQfWuqnte1jatKVrRjZev6
+  6E1zPFJLKodnmLk7l PHQZGPoMVG8TSwW1wkihPLYgE54B4J+lQuGZFkmVVMkZIAXBJ6A0WUcs
+  V9bTsDFD5bsA4yGHTp71 ajaOj2OWpdQXJq107+X9IuxJNcaTl2ijkO5i5TllB4YH0xWbM8TR2
+  qqNrhHyeMsO2ferZmJt43Id Sp/dgHA2n7yn1qi0QjhEKrnOGB7r360U1rqcmGoVXa8LO/f/AC
+  /Uv2awCVVd3jgCtvZm7kcfhnHF XEjtn+z+VtOUK3Hy8I56D8ax7ZjHOrPIi7RjDISMHnmnzTM
+  1wHjDRMwO9ccEf4980TpuUtGddaDn UbgmrLorK/mdPcxRRwx/aoXJiUoCDjA43fiOOadKtt5M
+  rBJd7tkYk7ADmksr6O5tEieeKQRRFSGG S5PJbntxVmLUIrS0vH/dyTb0c5UEcDoPrmvMfOna2
+  v8AwTx41qqlyRT5+urtb7i7ZRBoTBHJH5DH JJ5JIyVI9OaqzXM8Ytp2/eTFFYvj5Wf0/KpZYo
+  2DkrKBHKRuR9oHy5FQ3E1obaPaxSTH7lGbJCsO p+hrGGsr73PTw9V2va8PMoz3krS2txcP5i7
+  d4CDBIU4B/XP4VCbi5M8SW8SlpJciXaMD2/GmTwSS zW6Rt5swty0hTowB4IHYVEzs0aedbywj
+  a/mnOAGP3f0x+dd0YRstP69DrnQpcqlbX06ely8zSxpN HvSFtwk9Dz2/PFPmWRoEnd3V0VhKC
+  f48gAD061HbXGm3NpFLPHKobAIL9wMY/SrP2GUWgkhJDuQz Rtyc5/p1NZuSi0nodeCr0YOKWi
+  v1W50Nus1vbODA11MisSq87Rgcc+9QwlNyugCXMh6MM7Tjv71U Nw9oywz3CSCSNpHdSeo+7+B
+  pYHk/s4eXcwkD5ZAyZZXboufXFcDpuzfcxSUbycd3uXdQMk1l5bTQ KLn98zBfvbR1Ht2rOhju
+  0iuD5ckvluoGDgrkdD+dThmCeUVwyp5aI3PH94ew709lkguYYTKoGxvL bJwyqO/qT604Nxjyl
+  08RKlT5W+byaLVjcLbzSSXTRSKVGeOrEEDHpUXl2Uott0U4ZIwku2TGw5OQ femRW8XlBp2zGw
+  LKAcFSPu5/Gqf2bfq7QC+jhUBRITnlz0/PrUqMXJtOxPNGo5VFLla9TV27Z1+z BYAswXzDznj
+  Az+NQzQ3rTJJIoe3izE8YGGDnkZNVbotBLJgmZNjeYUJ4fjj2PerdhPDPFdNNcFC8 qyKpPTav
+  Q+9S4uMedanPWo1Ir2kHzX8l/SMi1t5Li+DyLLbyL0VjwVHXH41uzWhN/CyRPcRFZJ5V jOD8o
+  wBTZILe9szNb3KxyOyquSf4u341Pb2VzFZyW7u4aGb7OTz8yscmlVrX1vbyMcZjYS5W5a9t Uz
+  F+0FDE4WMkR7sFB064/HrVdobZ7mI7pZ5XV5MRPgEAdfp/hWpcGOKeWNF3M43lcdQBjj2FZuLh
+  oYdpiESIYy4XlApyQT710QldXWhvGrSUk3dP1HxG6hhRp7WRoTGA7AAfMRgVnSDybmeKeQpsmS
+  Mu /IC4yfx96lee4k+0JCWfcN+M8YBB3fTFPghSXzXWCVw0scoLNnCdwfUit0uW7Z0z5qadS92
+  +3/BG pDmwkEjSkDI5fJyM5H16Vo2dhb/Y4Y1w0uV3g8k4PI/rVaWd7Z32Wsgh+bypG5DqeM/n
+  SW4vJJjL cKyJFJg7fl6L/U4rOfO43vYiNOs6PNL5K+/3G/dxNv2BXSGN9pdTjdznr9Knhv7Y3
+  ds1sszYR9yh s9fX6VjW85HkQsxtmmPztM24MR1I9B2qxaEfbZUMfmKswMZj4MYUHIb17Vxype
+  60+h2U4XptSVmv UvyO6x24jjfYFU7s5zt649OtOnkd4dtmI2lLMSNuSg6AH8M00XLl5ljCvk+
+  Zux8qEL938etTw3Jt rSBvI/1sS4bAwM9B9cms2mrO2p3fVpwjGX9fiPjjKw4SSKQiRQ6qPmOO
+  hHoPX1prQqNQmWRiQjFW wTl2Azke3tSsbpkeKRo41jOHbZjcVHGPqTTRJcLA53xRzrMmSyZ2M
+  F6H61HvdyalOo4tp7/12KVy Y/It7lYGjMcfzPxtUnnBH0pFW8aaWVGj/e7QkTx5wRUF/DP9g3
+  PFI0CyjcFONoAztPqeetTXFoJk S4S88mMxOQCTwcDFdC5VFa7/AD8ypezhRj3lp3T8inLczoS
+  Xy7OGbbHx8o+Xj+dQLGsttFBPcoeM fJ8pJzkE+2KIC8Ee62kSeLaMoRlgSM4z+pqjDLGl1bTP
+  l41/czFegZs4b6V1Rho7f1/kS6cVCXdf P81oLdFo98Ak3bcgHPXBB/OrgmhuJmW4SUOZWdSGx
+  yB0qwkMSynZLG8aHyWB5Oeuc+prDuEhWJwj mYqxbYMkgY5qoWnoZU3DESjTbtbW+paiZreTyx
+  NDArK6KrjLEsM4qKWa5jtYw4XYgQoGQENx15rL WUhxKsifu5AFB53ZGMitOK5vIxMTGWljyrn
+  aGVScYGDxmt5U3F30O2M6lKfI1Fp/L+vuZHIlxHcR hI2KtExRh0IHUioZpQsqExsVVAuBgdR1
+  zjrTXuFGnwkCQSOcyEtxjPCj0zzVqXc4kLRbEjZs5H3W IGAcVabT1R2UMZKcveVr6X/ytr+Qv
+  n2xsgYmKygqMk9hwAf1qQRCOwhmklJaKMhUyeRn/wCvUEdr KY4o0VCZAJWGMlWUdKRz5qONks
+  Yd1yWOcORwKhpXsmcHPSc+Xm0W76/jqWYJjLHFK0BMUb71J5AP Py/U1Uu45VltXT5nZP3qehP
+  c/hUiRTiyaFj8kL4nPq+Dge2OhqB0dfJ8+YSgfM0aEhvzxRFJSujl p0UqjcVa/wCP6fiiwFVL
+  YxRxy5kLBeeWAHQVBGbSWxzJMd0qAgAk7McYPvTIm8qSKRSTsOWU87jn HH509ImjupgqIfL+U
+  7kyoq+Vrr/X9WOmGFxCfLKVktVrb8NSoGJWXzJkaQtuYhetWoWEljLCQrYu IykgHA4xj6Vcnj
+  ki0yA+XBL5n3HRABg8HNN8m2huHQLIhjkMe4njHbPvSdVSj/XQqU/b0kkrtP1M uS1ZVlXEhli
+  nCDHQAg8H3zUsQnUpsDCKC42tnrz1ye+KvzyXeUjJjXOOduN3v9aW2ZVeYQRuXMgY h+e/X86b
+  qScdQnQbp2aTt/X9aEb2Rd7jkMBIN6jqrdqV4DLIdriWRWCtsGMk9x7U6BGgJjYtI0r+ YVB5K
+  pkn9asbA5edQxEqgqqcEZ7/AIHis+dp7nXQq1Ixeune36Ec7XEQBlw5DYyo4JPIFaTWsAgM Zm
+  VbqM7NhPJ3DLfnUsziw8gGHz2l5U9QmOMtnrzU9tZ3Mm9J4wud0nmbeQ46ZNck6uiexyzrQnOL
+  ulbrt+n6mQk0sMokiZVwArRSDcSADg/hWskk8mlQIhglEqpIzKnRh0/D2qF7RxpxWcq028AMox
+  we tTsst5A0EEiQlTuChMEAf0pVJRlZm1b2dSmmrNp6t9hsc9zcJcpHG0VxLIZg7j5QF6gemao
+  TzTbk dHQqUICAYZQ3TJ7nrzT2jleAmebKOSyyJkBVLYIqN2CagySMq+TmJAOwcZyfXFOMUnoj
+  jknLWCSS +f4/oZ8sCzBI4bgONrGPk5wOAT+tE6LFpM0C/ZxiY5fbyTxjn3qIK0M5kZTNFEApM
+  Zxu545qxIft dzcNJG0UDNuZ+yt2Fdeqa10RpiaUozTTuo6303IY4YhcCIqXVSGDA/cPYH15p0
+  oiVDayQFpI/lIX ggk5yPYGojIz237wEuU3yBePnzwP61CxhliLrP5cvmiNg5O47uapRbd2ee8
+  N7/tJpq+ztpf1LbGB pZXtmiaU4GSPlbcOw/OqN0JY4m8sAGErGGA4YEdfc1bispk1EpEyqY3G
+  4Hkgj/6xNW5raGSVFjDA tvkV2bI2g4GfpS9pGMlrcdavGlU5ea/9f13IY5kMc8e8IyS4DtyBH
+  jkfhTbeGK2u5J5DJLGG2RkE AAY7+p5qKFZVlGCnyk4BTO5e5P0qzJdTRG3gkltvsnmEZ2cgD5
+  tpPqexqZJp2XUwxkZUpcri0nvq 9V9w0MtsZZDHL5ynDNu+VvcD6cU+a3Atrie1QyASBFA6gEA
+  Y/OqcV3cG/ZgY2EiM7AjPQYJ+lV7p ZFslkklI3IBEF43L3B9SPWmqcuZXOapRnCqoOyf33X9e
+  ZetpmZreSCSJpLfKTfLw574/lVAGS81K NEkigLtnLLwuQcr9apCO5tUSNZY8PG/Qcrzk596fH
+  O8srJcRmXch2tEdpBxgGt1Stdo54YdTUpJa 9+33ktyluJvs9rDceaR8vz/6vjODUcM0DReakq
+  ZdG8zcMjJOBgfhU0V5J5CM0Qh2KB5hTODjGD9R StEdsUUJguABtxGmDnr+lUnZWZU4y05nZd7
+  /AK7fIrQ37TWbys8S7XAQOmcAdPzqQXsf2gPMjElN qhRjHOTT0tjDpyy+Syt5Q37gMZLHBxUw
+  WVkiWWS1kYjcieWOGJ4B4+tKThrZE1I04Qumtd3s/wAm ZMitcz+bK+yN32owGFB6gVfXT34mb
+  f5DgKxz39vQCqIguXuJoIY5M+fu2f3MZqxcs5smQrITGoG7 OF59q0k27KLF7Kc1pP7t16bGZc
+  281oIVZ1Eb5PzdSAeo9Kz5vP8APiCzDYxz908itWSWJkJjjYbc qpc7hg9OKryPbkp8mOBjcet
+  ddOTtqjVUZypPmla3qm/u0Ic/daaWMfLg4+Xn8qsecUubeRURI49y OzDPJ4wT61Uz8yywlJDv
+  3AFM7fz4p2x5JGW2UyLyxBG7k03FdTln78bP4e7b/LRFyygViiksksW5 ZADjIPf6VrWkcUdsv
+  2dXnDyLzuz8uDk1hRrlkEciuTEc7euAauG9JtraBQUMULKhHG7JyDXPWhKT 0Zy18NVTUYz372
+  aS9f0Okvb8ShZYwwt3jdSgONpJAGT+FZqSXFvpUYyhdZCnlFMuvfrW2tvBYxXJ tHiuplIG3G7
+  Izzwf0+lZ17Z6ozFYIWigEnzlwCdgHAz65zz3yK4aUoaRWi8z0VUUFBW0T30/9JbT X6g89wuP
+  N8os7mPcgxlAM7h7ZxVUS3dwpWWaEM7jgrgMvdxx0GKuMto2lI9zMFeSVEgXPOGGD+oq hIjW9
+  0UM8c/2VigZFxu7H8O1a07PZam+GqXbVN3f5GjbwQ+TJCrLKi3AKhRzjqD9KszmG2niks7w M8
+  sLt5ZYnbz/APrrMi2NdNG+6ORX2pg4Geuw+56VcnuFuDbR21vuuWDArs5iGeB+hzWcovnXY7Kt
+  KFOpfmunvsvvNCLZPcSyOvneXMAhX+IEcfgTmlubaGSLq/zuWkVf4cevvjNUivlRvcNdRK6N+7
+  AB AKk8Ej37VIZbq1DzR3Nu00b4ZGTqOvT8aw5XzJxZzyo1PaqdKXpuv0tbzLcE8EEPmxhwJji
+  ISHJ2 g4BHtzzTL1Un1m2tzcCEWiMskhPDnqCPY9Kkljm2LKkW+6dS8S8YjUEZXHqc0/zCby5k
+  AjaWMhXU r1BX730xWStfmX9dBPlk25tv87lszl4LhIFBkllVgP8AnkOmD/Oo7uSEpKkmyNXBL
+  SgdCuMD1ye1 UzvTyt1tMlqUKRgH5gGH3ie/NUY7Bmw0kwaKBfL9iSpP86UKUU7tmlKlCKvKVu
+  3/AA//AADW85vs 8z+bGYpSJRhfu4OOar3d08ojMBjhaUkPlOFJ4x9e9Ogjhh06CCRZVmFoVkD
+  NnDZBNOWMtesw2g4y UYZ2E+v4c0LlUrmMqkVPmta3f/gEVtGE06NUc/u2Csp6gjP8qum9SaOS
+  C5uhE0k24tkjhOv59qyZ UjsGWWBjcIxyCG/1ijgEfUmpNqvKJIruC2nj3mVZoycgdcVcqak+Y
+  HRdT3ld+aW35/eWrlJpXijL iPzVYrn7ygclCfXHNJFbQQu3mSsodWK7m4LHgVfhmknuojFGJM
+  li6gcxkjhT796o3W9o2Vp0hWMh k3DlwB1X2BrKMpP3djkoOpKbgtGv62K0YCtEl4PIieMhpOg
+  yBjFK0VmNkTLcNGcIHSTAJPAHNILZ rmSCOdxJvy8pBxtccZ+mO1RvBPbX8sjgLFH94kZCuFwP
+  0Nbqze+p1UKbvLln7/ZaL/hyxL5ccYtX WWIJGTukbIJGCcfyp9rPO939olkimjILEovyjPXj2
+  plrEshJguorm7IyAVJGPTHvVwZS3EjQG1yX K7+ijgbTjuetZyaSsdUq1OLtvff+tySC3tFjCM
+  j3LybfKZW6gnoPerQtHmlkgkjdZoW8tFRsF16u frWQikPNAqvE6vj5jnGBnj8OafczTrPb3cj
+  O7NvwEYgnoP5Vm4Sb0ZvBSfvRevTV/wCZs29l5MpP zLAgKqrHkhucn6CkmilN1bQ2ys0UduVi
+  Oc5H3ifwNU7DUJSXV1Mwb5jjHY8L9a2A0ovY2SBgEVlJ z98YySPQZrnn7SEveOx1a1J3mtfl/
+  mNuVNxYeZbJJulO7G7O4NgEj6Yqo8Ust3H9qguI7cRNI8e/ DOBwDmlS+uYdLEwaKN2dXTcmcq
+  ODj6VbW5AneUHzWU5dycqvbb+INSlOGyFGUlD4bp7WetzMN3FK sEcUm1JVOfMJPGOPxNJHNHc
+  yxqXUxrCElAOMEngfWopVWV0W4iMboR5Kr8pZAPvcVYktolt7yWOR I4TIGRz3xyP61u+VKw5V
+  ZWtG93t5P/MhRre0nQjDsy/6tf4SPlAPuRUKRpDFcpAqRtFIqfvRu3EZ OfwHWh9NWW1uZrecP
+  5kgcnJ4HWssSQNdtBK0qROCyMW+97/jWsIKV2n6nHGFKo5VFK7XxafpsUUv pGs5JGcC5kcYZe
+  AVPXj14rUitdqM/mRxJLnmQZLkHhh7Y7Uu6C31GZTbiZRc4PyjCfJkVTleS6nh VMwRbBwx+6D
+  wR9TXS3zbaIbrOV+TRLdsjSzaSaSeW3biQMGXAQjvx/KrE87S6dEfs8sMTFpJSSM7 iwGPypFl
+  maSK1jdYcKyv5vIT0B96WELKsHmXMUrOmDGoII3HFOTd7y6ETi3UVSotvX7+xUdpZL2F IEUxl
+  cxIV5I3fz61OL2VtXcw2T3cTlkwBkEDjceO3rUMsEaahJGjOiRnYCT8yn0z+tMd5I42cNtw 4D
+  ovBYkcEe3qK15YyWx6FWDs+RaND4vtJuQiCQIMMrHuq54/Gp5orqG3juoJUAcZdCuTkHkfWodz
+  JPcfvlgZJFjw3O44Iz9Kv2yOtlDCY2mWWPdJIDwCOorOpKzTOTERcZXsrPfT/MymuViuZVkYyI
+  z8 Ecb8HipWR5Bb7Co25jlyOV9zVyCCW6aZSmGBMiqFGSNuOP51BGlytr5SqMBgzHb2HU/QVXO
+  ntudq qqok4u0l+C/ryAx7LMQrAdpypLY3bieBmoY2YWkqzEhWkAYd9wBz+XGauh45J2ltpDkl
+  2R2OVx0H Xv15qSG12WNzHMVhjdlcO4zg45pc9lqddBT7K34/qVEV8W8brIVBJky3G329Oasww
+  2812HQS3GDn APXA60sz25gikihnQsuBKzZVselWYIGlikiWMo0S9hg/T86icvdvsaVKS5HKL3
+  03/wAhslsyRmUR m5BT5lQ4OQM7h7DvVIIhh3NbTtuOZJEbhj3IyOBjmtFmaEETRTsGbfuVsAt
+  3H09qfGfPs3RsIC67 UA5Xjqfas1OUVdmK9rCKlLXzTf6fqZkUFvI7gz7o43PlFeGcMOfw4q3Z
+  oWnt4XUxmKM+axPHXJ+n FPMa3FiVSFllSZV2rwWXHzEfhUhZEhuBDBOoklDKzHOAByKJTbViY
+  zi7qKavtr177/oTvObi+VEl RLcqGy65OCRgZpv2vyZpEa5E/mb3UpkAc9KLiPmCdXSFnIXbtx
+  kfwgU42luJXuJoJYn+YtGWGVyM fkOtYr2elxxhh3JPl07abjVYzyxfaJEETKXJ5HzdAtLcHbe
+  wQhmQxHbchTguSMbh6AdxVmyjlJgW 4h8hYY1QxOBuBz1P6fnVRpn826LwmJxMqNMw+VMclT78
+  Clf3rLoVU5Ze4nttbb7u4q2NlLGlvDfK h6eYWJUY6DHvzUN1bWSokkxlkUEttV8Eeise571Pc
+  rHBcvcRW0hlY78Z4XHt7E81WhuZbu6tY5EW NTC20MnXPGT+NOLn8SlocVVT91yqPkW+quvuK0
+  UUCTypGssNoVBkeVt2GB/+vTyZp3jtYwjxPC33 V5HOMH3pkEsFjqEQ8uSXEZ81G5AwOevvzU1
+  0JglognjzKdoVBhpCOTj0raV+YjGWhJO9n0b/AKuZ 8xdYYrcW4WURmQM4zkKORVBIzMUEiLCu
+  C4YjAz1BOO3pWg9vLLIPNV0hlO9pv4VYZGPYH0qBGW2v LVJ0aR5IsKgONuD3rphKy03MvbRjS
+  vDV+WuvqwB/cRIJDJKG271ON2eefU5oljaV1Y3KhiMbEz8m e39amLSS6pIYo1ZncSAIvAIGMj
+  0FRnfJcLJco0ZAALAYx7/n+lJXuFTC1oys997X/T/IWdbaPz1+ 0JI4l2qUGNyNgGoYXtIMiWC
+  YiDzFILctngP9K0YkVoYPMntZHRWErCP7xJyo/OlTV1ghJNuhnLDc rqDksKjnk1ypN/OxwV1V
+  qacrfzsjJQ3LXVokCKxjQtgLzIAOQKgvW+06dZtEksYVirFmyE3dAfpj NakkjvcwLKVDquNqD
+  aQSf61TffFKiRSxy/eZTjgY45Hrk1rCXvJ21/4c4K6caqaVmvN/1+BDaWTS WxWY+bcSSfumj4
+  GwZ6+5psZ+zNbmZ40Z49yx4+bgkAZojkuZ4ijzJBMWHloVwQoyG6VWa3lazjn8 xZltXEG9Rwy
+  sTz+taattSZhUqzc+Wc21/Xf9BywRKSJ7gOi4xtyeD1H15qSeSYOBFIkkcb7Q6Jgj Pc8d6Rba
+  eC+jVrWWQRoUlUrnLHOMehqKCQxXNtFImxRGY2OMZOc5Nab67nouVNrmT5rdNPyV2W7i ytGtg
+  ftMqvHPtCsxOF4yCKmF+2y4RYVdTOroQMEKO1UpriRrqZjGrGRwTxwB3pLaYNbSjHlnzv3c WP
+  mZe/NR7NuPvaieHpODcru/fb+vVjb5ZbnU2cs0R3MdwHVsdOKhVjPGipbmUScKI8ZXnkH3qW4a
+  G5uVaAOoJwFLZPTn8qqzwvGINknlhv3jZ/jA7j0raGyWwVHVpRcYaXWyX/BKsoKS+XuULvG4YP
+  AP +FOAE8YBxcgPtOxSBzyB068VJMrOHmkXyGWQY3dAOwPHWq7gM0qYUEys7benTgY9ufzrdO6
+  Odyqu XIne+9/6/UPs4s5BF5ckYYFhvJz79qQW+IzJ83UEDJ47c8VVmtZ4ju84DrkMOxqKS6m8
+  mOMqclvL Pzcgk5B+gPFaqLezIdRUItVo25dkl/w7/EvqyK7heCyHAA56dar4e41C1/eoFHUnj
+  juKaYlmBcRS xmI4f5uhz0/StBPOuRMkcaRqz7x8oyOMY/Gpk+XUyxLU1dpcvc6kzxrcWz28qG
+  WJZEkfqGJHynH4 /pWpG3mQQmW6EkcahNqkjecjJ/PiuY2W4uGihbfIW2qT0bvuFT2nnz+Qt4r
+  CSQnyyoADoT1x6A/z ryKlBWTv/maYjDw5UnLlXV2V/wCvQtX0ofVJsGNIWuNy5TOwKBwPxpIC
+  ks0kk08JBIDDbyS3A/Gp r0RItvard2rBo3D5HKsPu546mn2N1HC9tbmENcrEfOUoPlK85+uKV
+  7U7pf1/SLUlCgqkL3Xy077C L+5nYSmJDFIowV5XPBz6n3q9b6hdNPvhhhwFZCgiG4N0GT/nrW
+  ebiOeG8OzALh4mYZ3KOT9ajjjm nlEgWZ0lQsrJwCAaiVNNXkdE6VGS9pLd97W/QsvA6zSNemN
+  FBAcFfusF4Wli1GOE5V4cnDMXjz8x /pTY1trK9bzZzL5kgePechsjgn15p1u/2zZC9j5mMNcB
+  QFYvgjAPbpQ0mveV19wOlTUX7SDkvu/B /wCZcN/LdWpllChlZj5qDaoY8Y/Gqs63U7Ov2qKXy
+  yqOI1wZCVOMfyqoEvnWPMTwJKPkUrwOv54F OtnYxRhI3JEZeOQHgqp5z6ketCpqCvG39f10N6
+  UY0oJwS/P/AIP3FnLpZKAJYW2qxaUkgMODx6DP SpltktxKnm+ZEHMiyAnB2jkn9aSPUHlumEf
+  leVJtEe9c4DHkn8RUNzFeDTbuJmQwGThtv3s9MH0P TFRZ3s9CasnKqoysk/P8rotR7Lqzd47h
+  fKMgbJzlsc4FURdXdxJE88iZnfd8q4JwfmH4DFEV9Hbx sksDJFISzBRjYRxs+uK1dOv4nRYka
+  2ljhZ0jfYPlj6n8T60pqUE3y3OLEOrBqMY389zKZInupJYy VMPESkkgj1+nNallZLdLIPMh/d
+  p5bqF+c992fyqnLZ6emnM0UzbkXbneSORkZ/lTbCRlihk8wTxl R5qx8NjJBGfX/CnNuULxex6
+  1So/YKdK6e2qSZoqJba5w8ciuxDGXOFzjG3681Qe7WS4SKWaKOJVK NuXkNnp+lPu7dEglWP7R
+  vJPll3z1I/pTIo7e5nlWGEzskhKqG5IwRj6ipgo25mYwownLmW738vm2 aMtwlp5dzgOTHIrr/
+  tY4Htwc1Qs3uprMBFEssMibwwyGz6+pq4bT5o8SBJRbsWR8nb8vU1ammnfT YJY7yxhcKGm2xd
+  8ZA+pHSsVKKVluzCVKlCKT3b31Ms6beJNHIJFbehACLtKjP3T781Q8ue3lQPHN GIj5e2Rid5L
+  dB71ckJuILSRROxMY2RqxBPzcH8O9Thpv7VLjExkLOEIyRt+tdCqSS11/A7KVVRjq 1Jeegrah
+  N50kKQq0pbc6EfN6EfXHFRx3DW4L+TIHiPlqJDkAE5APvRNGkNvFeOkiIQZAucMB0AJ/ HNTmz
+  tDYsHn865Rw7Krnop/ng1F6aW2hs8RF2apvle9no/XU1ofJuMXJkjlmB2qkI28Nycj2NN8m RJ
+  WSK5MMucCRzlZPl5I9BzVW3kl/tOTZGV86f5owozyuMD0zVS5Bkt4AEmSSJQqqzffUHlvpXNGD
+  5rX0M6FV+35ItJPvqaVw8U+mSKsyRy2x8sow6hwMfljmoI2nijmb7VbCF5VkPyH5x0Zh7CpI1u
+  Jw 8pjSQSZlwigEsBgc/WmzRXEMZtoVzeTkO6kZEQx84I/XiiNl7t/66h7ifI3Zd9H+mn3FZ5n
+  GpNC/ +kIFPlugx5g6DHtiqK31vJbrEokMe0ADdnBzjB/CrSytYqktxGZwFAjdejL04/MVntFb
+  QxGU200Z gmREy/Xkkg+p5rqhGL6F1ZRnPSGnR3/4KJ4rk+cyRMzbnBKDuR0I/wBnHaqVxeYvJ
+  HBiBEWxMp1B HQVduordWtNgaVXDiF4zjCDnJ9frSTiOWZllkgtodmI2dckflVwcL3saUKlO7l
+  KNvVf5bjjLJb6T KH2lFdvkK/NIMAZz7ZqlHFFdyRpAkoQfu1lJyHHXPTtipoTdHSrcqYrsvgK
+  iryAD0/H+lU2FtdSQ +WJIZoC3lENhQT1B96qEbX/r8Dmw0qii40klq7vXRen/AACYrE5Bjt5R
+  MHJiO774IyAfU9a2LZok jiR4o4lki3s20AoRnCk+tYAsnRbeFJGd3XzFKsf3Z6EGrIW9e+4tp
+  1Cy+UUPVdwxz/OirBSVuYur Dmi1KasvW/4kFmYra+zG/knG5GuPmD9Se34VYBSXWFmkXCMuXI
+  6AgcjHt0+tWf7PlWGC2lnhIC7Q 23liT8pB9KtRW26/S2urZxIwKySKSFLA9MY4z9aU60NWbwr
+  05U5N7Ja2aTt313/EzpoxPZwmFN7F clRy2eufwFQW0a3EEUbu8XHOW4CA8j6mrMkkdrqfnxQS
+  iFEaNI93UnufpUaRJcqJJpoo23gOo4LZ GCw9qpSfL5BOo5U7JtR6SV2/nYdeWhglaSBmSJ5So
+  Jb7ikc5pli91sESQSyoyjaMdh0/OnvIsUC2 tvMLhUQlJApwB1wcjk1es4C9tBK0U+1kBba2Aw
+  znI9PSlKbjT946IRnSw37zW+2lv8vzIGt7VIJ4 445IZkfZ5bPzt6//AK6beO0SrAhBUph1IyR
+  jFXLyKdlE9soG87XLDJJbrj0xTreBrfUsT27XDoCo C/x8c1nGorcz18jqoVYOldO7XS/6sSEW
+  66XcLLG7AXCiJd2cLkZpgW5aSYxRuZHlaNsfxtnOR6AV phbaW2Xd8iyxCYKTyhB6Gs12vVjla
+  4haRWYAGL5c55JH6VlCV2/1LpxcXeP3X/4a/wB5K8cy6ewR WeVTwx5AB69fypGkvI7RJY/KxE
+  +zOwfKP4c+pNVpp/LaRSJDsx5YB+8uOTTbd4pmjtNs9tFO29Wd s4x/F9K05Ha7RpWpuMbzSSW
+  r6/gacswiR2tLiFpzPtVduc9Mj9TSRtbpZyrOWO2cEbTg4HGPrzn6 UxjErqIpY5pgU8xlXgnP
+  GPTNWVuJWvZGNk5iYliuBken5HrXO9F/SZwVJw5ubdLXsytfXcUjCGCN ss6AMeig8VI8JIm+0
+  3kaNHIURtpw+eCfcA0yKMREXUg2jz0JJ6MO9aM/myRTRLHFIs8m62YgfdHc /XpQ5KNktjr9pF
+  SjFO0O779ejMuG5eR5Wn3TgFclTjGOSK1nure4QrsEZKtkNz1G7J+vY1lOGECR CBm3JkheCXb
+  jFNurNEvI4FuQsm0KAepULyfwpyhCT7GeIVGfvN2a7fn/AEiSLy7rSjJBNnzFXezH IDZ6fl1q
+  nbuzzvPdSrnft3JwBk9Me+K2UMVvpj+W0LQtKMkLjJOMY+lZshimu7mGOPyysvyMfukA ZJx60
+  Qne6tocEcXUmnBrQqBW/tLmPgoWLYGQOScn0z1qrb3E7PbCS2kuH8sSYUcggnBHpV2FWMWL Nt
+  8Ux8+QuM7e5T8ccVqwRv5nmeXi3YhlKgAqoH3c+3etZ1VFaoWJrR5fehr2ujLiuYnla4WRVYMS
+  0JP3yehA6ACssRBruNY7iKVwmd5XOOPm/wAK3J7R2vGEaLl2DiQLwBjB/wAaoXCzC8eNTCzsPv
+  xx 44z/ACq6U430MsNKLctenW2n4f8ABKLSCOUlLhQxQMQo5BHAWlso8kJMXJdd5ZmJDnd94ew
+  9KvQ2 6BZTPCJJY32DGPmwMhvpVUz/AL6znELxIiPGpY8AHqT7Vrz8yaRGJrQqRaite5OLOJbh
+  zbs2USRM k5Vm6j9M1kok93I22PftdWYAc7sHHPpxV1IIoLkJcXiSW4DKux8EnHB6VmxSSiQC3
+  lXPy+YwPBIz z7CtKadnrfzM6cHyySfNpvrb5vf7jRmv2YPlUlZpFl+Ucj5eF/rVBEE6wTy285
+  aRNqIhxuGeWqK7 uX862VWSRXjLL5S4LEHr07U/7SkmntIWId3Mke04wB6e1WqbjFWW559WUoR
+  5YKzejs/+GZlS7H8p YQ21W27mOSzE9fpWj5scek3UMzssq3SlVTjtyfwqoCzR3EoVQfOR41AA
+  9Tn6Uryqtys8jQ3DHcJE C9T2b6V0yV7L+u4V1pHmWnTvf7ixdXcguwiSswD/AHgfvnjBqGO5a
+  DUFlaEN5UhRm25CseckVTa3 gm1GNTMqgLkSHIXjrT98ZeUkLH+82hWHJHb8apU4KNjeEI2dGL
+  5U9+t15vYdJdGRycbd4xICOjdB 9Kngc2kz5AmkjPllfXPcfrVXa20eXypdS4xzkcAe1IrwLAk
+  bI0bFiWZmyWPRe1NxTVkVNtrlXp8i NRJyQuF3Hn2xSRSMzxzRoU8sFf3gyBnt+IqeMSrMVbyy
+  2MFduc+oqNTtZ9kZIwCPbirua+xvaD26 33/DUgkj/doXGWJ4XnKj3OMGo5plggjii2Om5juxk
+  g45yT61JKJiyF2Csqc7uj/SqSzGO3XzmR5n GFwuQo7g8da0jG/meRjVGLTaej0ul+mxDGxZAw
+  lZmxgBzkVOGK3EQgj3s5wvGevHf+dPdovNVAUI ckBkGAB+VJakf2Y0pYk+aVUKcEHHB+laOWl
+  7HC66cbLd6eRatYpRp8scUgOZAxQ/eycjOcVNazT2 rIYkDOYmBQjJGDmlMTSXcUbSJFN5Z3A8
+  Hf3Bx+dILJ1+zmaYW3y9G549eK5nKLvzdQbhGnyyV0+i X/A/I1ILjzTLcTKrIWAYxAL14IHpx
+  WzBJZpvlYmBCPLj8x8kDPb+dYETm2W4gbbIHlRmAGM7c9Pw NWJYIbm8kSOXbCrEQF24w3IU+p
+  J6VyVaabtsv+GPXnQkr2bXez0+W5p2bW6XOwlLto42ZnA+++eM Z9KAlukiXE0MsaPJlmL4Ib3
+  Ppk9KswC4trYJdRRvMSVMcahTyPm/SsC5Xy4rOC0aT7ORukV23HPr ntWEFzydmc0IyqVPcWnV
+  7misksOoTeUgnXG0pt4PBGR7CrMAW0tw8Ql2qgHmM3yjJwePxrKQ3HmK U3PtjKlh2B9at20pW
+  5jgumDBYjgg4AO8YyKupB2PTrU3FNvWy6XEV7Ga8InlGxGEcZ3fdPofpWva TW1vqRcafduGcl
+  sTHOegHTHfPWsW5soxIQq8OS8be6nGD6k5xU0aXBE8TwziFpSR83KFuxPrnmoq RjOO+nqRKdO
+  vB22atu1b5Jq5rXXnXMYtLWKSIxoEBdsn73JqhdDZdLvtZ/lUqGjbCkE8jH0q3bKm 5lluf3kY
+  xO4JHPU/oMVnm5sprsywx3LEyAwoZM4Q9c+prKmmnZLRfqRGtaXLFO0dev53JrtIbXUH uWzEn
+  zKsQ67QAOPcE1nwb2gzH59xErYlbcSpxzx6etblywnaSEoGQSdSv8IAU/nmsOS3NjqTFJ0j SP
+  dsU5O7AxyPfNa0Jc0bPcdCq6vMqkdX6/oXRL9rhPmwNE73CBWONoQgk598d6f5dvHDMiLkM6FW
+  Q4ynQn/GpFtobiwsgEk2om2UB+euAfwqEC0jiUBzcGNirBGxuGc5HtUJrZfcKjO14rm9P+CW0i
+  lR 2lhjx5ZYx7+Qy9Rx3qFJhL9nlnTbGUIRIxtyvPX6njNWZftFxGWhAj85twJ6Y4GB/OqccUa
+  MkM6s 3kB4wynAweVH1J6VMHda7/idtCrzK6WvVrf+vQsLd7olHWWNlTYw5Q98+uKmhv7g3O57
+  dJCilGMK hSxY4DfSqdpFZtYrDPDO1zHkqivg4/iB9T3pF1GzhmzHbTfNtYHfnPBwfpmiVNO6U
+  blKjFxlFw97 5foy5vnXU4pLu5jWO3BikOMByOf14FaFwM6fMyvHctJIDIsa42nI4/Cqdoz+Ra
+  BHiMphdWEi7hIx P3vp2qFp7uCFXlaFsuR5YXDMw4zwP8msHBykrdDkadWrGUZJNaWv2+X6ot2
+  9y0Sz+ZJCgZiVm2fK hHBGPftVZJ4zEhS9t43iiwqkclh16etaXlwvpcESERkt5ZRvmJ9W/A9a
+  r3FrDKlnbugjIt3ZHAxu z/8AqqYyg3qrEwq0XLWNnfW39aleKR5YVS4kTyooWjMjD5SG5FXIV
+  228KI6XMsSSBVTqExyG9W75 rJspoIbTyEXzBNIr7nORH/sn3yKtoGlkRmguJERShhiba6An7p
+  Pc55z6VdWGr6FSjTlqk1b7v0/M mNgzTwLMk8kfl5DRvtJIHX8jVUPdRNFOkscoCZ8sqSSAe38
+  6tSp5ltbhfNilVmWPc2QU/iOPamS3 9tLAn76FAE2sAOu48D8uaUHJ9LjoYhyTly3js1YZNPLO
+  6mSdENwrSJ5eVGc8KPTmo1vdRt71pDdQ CcJh9yZ+o+tUrgRTw4+0RxRW9wYY5MHBU4yfwP8AO
+  p3uJIblg6JcIWKzKBzvzwM84xxWyguW1jvV b2kOTlV+3/BenyC4W+l01ZHZZjHhDtXGTkdPpx
+  Vq7iuRNJB5sUku5yVC8lhjn6YqjLI8sk4mWS3l mYsMnCrjhhj1PalTzknt4re5S7ZlJKoPnz6
+  Z/SnZ6PTQ5HOEUm2rp9tP1/QXyb14n3KZw6bj5ePk wRwPTNLJLNfRKmIond+Qy5Kn0/SmXwRY
+  YpA8tuGGzBcnOCBnjtmr6tNGm1XhnMZcSSKvDt0Dj2Bq XLRS/r/IKtfnUZJ3d9LKyX6FFIZbM
+  QTW8yzEKQ6qp78Aj070+zigjmNvI6WrjMjPKudxA4/ADr61 VMs39nzKWDneoZ04CEdAfrVyJ4
+  rrUZXLJmUNhT1U4wq/UmtJp8rv/X9andKlTdHmqX9V+qelvkSR W1w8UDPIkuFKh0G0EN1NVGJ
+  s7qMzmVwIyGAbB3D7uf51eWae20hYZ03yJgTBePLb+6fc9qz3kjjl mnJ3MXEZicZKkjv9Kinz
+  Nu+xGEhupaJ7ef8AkOt9QuttvKWRyLdspt5GM81MLuZ5EV7oWzOglZ3B OCO341BPOsqiFlj/A
+  HZCxmMbdy9z9KijuHilD26R+azEssqhtoUe/bFa+zTV+Wz/AK/rY7XhIqD5 o+8/T/J/kTW6vd
+  TuYY3ljcrs55OM4GfXjmr95E1rpAleJoiD5QLY+UNyc/54qW0gkMMN0sTOHjY+ XEduMHIHHc1
+  SSyYMs8t6hjLYaNiTtcg8H3rBzTlvojgqV5Tqp8ytHbTd9rX/AEH2lqlxdpmL5IlK gD+Pb61c
+  kuGO5VgmV1G8KrYEY7KQPp+tYSQxwPDbrclopF3Pz0bPI/KiW6eS5ke0LQws4Zg53FwO nOK1d
+  Fzle56sKEaj953fbZGz9pkkMb4E7SOJXSPjac/Mv6UlzHJcT3txLFcRA3QdDu7HsKpQtHai Ga
+  VvNSWL7iNhlYnp+VWbNkt7+RmZYwkZCh+fYqR3PpWbjy3aRpSp06bk1Hb1s/Ly9bGhLLA42I4J
+  b52QHoAvzCqLuLTWIJHt7hICCVSST72FwD9OaZDDBEY0luUmWJGwqEhgCOpPfrTNzHy4ZLiNFR
+  N6 NLzjHQfjUwilpuiKDTTunbqtbfkatrZ2ly8Uqv5RijVCrnPX1qpLMpYj7M8UeTG0mRgE8gf
+  kKUS3 S/Pgojq6ncBgHHfiqj3Ey6eiSwNlduG7Ke2fXipjTlzXvf5lfV3Gq5wmn6Pb5lq1CXF6
+  sUJJSM7E GPmZD1b3x61O1hb2trdJ507SM2YoxIQ2P61SVdU+1w3Vqse2MAqoQfMucYqvPd7J3
+  AtbkzNKMsX4 wB2/CnyScvdehx1K8o1G9LeqevntY1keFCZkZlE0TYWQ5A9/w6fjUsN7I1laRy
+  xGacEBFiOCRnOf pWS75RZVkQRqwVYerIOmCf1NXwYotRgu4xJvSBxvY/KTjpis5wVtTLE14VK
+  d56vp/l3JHjmhinIu ol8yXfFleSAeo9smnwi6k1GC5cJORHk4XkEHBH1zVY3TJEkkhjKYUnK/
+  c7bfyNSJHEjZtJX3JIFj BYncvr+Z5qWny6kVJJUfZyitfx8r6Fa7tEmSbbeQh/NDFATg8csB2
+  Aq5bXAnACouxyG3EYJwDzn0 4xUV4tusM7SxPvEmAVbHJGB+XekYQIbSRC63C24V0DYALf8A1q
+  pvmgkzSTjPDK9/LZ2+4jur9pYE jYRR74lZ9i7djZ9u3FTRzq8RBLiSaeN/KzyM8Efj1qN4He6
+  SzSEQwratl3AJOOQc1EM3Ailms57c ufMjOcbuMcfl+tPlhy6f1/VicXOn7NKCs/l/wPzLGoMI
+  JdsbPCYm2F3bhm6hh7YyMVRgkto90pu1 dwpAbaeRyP606a1lkx5MUxEhOZZDlVOOn1zUUkOYR
+  HLFHmRhueNAoHHK/oPzq6ajypNnLT9iopc1 79rL/P8AMjfUYzpZU/vCWVgF4bA4YUfaPN1mRo
+  mSb905CKvB45I9v8KintFtZLfyNmdrE5GdpHXN Z6SJs8x5o0RbgINq7Sc9G/3euRXTCnBxvEl
+  zpQi7LR/12JLlGNvDLHbTJtXIdiCNq9fxz3qnE10J 3xGJHbLsqrg7T1/CtW5Vbi5ubVJS4ViU
+  xnHHUfjUVpJL9rnhiWOSc/cIXOUI+Y/pWsZ2hqjOo+Wh LnjqvNozXSQpG6FdrKyxbUzhW7fnm
+  s+b7VLd21uiBQgIKgY4IFXkmminhZWRYzIpZCO4yFHtkVGk 8kOqztAP424YbiPb8K6I3XQ4ZO
+  U4WsrrZ3aIGa9ZI4js+QMANg5XvT40aOJ1fazSLvUqOUHUj+VQ IJd0YZZQjKd24c568ZoSHfc
+  pHuaMlTww5+7nP0rVpJG7nSoWqRTsuutvw/4BGsgZvMV4wcgDCHkH r+VPZHM9zuKkiRixxwee
+  MVHYSxQCIXhEkIJ4ThjmrRULEfvbCzN5Tk7h7k4/rTk7SIjKdSak07/h +H6jE8poN+5E3DIHU
+  tg8/wBabsRZWMYRlR8x7gCxH5U7f5IUrbqMgFtx4wRg444zUkzvHBtjG0kd W6daFe520IuqpS
+  qr4fIjaRPJJmkBkZ+Tjj2PSoQWIcqrJ1JLdtvUfWophlH+YFchskdgOlSgyZMg TaNp2hvSr5b
+  IyvUcrJ6emtiBimAZCyqzAwlj0Hp05qDEBL4VyEkCq4bjPXninmaT7OZBGTHH8qgq DjcMZqJp
+  CD5UZAIbawx0I/ritIpnjJpycHa3q7siSU8eWyM7MzN8vT2/KrUbKY5W2rjcMEDAB7A8 deKZ5
+  EaXivEhkKksFAzhff1471IDEFdA4GZP3RP90evvTk10HScqUuSS0XzLlulxPdvC20XM2WUn 26
+  qfc02YFYUSRJPLhYEZblVI5Q++e9BkaJmkJEkqsANvHGOefrQtxgzI+JYM9ckEH0JxXPaTd0ZT
+  9tKstPdW19Hf10LFjD5ksQeVAskLMQfvDB4H41fEq/Y2WNUDyhWZSvQ5xj8qyYXH2BlQ7FTDbm
+  HI wfu5981rWLvLNLdq8cZDbIo3X7wPAP4VlWVrtnoxcaN5Tlr07fl+hZ+0XAmfdJtkgm2ByMj
+  kY596 seVk2/lzwl4YShyOozy34Zq22be2WScpIFWTcoXBLDHP4UttfJBcpF5AmEeVmwBkk/MT
+  9K4HNtXi v6/r8zOGI9xzhC7/AD+9FOZb6LbJDFiOPKs4UYbpz9KozpHJO7SB/M4wynA5GQfoK
+  6Rrpv3xiwhe RXIcZxuGMVgO8waWO2QOFKljt3Zxxjn0NXQnJ9LHThazt71NX73t9+hMXmSwV4
+  kM4bMfmDkMW7j9 alt4Wi1h0ll+VIGBPZiBwR+dRbfKiV7iCeOZnC7AcDr8xA9hVi2aSa2lC3d
+  uSg2jenOc8fmKUn7r sZ/WWk409U93tb87lNRZSxbYJJATGvzluE55Dep60t0kEVyTEd8cO4Yj
+  GM+n4VO+nziZt09u8ImH miNcEEj6U+VbecRS20se9FZGGPfHPqaaqK6s7o2eJg3abbflt+KuJ
+  IQ8skTiSFs7lUn72FBJ+lTS SxbYDbRG4nAK7D83B5BoX7RMJ4whRTOuHbnjGDj6ikSUR36LAh
+  SRY2SEkAgrjv6nrzWf6GanC9ub Va2vp8xlwUbT32ziGaCREVdxGc8mo1kPnuFh82CdS8ip94M
+  PQ9gB2ptxfSDS4wEgKyc+Zs64P8zT JJJxdTXLELuZtqquMgrzitIxdtTpw8mqWi1d+v5bWLs9
+  z51nAI5o/KPzMw4xtAA/CmxfacmFpYJX 83dOQvOf4R+Ap1vaW5sEmiG7KYWHPLjozD2FSRKrS
+  MsjBJopEUEcZX1Pqcd6ycopNLockZxhzOCv b8GXI0txEFZXnm+ZlaM4wmcf/WqBo7Z7VLmC0d
+  JNu1Ucg7gGwCPahMPfPFBeRbZG8zO0/KEHT6Zp GihlngcXIPnJ5ihSQAB2/PmsldPf8zVJqSk
+  m9NWnf+rE0EdsHnRre5gnBBVHk5Ht+NMF4Y55DOFj m+0bgjjOz1U+9VUFzc2UsrXCgPKjEY+Z
+  +xYH0FWpPLSzlFrieXzVMRPzZTnnnqc55puKvZ6m8/Z8 /Lvf+tW/8i1GkZCJc3kW4xMV2ccDO
+  aoSzwx2qGPzWSSRSjl87VHT8O59akt7x2nhhVUaRoWZztB2 kHJFNmu5PLeIpDEXdQW2DGWHAH
+  pxUxhJSsznp0pqoo307X0+5LUnnK26NALX7QzuGRo+BtXqf1ow jXUrXUxEJYCURnaVI+6f8aj
+  Zrh7Zjb3UDMXVI1wcopOOfrVq4LyrID5bYYh0UYPy+/rxU7af10Om M40/3fPZvte/4ozw0siO
+  jTpwjqrY7sf/AK1Q28V3AJoZVilRdxEoQY5H+FWbx7h9OVtibXj3gquN xzkmoJA4t1ntpQJZV
+  Y4c5DKOcj+Vbx1Vu5tKMXTSvu9P+HWxBay+YwkaMRwJgMWGQSTwarz3dxdt cllEXnEyAlcDch
+  5Gfwq2k15HYTqkK+UzD92U+YMcbapXAAntY5VZbkbyX/gJB4GPetoJc17f1ucv PGFRt2u9O9v
+  PyHjy5kmuZLjeguMB1PChjnB96kuNh1KeNJo4NzqzScjp1PHpVe3uYhFaxXCbGVGD rjG1s8Fv
+  XFPjhVLwOG5VTGHcZV+OcfXtTas3cyp1/Zxm3o+nb8V/n6FQCW5yrrLJIT5kZB4BPGPx xWlFG
+  0LzTy+Y4hYoyI2CSRwfp/hVS4WOYW7W8oUrErhFz8oB5BPcjrW9DcIJrsBont5JfnPUk4+U j2
+  oqzajojv8AbzjD4bJ9Nn6/8MZsNxGPPCFVdpMDfyu3HLY/l6VZuTDCkc0KFlkBjCpwUX39ST3p
+  rWbw24JELs7qVUDDZHTHtSSXVzZ3LLMYjMuRjZ0J71npKXu6m1Kftbezlfvr/wAC5Zmth9kRYr
+  W7 G9SDIzg57/n71NbJCssn2x4ordjuIcZbJ9/bpVBtSvTZrDlX2Abzt5B9KrhkmTz7gtuK4VV
+  ONw/v fQGo9lNxtI6KeGrKHLUdr7W1a+8sSafbw2SyTbmMM6wyBW5GfX86fcfYY5CEjeONg4iJ
+  Iy3Ytn0q vDdG1htw7xzZlBkVhnzDnjH4VuXFsizSGSB50kfdGo6gL/B9T7UTnKMlzO5zqcqdR
+  Nu/bVK//BMm zliS4W1SWUr5ZUNu4Bxk/rVoC4S0QXOxbcRbXBAyr5xz75rOjlRL9XuIWjRjvd
+  BwQRkgCn3EcbwB mndR5oyrEkoCMkH1NVOPvevzHiWlU93Vel7Py8wtbdUFz58hZYpRFlcZIOf
+  UUkkdvHCTAXZygPzD 7gHGDxgmq+5vs8gglVYUkAAfktuOd34VZiLpLcmUpLtkEcgAwCTypHt6
+  1o73u2dNCvLl/eyd+i6D 2hWR7cJG8sUaESOp4DAcD8aginBJCNvGBtJ5wR2PqcmrMoJ0xoow2
+  4SBtwP3yvUj2qcTndJKIInj c71WNQCCetZqTsFLFOMHKyfkMtrlJbxJQI0kZBu3pkcZBGP1qC
+  UGW6MkTJJ5ThFwvBB6VE1s8iZS WPaMqrD+LnJx9elFtcXK292GKRsJApiZMNycjn2xVKK+KI3
+  U5ptqSldf1/WhZupHjkWO5jlJ2uAU faOvWqyzQm8iWMmE7SMzHcAfU0rwsq+ZcszFiERQfmI7
+  nmq8MipOgYRQptdCzqCfQ/4VUYx5dA9t S9lLkl739bLUs3d4HQNCzo8cvloFb+Hj5TjqT2qnN
+  qUt5JvSP7PtmbzVIySemB6cUyW4iW2RLWJl VGZn3/MeeMH3qpbovmK0Nu8jtIcI2DtIAG4+x6
+  YrSnSio3aPLcYqMZ1F72t15dNVp+BbVY3uJt4c AMzRgHBwBxn61qaWIIZoGlYxpNgh5D8vQhl
+  +tUTazknzjHGU3KQBjdnqfoKlEMMcsMy3EchjP7sD 7uQOWx6VFRqUbXNqtanOm4pu78m/xSt+
+  Ben05AytG5khwvybuWYf0piCZr0JPKLYBHDSlTtPcY9M 1FKzXjWKLeRbowwZFBBfHORWnbqiP
+  Gcbo0t2CmT5gcc/pmudylGPvasqhiWo+/rLbs/x/wAiulsr RWaq0su+IsfmyWbOQw9hViB/sP
+  2lpraSSScecrMegHUdPeqv9omSCFoY1V0iESDaOecnH86fa3CG 4Xyo3UlcoZm3fKQRj+ZpSjO
+  z5lodjUqcJOS5k+l3+n+Qksm6II19ChKjZnOUXrg+pNSz3CvbxTWm ZRGwQAH/AFeTnaffrQ0E
+  V7MYFAHzqGcD74A5YewFR+baw2Qmt7qKOSVpNgcEjZjGcAdfQ1GjtZan l1a8bxhFu99rfnZBc
+  yywSARN5cBT5CxLbxn7w+mefWoLhg9lFcx7p42cNJsbgFSAP8arRQW02k27 W6TzSqG3IXzuPt
+  7e1Pgtr1Lq3uYgnnsm94yo2jJ6EfrWyjFddV+P9dyKtWnUg5J2a6d/XqvX8B66 hIb65ad4o5f
+  m+Vkz1HPHpVK7tWXT7YJAzl4CJGH3Q+eB9QP51Tv2mkuoCI9kUhLKpGTtBwcn9ald PIEodZ1e
+  ab92+/5W9xW8aajZowlJRcWn8v6t+QyK3n/dxtuAAJMnTDHoCfWpZLtkWJbiCQXCxtEW Q7T8x
+  6cDrUM81zb3EyRk7FLJhgG9OaheS8jeK4mt5eSGiJHUDgn34rXlctXYdec+TWzXTVrX1IGk t2
+  l8pt8YIGd3O0r26UlzNEI0eMofNyYyvYDgg+p96givWnvBPsiL+ZtUEZyM49KhmSRrolYVQlm4
+  K8j2+tdMafvK4vbTnOLT9O/53JbadI2YzRO2MjBPHI61WIWS7iKySIUYEKxJL+1SMItjeYjh8E
+  gA 9B+X41FEF+yEjzJI0bBOCeT0FapJamFSmuf33o+70+64scFqJV3sEycqSTyT/wDXqzJIMRy
+  BhNgF ZAqkbccAn15qgJURvmid95x2+XtThKy3SxmEkFtp9vr9abi27l+1jF8ylZLol/wNSx9o
+  R4U+YM4H 7ojoQDzxSODNbyyrIJVSYBSp6++MdKpJMHVTtRWVxjA7/wCeo70Ga5jKhI0Ul8MNv
+  Ue1P2epz+2W 769lf8Cz5jQ3ckWCQcgFhkAdc9O9VJ3kYxeTIWKZEiMD0GORx0qWNXnlIMckbg
+  c89ewxSJbTxlpk VtqPhi65ww4IP9RVKyfmcuKrVKkLRk/S6X6XKqRiWXJ8x9nKANgfjVeOJt8
+  AiKA5PmEnnPXP0rR8 uaNPljYnG0ADnrx/OqkUN3MQ/lnYpJPyHnHJraM9HqcuJpRpOLlfn7aK
+  /lrsTNdll2Sja5wq8djy aHx533ldEYBiF4NKsLKqlQZAVynHb1zUaoWARJUK7gZBg5PoOnXFT
+  7vQ6pzqO0pu9/T8+v3lnzld ZXkRn3NncBjdxz26U23knW5R7Et8zhcMoOARwTkY4phgAI2jIV
+  Sck579KSR1gjtWjcywSStgISCu MEZNSkun/AOHGQtT5Zu172X6dvxL0QdY4nRSltO3VjnIB5P
+  4VuwK0SWXlobqaRJCuzptJ5H1FVLe 2jlWKLyJYEVsFXblic4I9B6+tKjzRX0UbMI5S6DJ/hJb
+  t6VwVHz6HoRm6ivdNx1fb8DqZLiBNOgD W0/3N4QtlmHGT+dULgSXeryKlrKoaQ72BHHy5q3PM
+  zJevHNBJKLkALs524AOPQVcijiBZT5gjtmM Bkz97eMj8e2a8yM+RXtqXRrOhBzjo/V6fIzYre
+  SKK5K7pJFkjJIPDDHb8OazxJENWQRRysCDhlbg A9z9a0TA1pMsltchZRbner8gkDg/lxWXEqy
+  WisWDfKuHTgR84w3qc10wd7s9Jy505R2a31/Ine2m jZJJGaNGO9WkYtuC8n9P51oWSWc10YJL
+  iNJpnypwQF44U+p7UyZTBpgtpnZ4TJ8zn+HAwcE9iaqN uDqUSFJX8xsheVwOV+vcfWod5x3OF
+  /7XFq+uy5bW+7qW7q6uLOS8gVRGJZ0cOy5AwASKZb6legSG OO2ZPOY5EQ+ZWFSJNNLbRXEtlJ
+  GVjxC7kEbMfNkdz71Wmmne1txB5Z3orLtXHy1MYpqzirkKFOpZ cqvt9332LSXsMcNrHcSKksk
+  uQBwemPyPaqKTKXYSI4ztHXBQ54H88+tSW9xLc3hM0cYjkYSKSgyF wQSD6DirSzQTyC3t7ORQ
+  JFYlmBbAGQM/WnyqDen4k0aCpSlGMb97NafkQRXNm1uiuVOHKlffPDfQ UyRAwuAZFdoblGLDO
+  0tj5uP7vtWtbwQ3FkbmMRCSZ94wByc4wB7/AM6jXSjPE0LZjdxvI7oQDtDe tZqtBN9CcPXpKX
+  vJr1s/wsUoo2WcSeTN5fzKpRuF55qHybmeRBb286OU2sHbluoz+FW7eO5srP7Q xOZlMgVhnfx
+  gsPYGmv5loPtNzdJu8oqO25uMEe1aKb5nb9T06GLvUlyyS00639UWLXy/IMbyxyJb qIkZOCVc
+  YOT35omDw3KQrJE2bZjkL1KnHHoPak8qO3jj+1uqxKWWZV4LNwQPw4q3FZLc6xKFSWRF CsoDY
+  OQMkfjWMpJNyvp/X/DlyqR5ZtbW62t8iC2gWztZvLlXDSDG7kIM/dPuc1IkD2m37UrRKuUX HH
+  llzwp9TzmrLyTSI8kVsLcv87JKAeW/wAqrdPJLFbMpKxiMhQ/PQg5J7moUpSevU56L9rZuS+/X
+  8Cf7Ozz7YLWTzI28oyqfl+7yD7mqkRuo4438yCRVfy1Qx8jP3c+46Vqgzi3muTIj+ZcRCEJkD5
+  vv GqMscxcwW0ihg7mUMOcr0b6YqYSvo/67jptynaMk4+jf37laeLyQyI4WdnVdwOAo6c/rUA0
+  y4i85 WaQbztVi2dw6k/iKvQE3ulNmCSVVKMrqcE8/MaiS7mGozrOTNDIjSEIMEkdMegx2rZTm
+  rpbo7ozq Rva116O/oUPs4SwJa7+Zf9WpY/dbGPy71bK7J4Ty6hWLMvTrzgemOahnWcCye3tnf
+  NswDkZUqOS1 U542ihMjrO6OC0bK3BXHJ+nNbL37XZMqilDl5rX+/wDr5sjks5pXZopPtKMzOr
+  gnDYHXH8qdHI9x BE9xDIwYKY0HDFQCvX6jNakcEuz5EYuAxZU4w6DqPbB6VizvNPALiRkK5+V
+  QMfe44HpWkJ8+hzrF KpL2bkkl1t+hLdrE0KTCQSo8m3CDB3jgjNV0g3Swkz/ZoIn+TzTn3I46
+  1O1iPs0UUbG4Kk/u4j1Y d+fqaLi0uHtT5YMaxseDzn2+vWqjNWtcwniIygqTavfe35X/AMiuL
+  hXuAtsY2LuVBA/vf4U2C6lI j8sgAsIxIB8qgDGCO5680ItrHC8QBtwASsjHOM4Kr9evNWrGyj
+  lbcZTtD8p6MDwfpVycFF3NfaNQ 5ai28v1X5DgJFjztnkYErFznCdDn396tpLJBA8UMLvMW3gz
+  DcQoHQ5qK7uHjupzg5QADacAZxkfU 1YH2x3zEgUSKWBZQSRngg+lYPVJtHp048y/eR9P+CTW0
+  EM16I5CVjZMh84z6H86sTWtpbtFIJBvc Mixn19RU9pmCzcyJ5qtKpDKMDnpj2zU9pbia5ka4j
+  eSdZWjQrwCG7j8a451WpN30RksdPn3tFeZk zRpMyhgrMgCqFGCSe31NXZL6S28mMK0jKmGbGf
+  nJwAPxrRm02Y7txjd02tIVXGHXv9AKqm2hudWM UW5pnTzPMB+VgP4gPQVKqwktdUjrlWpVI81
+  rpev47GHJHLFdRRGJjPuQOvXJB5xVk2c02p3ASFt7 KxQnG1VHAB/2vepp7aC0lWaSVrgbGUFW
+  Odx561RS4aTTokjMkchAA+bnqSB9Sa6VJyV4jdWd1Ui/ d762+epbRUs57VrieDdsTdEEz8vfP
+  GCcmnTQi41ZxERIFDLtTghgPlJqtOjTSkwxMZD80pc5wcdB 6Ac09LmS0YbZo92xhG2zIKEcE+
+  p9DU8r3T1MKlNuo3Tl7z7pfp/w5UY3K6mGwnnN8gAHD5HJA+lM jliil2eVKu1AG3Nna3UH6Yq
+  x51vdTxp5UodTuUbuSMZx+GKkmhs3tMxuRcdSO20jn8a15ukkOU5y k1Ui1fTRbeen+RDc3iJZ
+  GKGJ+CAHJ4Y5BBH61XV/tF+ZpnEbSTq5XpuIHb25rQZbmeyt7cW+1oNm G2g7jnr9AKkkt2zcy
+  xR+WwugVLDIIyDkD0qVOMVbqcjrRgnSirX6tpsx21CeW5DSf6yJiqggYB71 dSe2/dyyyxqyw7
+  XQpkk5zmqxgAvJzc8tvdnRMA885+grNG+S5hXckbBfmLDIJA4rf2cJLTQ6bUql J+7ZrdpL+vw
+  L8TrPG0Drl2OVA4JA5Jo2xRi2nZ8jawk2HGGbpQbq9itIJnktg5AaMiP7w6Ht26U0 XX7udftF
+  uIjKMKU5Y9iOOgotLpt/Xkct51ZabLzf+RUhkZ7MEB5WyUGD09Sa1I7MJZlrtm2jIV0O AVHp9
+  ayVuGxIpZWywyUUAY7/AJ1YUf6JiIuxchvLJyVA9a0qRl3sdjhUkoqU7Ly/z/zNK3hdM3lv NC
+  ZMHam3JB9PxFTW5mgjt2ifM7DzdrcjaB6fic0yIwQwyzSMZY1cIAhwSSOopJopblLVkhkjkfIV
+  G/hHQ5/KuSTu7PY5/rMNFVel7cz6+T7/AHISRJG1aNrcLOUh/gTAI9f6VZtPPMcwA8mMPnEi5K
+  sT 8o+lRx2csEqtuI2KY5FHUknIqUlRqIkWZdskbloyeUkI+UGlOSasjqrYihO/IunUn8yS2Fs
+  yAp5p ZjuGdozyv5c1WLESsIreGWJUzCSgO6MdxUMBEdujt5kxdWaQ7uBnsPTPQVnyTQTsdknl
+  RHIRSclT 12mnCldnDh6bi25W16/5dTQjnjS4FzBhY5QPLtx944OAfp61HPLPa6vKZWZlBOEU4
+  yFB5HsDUEf7 /TlkEDkruNuV4BXPzH8Kvubf7T81ldSQ5Oz58kjGetNpKW1ylzNuqo8yenToZk
+  UsTWcc9xA7KVGw bsZQkhsH61dk8w3RNqgM2/bGW+ZSmPmIz0xUxgmeximghA3qC6FchAT0Hpn
+  FUp3jmjurgJJaywS7 FRm6Z6jiqupPQJSjNJX1/BeqILgo1iXk/eStKu0A+oxn8ayt8q3MeJAk
+  gUlQy5zjtirZgjjVNkoX zAGG9uFAOMdPWmG1zPJErRuUb75PTHUE/jXVT5YrUUYqnCUZNL5Mz
+  1MIYdWjcMfLXqp7Akj1pzl4 ZA/kOwlLID2wBy35mmvCFViZELpLtG3ocDn8jTVd0aFVheT5jG
+  zZ+VN/c/rXRvqjhqVJpKcZOy/L yJUQ/Yo8FW3qSCRnft7j2qq8saxwqoUOWGWLZBHcYx1z3q9
+  NZ7JlC/NalSY3B68/41SCPLKCBGSG wTjH3qcHF63NbOajbWz0av8A0x6/O0qugc4ypjGMD3+l
+  DxvGIjMhQtH8rFcbu+fyqZVtzZxjLJNu IJJ9OQPxp6Ry3d38zGQHhlA55HUdhjApc3XoTJTfv
+  2vr13/P8Rm+CaUptjcMDtIOADQLFwltIYpj bv8AOzL/ABKBjC5HXNacOnI2jwSlUS4MZCgLgv
+  zjP51cnsljs7iGOQmXzAJEz3HXA7D2rnliYp2R nXxftmlJ2/B/hb9TFuFs57dGjhmh/egRl2z
+  lO7H1HHFOhsd90ZXSVLZZNwJbjHXn1rQa5RHiLGJ0 KFgu3O0d1+tI8dv9reQXqfO5KoScBSKX
+  tJJW/wCCebWhKEeVrfrvf08zDW5EN48kCrKgOYxt9uvT pVJGuJGUpbyM5UgFF+9z1+ldBLZkP
+  Ir4uZGbgQqFAwuRj+tZkQkWBJchZVRQcd92cY/KumE4tNoq MqfMuV2b7oozrKh8uNjt6MMdPp
+  6VDGlyp/1eSTlsIBgVceS6nkWOIM+xdhxHyO/Jx1zTSLhYla4i kjITauR2Oc5Hc1upWVnYqUq
+  LqK7s+lnp8hjpiIGbzfMB4Yfdx9MVnRlkTaxWQGTPyjG0Y5H16VMV
+  ZpLZVl3hEJOfXng+tTh59q/6tgyA/d6ZrRaIz5FUlfVee5//2Q==
+X-ABShowAs:COMPANY
+UID:F0A6918D-8E09-43FA-9684-226810B8A96F
+END:VCARD

Added: 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/home2/addressbook/FCBA0FA3-00B2-4C95-B4EC-4CCC4843F8B1.vcf	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/addressbook_store/ho/me/home2/addressbook/FCBA0FA3-00B2-4C95-B4EC-4CCC4843F8B1.vcf	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,12 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Kawado;Saeko;;;
+FN:Snow Leopard
+ORG:Snow Leopard;
+EMAIL;type=INTERNET;type=WORK;type=pref:snowleopard at example.com
+TEL;type=WORK;type=pref:777-777-7777
+item1.ADR;type=WORK;type=pref:;;1 Fidel Ave. Suite 100;Mountain Top;CA;99999;USA
+item1.X-ABADR:us
+X-ABShowAs:COMPANY
+UID:FCBA0FA3-00B2-4C95-B4EC-4CCC4843F8B1
+END:VCARD

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/common.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/common.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -324,7 +324,7 @@
         yield self.commit()
 
         # Make sure notification fired after commit
-        self.assertTrue(("update", "CardDAV|home1") in self.notifierFactory.history)
+        self.assertTrue("CardDAV|home1" in self.notifierFactory.history)
 
         # Make sure it's available in a new transaction; i.e. test the commit.
         home = yield self.homeUnderTest()
@@ -373,8 +373,8 @@
         self.assertEquals(
             self.notifierFactory.history,
             [
-                ("update", "CardDAV|home1"),
-                ("update", "CardDAV|home1/addressbook"),
+                "CardDAV|home1",
+                "CardDAV|home1/addressbook",
             ]
         )
 
@@ -505,8 +505,8 @@
         self.assertEquals(
             self.notifierFactory.history,
             [
-                ("update", "CardDAV|home1"),
-                ("update", "CardDAV|home1/addressbook"),
+                "CardDAV|home1",
+                "CardDAV|home1/addressbook",
             ]
         )
 
@@ -695,8 +695,8 @@
         self.assertEquals(
             self.notifierFactory.history,
             [
-                ("update", "CardDAV|home1"),
-                ("update", "CardDAV|home1/addressbook_1"),
+                "CardDAV|home1",
+                "CardDAV|home1/addressbook",
             ]
         )
 
@@ -811,8 +811,8 @@
         self.assertEquals(
             self.notifierFactory.history,
             [
-                ("update", "CardDAV|home1"),
-                ("update", "CardDAV|home1/addressbook"),
+                "CardDAV|home1",
+                "CardDAV|home1/addressbook",
             ]
         )
 
@@ -916,10 +916,10 @@
         Addressbooks in one user's addressbook home should not show up in another
         user's addressbook home.
         """
-        home2 = yield self.transactionUnderTest().addressbookHomeWithUID(
-            "home2", create=True
+        home3 = yield self.transactionUnderTest().addressbookHomeWithUID(
+            "home3", create=True
         )
-        self.assertIdentical((yield home2.addressbookWithName("addressbook")), None)
+        self.assertEquals(((yield home3.addressbookWithName("addressbook")).addressbookObjects()), [])
 
 
     @inlineCallbacks
@@ -929,11 +929,11 @@
         user's via uid or name queries.
         """
         home1 = yield self.homeUnderTest()
-        home2 = yield self.transactionUnderTest().addressbookHomeWithUID(
-            "home2", create=True)
+        home3 = yield self.transactionUnderTest().addressbookHomeWithUID(
+            "home3", create=True)
         addressbook1 = yield home1.addressbookWithName("addressbook")
-        addressbook2 = yield home2.addressbookWithName("addressbook")
-        objects = list((yield (yield home2.addressbookWithName("addressbook")).addressbookObjects()))
+        addressbook2 = yield home3.addressbookWithName("addressbook")
+        objects = list((yield (yield home3.addressbookWithName("addressbook")).addressbookObjects()))
         self.assertEquals(objects, [])
         for resourceName in self.requirements['home1']['addressbook'].keys():
             obj = yield addressbook1.addressbookObjectWithName(resourceName)

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/iaddressbookstore.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/iaddressbookstore.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/iaddressbookstore.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -26,7 +26,7 @@
 
 __all__ = [
     # Classes
-    "GroupForSharedAddressBookDeleteNotAllowedError"
+    "GroupForSharedAddressBookDeleteNotAllowedError",
     "GroupWithUnsharedAddressNotAllowedError",
     "SharedGroupDeleteNotAllowedError",
     "IAddressBookTransaction",

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/common.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/common.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/common.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,6 +1,6 @@
 # -*- test-case-name: txdav -*-
 ##
-# 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/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/file.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/file.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -117,15 +117,25 @@
         self.quota = quota
         self._migrating = False
         self._enableNotifications = True
+        self._newTransactionCallbacks = set()
 
+    def callWithNewTransactions(self, callback):
+        """
+        Registers a method to be called whenever a new transaction is
+        created.
 
+        @param callback: callable taking a single argument, a transaction
+        """
+        self._newTransactionCallbacks.add(callback)
+
+
     def newTransaction(self, name='no name'):
         """
         Create a new transaction.
 
         @see: L{Transaction}
         """
-        return self._transactionClass(
+        txn = self._transactionClass(
             self,
             name,
             self.enableCalendars,
@@ -133,6 +143,9 @@
             self._notifierFactory if self._enableNotifications else None,
             self._migrating,
         )
+        for callback in self._newTransactionCallbacks:
+            callback(txn)
+        return txn
 
 
     @inlineCallbacks
@@ -337,6 +350,26 @@
         return NotImplementedError
 
 
+    def imipCreateToken(self, organizer, attendee, icaluid, token=None):
+        return NotImplementedError
+
+
+    def imipLookupByToken(self, token):
+        return NotImplementedError
+
+
+    def imipGetToken(self, organizer, attendee, icaluid):
+        return NotImplementedError
+
+
+    def imipRemoveToken(self, token):
+        return NotImplementedError
+
+
+    def purgeOldIMIPTokens(self, olderThan):
+        return NotImplementedError
+
+
     def isNotifiedAlready(self, obj):
         return obj in self._notifiedAlready
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -64,7 +64,8 @@
 from txdav.common.icommondatastore import HomeChildNameNotAllowedError, \
     HomeChildNameAlreadyExistsError, NoSuchHomeChildError, \
     ObjectResourceNameNotAllowedError, ObjectResourceNameAlreadyExistsError, \
-    NoSuchObjectResourceError, AllRetriesFailed, InvalidSubscriptionValues
+    NoSuchObjectResourceError, AllRetriesFailed, InvalidSubscriptionValues, \
+    InvalidIMIPTokenValues
 from txdav.common.inotifications import INotificationCollection, \
     INotificationObject
 
@@ -150,6 +151,7 @@
     def __init__(self, sqlTxnFactory, notifierFactory,
                  attachmentsPath, attachmentsURIPattern,
                  enableCalendars=True, enableAddressBooks=True,
+                 enableManagedAttachments=True,
                  label="unlabeled", quota=(2 ** 20),
                  logLabels=False, logStats=False, logStatsLogFile=None, logSQL=False,
                  logTransactionWaits=0, timeoutTransactions=0,
@@ -163,6 +165,7 @@
         self.attachmentsURIPattern = attachmentsURIPattern
         self.enableCalendars = enableCalendars
         self.enableAddressBooks = enableAddressBooks
+        self.enableManagedAttachments = enableManagedAttachments
         self.label = label
         self.quota = quota
         self.logLabels = logLabels
@@ -174,6 +177,7 @@
         self.queuer = LocalQueuer(self.newTransaction)
         self._migrating = False
         self._enableNotifications = True
+        self._newTransactionCallbacks = set()
 
         if cacheQueries:
             self.queryCacher = QueryCacher(cachePool=cachePool,
@@ -187,6 +191,16 @@
         __import__("txdav.carddav.datastore.sql")
 
 
+    def callWithNewTransactions(self, callback):
+        """
+        Registers a method to be called whenever a new transaction is
+        created.
+
+        @param callback: callable taking a single argument, a transaction
+        """
+        self._newTransactionCallbacks.add(callback)
+
+
     @inlineCallbacks
     def _withEachHomeDo(self, homeTable, homeFromTxn, action, batchSize):
         """
@@ -246,6 +260,8 @@
         if self.logTransactionWaits or self.timeoutTransactions:
             CommonStoreTransactionMonitor(txn, self.logTransactionWaits,
                                           self.timeoutTransactions)
+        for callback in self._newTransactionCallbacks:
+            callback(txn)
         return txn
 
 
@@ -264,7 +280,19 @@
         self._enableNotifications = not state
 
 
+    @inlineCallbacks
+    def dropboxAllowed(self, txn):
+        """
+        Determine whether dropbox attachments are allowed. Once we have migrated to managed attachments,
+        we should never allow dropbox-style attachments to be created.
+        """
+        if not hasattr(self, "_dropbox_ok"):
+            already_migrated = (yield txn.calendarserverValue("MANAGED-ATTACHMENTS", raiseIfMissing=False))
+            self._dropbox_ok = already_migrated is None
+        returnValue(self._dropbox_ok)
 
+
+
 class TransactionStatsCollector(object):
     """
     Used to log each SQL query and statistics about that query during the course of a single transaction.
@@ -480,13 +508,33 @@
 
 
     @inlineCallbacks
-    def calendarserverValue(self, key):
+    def calendarserverValue(self, key, raiseIfMissing=True):
         result = yield self._calendarserver.on(self, name=key)
         if result and len(result) == 1:
             returnValue(result[0][0])
-        raise RuntimeError("Database key %s cannot be determined." % (key,))
+        if raiseIfMissing:
+            raise RuntimeError("Database key %s cannot be determined." % (key,))
+        else:
+            returnValue(None)
 
 
+    @inlineCallbacks
+    def setCalendarserverValue(self, key, value):
+        cs = schema.CALENDARSERVER
+        yield Insert(
+            {cs.NAME: key, cs.VALUE: value},
+        ).on(self)
+
+
+    @inlineCallbacks
+    def updateCalendarserverValue(self, key, value):
+        cs = schema.CALENDARSERVER
+        yield Update(
+            {cs.VALUE: value},
+            Where=cs.NAME == key,
+        ).on(self)
+
+
     def calendarHomeWithUID(self, uid, create=False):
         return self.homeWithUID(ECALENDARTYPE, uid, create=create)
 
@@ -682,6 +730,105 @@
         return self._apnSubscriptionsBySubscriberQuery.on(self, subscriberGUID=guid)
 
 
+    # Create IMIP token
+
+    @classproperty
+    def _insertIMIPTokenQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Insert({imip.TOKEN: Parameter("token"),
+                       imip.ORGANIZER: Parameter("organizer"),
+                       imip.ATTENDEE: Parameter("attendee"),
+                       imip.ICALUID: Parameter("icaluid"),
+                      })
+
+    @inlineCallbacks
+    def imipCreateToken(self, organizer, attendee, icaluid, token=None):
+        if not (organizer and attendee and icaluid):
+            raise InvalidIMIPTokenValues()
+
+        if token is None:
+            token = str(uuid4())
+
+        try:
+            yield self._insertIMIPTokenQuery.on(self,
+                token=token, organizer=organizer, attendee=attendee,
+                icaluid=icaluid)
+        except Exception:
+            # TODO: is it okay if someone else created the same row just now?
+            pass
+        returnValue(token)
+
+    # Lookup IMIP organizer+attendee+icaluid for token
+
+    @classproperty
+    def _selectIMIPTokenByTokenQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Select([imip.ORGANIZER, imip.ATTENDEE, imip.ICALUID], From=imip,
+                      Where=(imip.TOKEN == Parameter("token")))
+
+    def imipLookupByToken(self, token):
+        return self._selectIMIPTokenByTokenQuery.on(self, token=token)
+
+    # Lookup IMIP token for organizer+attendee+icaluid
+
+    @classproperty
+    def _selectIMIPTokenQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Select([imip.TOKEN], From=imip,
+                      Where=(imip.ORGANIZER == Parameter("organizer")).And(
+                             imip.ATTENDEE == Parameter("attendee")).And(
+                             imip.ICALUID == Parameter("icaluid")))
+    @classproperty
+    def _updateIMIPTokenQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Update({imip.ACCESSED: utcNowSQL, },
+                      Where=(imip.ORGANIZER == Parameter("organizer")).And(
+                             imip.ATTENDEE == Parameter("attendee")).And(
+                             imip.ICALUID == Parameter("icaluid")))
+
+
+    @inlineCallbacks
+    def imipGetToken(self, organizer, attendee, icaluid):
+        row = (yield self._selectIMIPTokenQuery.on(self, organizer=organizer,
+            attendee=attendee, icaluid=icaluid))
+        if row:
+            token = row[0][0]
+            # update the timestamp
+            yield self._updateIMIPTokenQuery.on(self, organizer=organizer,
+                attendee=attendee, icaluid=icaluid)
+        else:
+            token = None
+        returnValue(token)
+
+    # Remove IMIP token
+
+    @classproperty
+    def _removeIMIPTokenQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Delete(From=imip,
+                      Where=(imip.TOKEN == Parameter("token")))
+
+    def imipRemoveToken(self, token):
+        return self._removeIMIPTokenQuery.on(self, token=token)
+
+    # Purge old IMIP tokens
+
+    @classproperty
+    def _purgeOldIMIPTokensQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Delete(From=imip,
+                      Where=(imip.ACCESSED < Parameter("olderThan")))
+
+    def purgeOldIMIPTokens(self, olderThan):
+        """
+        @type olderThan: datetime
+        """
+        return self._purgeOldIMIPTokensQuery.on(self,
+            olderThan=olderThan)
+
+    # End of IMIP
+
+
     def preCommit(self, operation):
         """
         Run things before C{commit}.  (Note: only provided by SQL
@@ -2274,7 +2421,7 @@
 
 class SharingMixIn(object):
     """
-        Common class for CommonHomeChild and AddressBookObject
+    Common class for CommonHomeChild and AddressBookObject
     """
 
     @classproperty
@@ -2373,7 +2520,7 @@
 
 
     @inlineCallbacks
-    def _shareWith(self, shareeHome, mode, status=None, message=None):
+    def shareWith(self, shareeHome, mode, status=None, message=None):
         """
         Share this (owned) L{CommonHomeChild} with another home.
 
@@ -2404,8 +2551,9 @@
             newName = str(uuid4())
             yield self._bindInsertQuery.on(
                 subt, homeID=shareeHome._resourceID,
-                resourceID=self._resourceID, name=newName,
-                mode=mode, bindStatus=status, message=message
+                resourceID=self._resourceID, name=newName, mode=mode,
+                seenByOwner=True, seenBySharee=True,
+                bindStatus=status, message=message
             )
             returnValue(newName)
         try:
@@ -2418,38 +2566,12 @@
                 resourceID=self._resourceID, homeID=shareeHome._resourceID
             ))[0][0]
 
+        # Must send notification to ensure cache invalidation occurs
+        yield self.notifyChanged()
+
         returnValue(sharedName)
 
 
-    @inlineCallbacks
-    def shareWith(self, shareeHome, mode, status=None, message=None):
-        """
-        Share this (owned) L{CommonHomeChild} with another home.
-
-        @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)
-        child = yield shareeHome.childWithID(self._resourceID)
-        yield self.notifyChanged()
-        returnValue(child)
-
-
     @classmethod
     def _updateBindColumnsQuery(cls, columnMap): #@NoSelf
         bind = cls._bindSchema

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_legacy.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_legacy.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_legacy.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -69,7 +69,6 @@
 
 
     def reserveUID(self, uid):
-        uid = uid.encode('utf-8')
         self.log_debug("Reserving UID %r @ %r" % (
                 uid,
                 self.index.resource))
@@ -89,7 +88,6 @@
 
 
     def unreserveUID(self, uid):
-        uid = uid.encode('utf-8')
         self.log_debug("Unreserving UID %r @ %r" % (
                 uid,
                 self.index.resource))
@@ -107,7 +105,6 @@
 
 
     def isReservedUID(self, uid):
-        uid = uid.encode('utf-8')
         self.log_debug("Is reserved UID %r @ %r" % (
                 uid,
                 self.index.resource))
@@ -138,7 +135,6 @@
 
 
     def reserveUID(self, uid):
-        uid = uid.encode('utf-8')
         self.log_debug("Reserving UID %r @ %r" % (
                 uid,
                 self.index.resource))
@@ -154,7 +150,6 @@
 
 
     def unreserveUID(self, uid):
-        uid = uid.encode('utf-8')
         self.log_debug("Unreserving UID %r @ %r" % (
                 uid,
                 self.index.resource))
@@ -166,7 +161,6 @@
 
 
     def isReservedUID(self, uid):
-        uid = uid.encode('utf-8')
         self.log_debug("Is reserved UID %r @ %r" % (
                 uid,
                 self.index.resource))

Copied: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/ReadMe.txt (from rev 10753, CalendarServer/trunk/txdav/common/datastore/sql_schema/ReadMe.txt)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/ReadMe.txt	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/ReadMe.txt	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,9 @@
+Steps for Modifying the Schema
+==============================
+
+1. Before changing current.sql, take note of the VERSION number in the schema
+2. Copy current.sql into old/postgres-dialect/vNNN.sql (where NNN is the VERSION number prior to your changes)
+3. Copy current-oracle-dialect.sql into old/oracle-dialect/vNNN.sql (where NNN is the VERSION number prior to your changes)
+4. Make your changes to current.sql, bumping up the VERSION number
+5. Use the sql_tables.py to generate the oracle version and save as current-oracle-dialect.sql
+6. Write upgrade scripts within upgrades/postgres-dialect and upgrades/oracle-dialect

Modified: 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-oracle-dialect.sql	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-02-19 04:08:00 UTC (rev 10757)
@@ -2,6 +2,7 @@
 create sequence INSTANCE_ID_SEQ;
 create sequence ATTACHMENT_ID_SEQ;
 create sequence REVISION_SEQ;
+create sequence WORKITEM_SEQ;
 create table NODE_INFO (
     "HOSTNAME" nvarchar2(255),
     "PID" integer not null,
@@ -58,7 +59,7 @@
 create table CALENDAR_BIND (
     "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
     "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
-    "CALENDAR_RESOURCE_NAME" nvarchar2(255) not null,
+    "CALENDAR_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
     "MESSAGE" nclob, 
@@ -207,7 +208,7 @@
 create table ADDRESSBOOK_BIND (
     "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
     "ADDRESSBOOK_RESOURCE_ID" integer not null references ADDRESSBOOK on delete cascade,
-    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255) not null,
+    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
     "MESSAGE" nclob, 
@@ -264,15 +265,50 @@
     primary key("TOKEN", "RESOURCE_KEY")
 );
 
+create table IMIP_TOKENS (
+    "TOKEN" nvarchar2(255),
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALUID" nvarchar2(255),
+    "ACCESSED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    primary key("ORGANIZER", "ATTENDEE", "ICALUID")
+);
+
+create table IMIP_INVITATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "FROM_ADDR" nvarchar2(255),
+    "TO_ADDR" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table IMIP_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table IMIP_REPLY_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table PUSH_NOTIFICATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "PUSH_ID" nvarchar2(255)
+);
+
 create table CALENDARSERVER (
     "NAME" nvarchar2(255) primary key,
     "VALUE" nvarchar2(255)
 );
 
-insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '15');
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '16');
 insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '3');
 insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '1');
-
 create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (
     NOTIFICATION_HOME_RESOURCE_ID
 );
@@ -358,3 +394,7 @@
     RESOURCE_KEY
 );
 
+create index IMIP_TOKENS_TOKEN_e94b918f on IMIP_TOKENS (
+    TOKEN
+);
+

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current.sql	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current.sql	2013-02-19 04:08:00 UTC (rev 10757)
@@ -567,7 +567,73 @@
 create index APN_SUBSCRIPTIONS_RESOURCE_KEY
    on APN_SUBSCRIPTIONS(RESOURCE_KEY);
 
+-----------------
+-- IMIP Tokens --
+-----------------
 
+create table IMIP_TOKENS (
+  TOKEN                         varchar(255) not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALUID                       varchar(255) not null,
+  ACCESSED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (ORGANIZER, ATTENDEE, ICALUID) -- implicit index
+);
+
+create index IMIP_TOKENS_TOKEN
+   on IMIP_TOKENS(TOKEN);
+
+----------------
+-- Work Items --
+----------------
+
+create sequence WORKITEM_SEQ;
+
+---------------------------
+-- IMIP Inivitation Work --
+---------------------------
+
+create table IMIP_INVITATION_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  FROM_ADDR                     varchar(255) not null,
+  TO_ADDR                       varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+create table IMIP_POLLING_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  PUSH_ID                       varchar(255) not null
+);
+
+
 --------------------
 -- Schema Version --
 --------------------
@@ -577,6 +643,6 @@
   VALUE                         varchar(255)
 );
 
-insert into CALENDARSERVER values ('VERSION', '15');
+insert into CALENDARSERVER values ('VERSION', '16');
 insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '3');
 insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '1');

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

Modified: 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/v11.sql	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v11.sql	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,7 +1,7 @@
 -- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
 
 ----
--- Copyright (c) 2010-2012 Apple Inc. All rights reserved.
+-- Copyright (c) 2010-2013 Apple Inc. All rights reserved.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.

Modified: 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/v12.sql	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v12.sql	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,7 +1,7 @@
 -- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
 
 ----
--- Copyright (c) 2010-2012 Apple Inc. All rights reserved.
+-- Copyright (c) 2010-2013 Apple Inc. All rights reserved.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.

Modified: 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/old/postgres-dialect/v13.sql	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v13.sql	2013-02-19 04:08:00 UTC (rev 10757)
@@ -1,7 +1,7 @@
 -- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
 
 ----
--- Copyright (c) 2010-2012 Apple Inc. All rights reserved.
+-- Copyright (c) 2010-2013 Apple Inc. All rights reserved.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.

Copied: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v15.sql (from rev 10753, CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v15.sql)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v15.sql	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v15.sql	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,507 @@
+-- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+
+----
+-- Copyright (c) 2010-2013 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+-----------------
+-- Resource ID --
+-----------------
+
+create sequence RESOURCE_ID_SEQ;
+
+-------------------------
+-- Cluster Bookkeeping --
+-------------------------
+
+-- Information about a process connected to this database.
+
+-- Note that this must match the node info schema in twext.enterprise.queue.
+create table NODE_INFO (
+  HOSTNAME  varchar(255) not null,
+  PID       integer not null,
+  PORT      integer not null,
+  TIME      timestamp not null default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (HOSTNAME, PORT)
+);
+
+-- Unique named locks.  This table should always be empty, but rows are
+-- temporarily created in order to prevent undesirable concurrency.
+create table NAMED_LOCK (
+    LOCK_NAME varchar(255) primary key
+);
+
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null unique,                                 -- implicit index
+  DATAVERSION      integer      default 0 not null
+);
+
+----------------------------
+-- Calendar Home Metadata --
+----------------------------
+
+create table CALENDAR_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references CALENDAR_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+
+-----------------------
+-- Calendar Metadata --
+-----------------------
+
+create table CALENDAR_METADATA (
+  RESOURCE_ID           integer   primary key references CALENDAR on delete cascade, -- implicit index
+  SUPPORTED_COMPONENTS  varchar(255) default null,
+  CREATED               timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED              timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID   varchar(255) not null unique                                 -- implicit index
+);
+
+create table NOTIFICATION (
+  RESOURCE_ID                   integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME,
+  NOTIFICATION_UID              varchar(255) not null,
+  XML_TYPE                      varchar(255) not null,
+  XML_DATA                      text         not null,
+  MD5                           char(32)     not null,
+  CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID) -- implicit index
+);
+
+create index NOTIFICATION_NOTIFICATION_HOME_RESOURCE_ID on
+  NOTIFICATION(NOTIFICATION_HOME_RESOURCE_ID);
+
+-------------------
+-- Calendar Bind --
+-------------------
+
+-- Joins CALENDAR_HOME and CALENDAR
+
+create table CALENDAR_BIND (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      not null references CALENDAR on delete cascade,
+  CALENDAR_RESOURCE_NAME    varchar(255) not null,
+  BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
+  MESSAGE                   text,
+
+  primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID), -- implicit index
+  unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)     -- implicit index
+);
+
+create index CALENDAR_BIND_RESOURCE_ID on CALENDAR_BIND(CALENDAR_RESOURCE_ID);
+
+-- Enumeration of calendar bind modes
+
+create table CALENDAR_BIND_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_MODE values (0, 'own'  );
+insert into CALENDAR_BIND_MODE values (1, 'read' );
+insert into CALENDAR_BIND_MODE values (2, 'write');
+insert into CALENDAR_BIND_MODE values (3, 'direct');
+
+-- Enumeration of statuses
+
+create table CALENDAR_BIND_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_STATUS values (0, 'invited' );
+insert into CALENDAR_BIND_STATUS values (1, 'accepted');
+insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+  RESOURCE_ID          integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID integer      not null references CALENDAR on delete cascade,
+  RESOURCE_NAME        varchar(255) not null,
+  ICALENDAR_TEXT       text         not null,
+  ICALENDAR_UID        varchar(255) not null,
+  ICALENDAR_TYPE       varchar(255) not null,
+  ATTACHMENTS_MODE     integer      default 0 not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
+  DROPBOX_ID           varchar(255),
+  ORGANIZER            varchar(255),
+  ORGANIZER_OBJECT     integer      references CALENDAR_OBJECT,
+  RECURRANCE_MIN       date,        -- minimum date that recurrences have been expanded to.
+  RECURRANCE_MAX       date,        -- maximum date that recurrences have been expanded to.
+  ACCESS               integer      default 0 not null,
+  SCHEDULE_OBJECT      boolean      default false,
+  SCHEDULE_TAG         varchar(36)  default null,
+  SCHEDULE_ETAGS       text         default null,
+  PRIVATE_COMMENTS     boolean      default false not null,
+  MD5                  char(32)     not null,
+  CREATED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED             timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (CALENDAR_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+
+  -- since the 'inbox' is a 'calendar resource' for the purpose of storing
+  -- calendar objects, this constraint has to be selectively enforced by the
+  -- application layer.
+
+  -- unique(CALENDAR_RESOURCE_ID, ICALENDAR_UID)
+);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_AND_ICALENDAR_UID on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_RECURRANCE_MAX on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, RECURRANCE_MAX);
+
+create index CALENDAR_OBJECT_ORGANIZER_OBJECT on
+  CALENDAR_OBJECT(ORGANIZER_OBJECT);
+
+create index CALENDAR_OBJECT_DROPBOX_ID on
+  CALENDAR_OBJECT(DROPBOX_ID);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (0, 'none' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (1, 'read' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (2, 'write');
+
+
+-- Enumeration of calendar access types
+
+create table CALENDAR_ACCESS_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(32) not null unique
+);
+
+insert into CALENDAR_ACCESS_TYPE values (0, ''             );
+insert into CALENDAR_ACCESS_TYPE values (1, 'public'       );
+insert into CALENDAR_ACCESS_TYPE values (2, 'private'      );
+insert into CALENDAR_ACCESS_TYPE values (3, 'confidential' );
+insert into CALENDAR_ACCESS_TYPE values (4, 'restricted'   );
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+  INSTANCE_ID                 integer        primary key default nextval('INSTANCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID        integer        not null references CALENDAR on delete cascade,
+  CALENDAR_OBJECT_RESOURCE_ID integer        not null references CALENDAR_OBJECT on delete cascade,
+  FLOATING                    boolean        not null,
+  START_DATE                  timestamp      not null,
+  END_DATE                    timestamp      not null,
+  FBTYPE                      integer        not null,
+  TRANSPARENT                 boolean        not null
+);
+
+create index TIME_RANGE_CALENDAR_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_RESOURCE_ID);
+create index TIME_RANGE_CALENDAR_OBJECT_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_OBJECT_RESOURCE_ID);
+
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown'         );
+insert into FREE_BUSY_TYPE values (1, 'free'            );
+insert into FREE_BUSY_TYPE values (2, 'busy'            );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative'  );
+
+
+------------------
+-- Transparency --
+------------------
+
+create table TRANSPARENCY (
+  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
+  USER_ID                     varchar(255) not null,
+  TRANSPARENT                 boolean      not null
+);
+
+create index TRANSPARENCY_TIME_RANGE_INSTANCE_ID on
+  TRANSPARENCY(TIME_RANGE_INSTANCE_ID);
+
+
+----------------
+-- Attachment --
+----------------
+
+create sequence ATTACHMENT_ID_SEQ;
+
+create table ATTACHMENT (
+  ATTACHMENT_ID               integer           primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
+  CALENDAR_HOME_RESOURCE_ID   integer           not null references CALENDAR_HOME,
+  DROPBOX_ID                  varchar(255),
+  CONTENT_TYPE                varchar(255)      not null,
+  SIZE                        integer           not null,
+  MD5                         char(32)          not null,
+  CREATED                     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                    timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  PATH                        varchar(1024)     not null
+);
+
+create index ATTACHMENT_CALENDAR_HOME_RESOURCE_ID on
+  ATTACHMENT(CALENDAR_HOME_RESOURCE_ID);
+
+-- Many-to-many relationship between attachments and calendar objects
+create table ATTACHMENT_CALENDAR_OBJECT (
+  ATTACHMENT_ID                  integer      not null references ATTACHMENT on delete cascade,
+  MANAGED_ID                     varchar(255) not null,
+  CALENDAR_OBJECT_RESOURCE_ID    integer      not null references CALENDAR_OBJECT on delete cascade,
+
+  primary key (ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID), -- implicit index
+  unique (MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
+);
+
+
+-----------------------
+-- Resource Property --
+-----------------------
+
+create table RESOURCE_PROPERTY (
+  RESOURCE_ID integer      not null, -- foreign key: *.RESOURCE_ID
+  NAME        varchar(255) not null,
+  VALUE       text         not null, -- FIXME: xml?
+  VIEWER_UID  varchar(255),
+
+  primary key (RESOURCE_ID, NAME, VIEWER_UID) -- implicit index
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null unique,                                -- implicit index
+  DATAVERSION      integer      default 0 not null
+);
+
+-------------------------------
+-- AddressBook Home Metadata --
+-------------------------------
+
+create table ADDRESSBOOK_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references ADDRESSBOOK_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+-----------------
+-- AddressBook --
+-----------------
+
+create table ADDRESSBOOK (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+
+--------------------------
+-- AddressBook Metadata --
+--------------------------
+
+create table ADDRESSBOOK_METADATA (
+  RESOURCE_ID integer   primary key references ADDRESSBOOK on delete cascade, -- implicit index
+  CREATED     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED    timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+----------------------
+-- AddressBook Bind --
+----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK
+
+create table ADDRESSBOOK_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID integer      not null references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_RESOURCE_ID      integer      not null references ADDRESSBOOK on delete cascade,
+  ADDRESSBOOK_RESOURCE_NAME    varchar(255) not null,
+  BIND_MODE                    integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                  integer      not null, -- enum CALENDAR_BIND_STATUS
+  MESSAGE                      text,                  -- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)     -- implicit index
+);
+
+create index ADDRESSBOOK_BIND_RESOURCE_ID on
+  ADDRESSBOOK_BIND(ADDRESSBOOK_RESOURCE_ID);
+
+create table ADDRESSBOOK_OBJECT (
+  RESOURCE_ID             integer      primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  ADDRESSBOOK_RESOURCE_ID integer      not null references ADDRESSBOOK on delete cascade,
+  RESOURCE_NAME           varchar(255) not null,
+  VCARD_TEXT              text         not null,
+  VCARD_UID               varchar(255) not null,
+  MD5                     char(32)     not null,
+  CREATED                 timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME), -- implicit index
+  unique (ADDRESSBOOK_RESOURCE_ID, VCARD_UID)      -- implicit index
+);
+
+---------------
+-- Revisions --
+---------------
+
+create sequence REVISION_SEQ;
+
+
+---------------
+-- Revisions --
+---------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      references CALENDAR,
+  CALENDAR_NAME             varchar(255) default null,
+  RESOURCE_NAME             varchar(255),
+  REVISION                  integer      default nextval('REVISION_SEQ') not null,
+  DELETED                   boolean      not null
+);
+
+create index CALENDAR_OBJECT_REVISIONS_HOME_RESOURCE_ID_CALENDAR_RESOURCE_ID
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, RESOURCE_NAME);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, REVISION);
+
+-------------------------------
+-- AddressBook Object Revisions --
+-------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+  ADDRESSBOOK_HOME_RESOURCE_ID integer      not null references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_RESOURCE_ID      integer      references ADDRESSBOOK,
+  ADDRESSBOOK_NAME             varchar(255) default null,
+  RESOURCE_NAME                varchar(255),
+  REVISION                     integer      default nextval('REVISION_SEQ') not null,
+  DELETED                      boolean      not null
+);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_HOME_RESOURCE_ID_ADDRESSBOOK_RESOURCE_ID
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_RESOURCE_ID, REVISION);
+
+-----------------------------------
+-- Notification Object Revisions --
+-----------------------------------
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+
+  unique(NOTIFICATION_HOME_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+);
+
+create index NOTIFICATION_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on NOTIFICATION_OBJECT_REVISIONS(NOTIFICATION_HOME_RESOURCE_ID, REVISION);
+
+-------------------------------------------
+-- Apple Push Notification Subscriptions --
+-------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  USER_AGENT                    varchar(255) default null,
+  IP_ADDR                       varchar(255) default null,
+
+  primary key (TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+   on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+
+--------------------
+-- Schema Version --
+--------------------
+
+create table CALENDARSERVER (
+  NAME                          varchar(255) primary key, -- implicit index
+  VALUE                         varchar(255)
+);
+
+insert into CALENDARSERVER values ('VERSION', '15');
+insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '3');
+insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '1');

Modified: 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_12_to_13.sql	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_12_to_13.sql	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/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/oracle-dialect/upgrade_from_13_to_14.sql	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql	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.

Copied: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_15_to_16.sql (from rev 10753, CalendarServer/trunk/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/oracle-dialect/upgrade_from_15_to_16.sql	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_15_to_16.sql	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,63 @@
+----
+-- Copyright (c) 2011-2013 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+-------------------------------------------------
+-- Upgrade database schema from VERSION 15 to 16 --
+-------------------------------------------------
+
+
+create sequence WORKITEM_SEQ;
+
+create table IMIP_TOKENS (
+    "TOKEN" nvarchar2(255),
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALUID" nvarchar2(255),
+    "ACCESSED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    primary key("ORGANIZER", "ATTENDEE", "ICALUID")
+);
+
+create table IMIP_INVITATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "FROM_ADDR" nvarchar2(255),
+    "TO_ADDR" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table IMIP_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table IMIP_REPLY_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table PUSH_NOTIFICATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "PUSH_ID" nvarchar2(255)
+);
+
+
+-- Now update the version
+update CALENDARSERVER set VALUE = '16' where NAME = 'VERSION';
+

Modified: 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_12_to_13.sql	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_12_to_13.sql	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/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_13_to_14.sql
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_13_to_14.sql	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_13_to_14.sql	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.

Copied: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_15_to_16.sql (from rev 10753, CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-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	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_15_to_16.sql	2013-02-19 04:08:00 UTC (rev 10757)
@@ -0,0 +1,91 @@
+----
+-- Copyright (c) 2011-2013 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+-------------------------------------------------
+-- Upgrade database schema from VERSION 15 to 16 --
+-------------------------------------------------
+
+
+-----------------
+-- IMIP Tokens --
+-----------------
+
+create table IMIP_TOKENS (
+  TOKEN                         varchar(255) not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALUID                       varchar(255) not null,
+  ACCESSED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (ORGANIZER, ATTENDEE, ICALUID) -- implicit index
+);
+
+create index IMIP_TOKENS_TOKEN
+   on IMIP_TOKENS(TOKEN);
+
+----------------
+-- Work Items --
+----------------
+
+create sequence WORKITEM_SEQ;
+
+---------------------------
+-- IMIP Inivitation Work --
+---------------------------
+
+create table IMIP_INVITATION_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  FROM_ADDR                     varchar(255) not null,
+  TO_ADDR                       varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+create table IMIP_POLLING_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  PUSH_ID                       varchar(255) not null
+);
+
+
+-- Now update the version
+update CALENDARSERVER set VALUE = '16' where NAME = 'VERSION';
+

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/test/test_sql_schema_files.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/test/test_sql_schema_files.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/test/test_sql_schema_files.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/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/test/util.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/test/util.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -30,7 +30,7 @@
 from twext.web2.dav.resource import TwistedGETContentMD5
 
 from twisted.internet import reactor
-from twisted.internet.defer import Deferred, inlineCallbacks, succeed
+from twisted.internet.defer import Deferred, inlineCallbacks
 from twisted.internet.task import deferLater
 from twisted.python import log
 from twisted.application.service import Service
@@ -45,7 +45,7 @@
 from twext.enterprise.adbapi2 import ConnectionPool
 from twisted.trial.unittest import TestCase
 from twisted.internet.defer import returnValue
-from twistedcaldav.notify import Notifier, NodeCreationException
+from calendarserver.push.notifier import Notifier
 from twext.enterprise.ienterprise import AlreadyFinishedError
 from twistedcaldav.vcard import Component as ABComponent
 
@@ -225,7 +225,7 @@
         # table' statements are issued, so it's not possible to reference a
         # later table.  Therefore it's OK to drop them in the (reverse) order
         # that they happen to be in.
-        tables = [t.name for t in schema.model.tables
+        tables = [t.name for t in schema.model.tables #@UndefinedVariable
                   # All tables with rows _in_ the schema are populated
                   # exclusively _by_ the schema and shouldn't be manipulated
                   # while the server is running, so we leave those populated.
@@ -415,14 +415,17 @@
     yield populateTxn.commit()
 
 
+
 def updateToCurrentYear(data):
     """
     Update the supplied iCalendar data so that all dates are updated to the current year.
     """
 
     nowYear = PyCalendarDateTime.getToday().getYear()
-    return data % {"now":nowYear}
+    return data % {"now": nowYear}
 
+
+
 @inlineCallbacks
 def resetCalendarMD5s(md5s, store):
     """
@@ -453,6 +456,7 @@
     yield populateTxn.commit()
 
 
+
 @inlineCallbacks
 def populateAddressBooksFrom(requirements, store):
     """
@@ -488,6 +492,8 @@
                         )
     yield populateTxn.commit()
 
+
+
 @inlineCallbacks
 def resetAddressBookMD5s(md5s, store):
     """
@@ -518,6 +524,7 @@
     yield populateTxn.commit()
 
 
+
 def assertProvides(testCase, interface, provider):
     """
     Verify that C{provider} properly provides C{interface}
@@ -608,16 +615,38 @@
         raise NotImplementedError("CommonCommonTests subclasses must implement.")
 
 
+    @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)))
 
-class StubNodeCacher(object):
 
-    def waitForNode(self, notifier, nodeName):
-        if "fail" in nodeName:
-            raise NodeCreationException("Could not create node")
-        else:
-            return succeed(True)
+    @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)))
+
+
+
+
 class StubNotifierFactory(object):
     """
     For testing push notifications without an XMPP server.
@@ -625,20 +654,33 @@
 
     def __init__(self):
         self.reset()
-        self.nodeCacher = StubNodeCacher()
-        self.pubSubConfig = {
-            "enabled" : True,
-            "service" : "pubsub.example.com",
-            "host" : "example.com",
-            "port" : "123",
-        }
+        self.hostname = "example.com"
 
+
     def newNotifier(self, label="default", id=None, prefix=None):
         return Notifier(self, label=label, id=id, prefix=prefix)
 
-    def send(self, op, id):
-        self.history.append((op, id))
 
+    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 send(self, id):
+        self.history.append(id)
+
+
     def reset(self):
         self.history = []
 
@@ -659,5 +701,3 @@
     aTest.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
     aTest.patch(config.Memcached.Pools.Default, "ServerEnabled", False)
     aTest.patch(Memcacher, "allowTestCache", True)
-
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/upgrade/sql/others/attachment_migration.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/upgrade/sql/others/attachment_migration.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/upgrade/sql/others/attachment_migration.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -14,29 +14,46 @@
 # limitations under the License.
 ##
 
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet.defer import inlineCallbacks, returnValue
 from txdav.caldav.datastore.sql import CalendarStoreFeatures
 
 """
 Upgrader that checks for any dropbox attachments, and upgrades them all to managed attachments.
+
+This makes use of a MANAGED-ATTACHMENTS flag in the CALENDARSERVER table to determine whether the upgrade has been
+done for this store. If it has been done, the store will advertise that to the app layer and that must prevent the
+use of dropbox in the future.
 """
 
 @inlineCallbacks
 def doUpgrade(upgrader):
     """
-    Do the required upgrade steps.
+    Do the required upgrade steps. Also, make sure we correctly set the store for having attachments enabled.
     """
 
+    # Ignore if the store is not enabled for managed attachments
+    if not upgrader.sqlStore.enableManagedAttachments:
+        upgrader.log_warn("No dropbox migration - managed attachments not enabled")
+        returnValue(None)
+
+    statusKey = "MANAGED-ATTACHMENTS"
     storeWrapper = CalendarStoreFeatures(upgrader.sqlStore)
     txn = upgrader.sqlStore.newTransaction("attachment_migration.doUpgrade")
     try:
-        needUpgrade = (yield storeWrapper.hasDropboxAttachments(txn))
+        managed = (yield txn.calendarserverValue(statusKey, raiseIfMissing=False))
+        if managed is None:
+            upgrader.log_warn("Checking for dropbox migration")
+            needUpgrade = (yield storeWrapper.hasDropboxAttachments(txn))
+        else:
+            needUpgrade = False
         if needUpgrade:
             upgrader.log_warn("Starting dropbox migration")
             yield storeWrapper.upgradeToManagedAttachments(batchSize=10)
             upgrader.log_warn("Finished dropbox migration")
         else:
             upgrader.log_warn("No dropbox migration needed")
+        if managed is None:
+            yield txn.setCalendarserverValue(statusKey, "1")
     except RuntimeError:
         yield txn.abort()
         raise

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/upgrade/test/test_migrate.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/upgrade/test/test_migrate.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/upgrade/test/test_migrate.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -18,36 +18,37 @@
 Tests for L{txdav.common.datastore.upgrade.migrate}.
 """
 
-import copy
-
+from twext.enterprise.adbapi2 import Pickle
+from twext.enterprise.dal.syntax import Delete
 from twext.python.filepath import CachingFilePath
 from twext.web2.http_headers import MimeType
-from twext.enterprise.adbapi2 import Pickle
 
-from twisted.python.modules import getModule
 from twisted.application.service import Service, MultiService
 from twisted.internet.defer import inlineCallbacks, Deferred, returnValue
 from twisted.internet.protocol import Protocol
 from twisted.protocols.amp import AMP, Command, String
+from twisted.python.modules import getModule
 from twisted.python.reflect import qual, namedAny
 from twisted.trial.unittest import TestCase
 
+from twistedcaldav.config import config
 
 from txdav.caldav.datastore.test.common import CommonTests
 from txdav.carddav.datastore.test.common import CommonTests as ABCommonTests
 from txdav.common.datastore.file import CommonDataStore
-
+from txdav.common.datastore.sql_tables import schema
+from txdav.common.datastore.test.util import SQLStoreBuilder
 from txdav.common.datastore.test.util import theStoreBuilder, \
     populateCalendarsFrom, StubNotifierFactory, resetCalendarMD5s, \
     populateAddressBooksFrom, resetAddressBookMD5s, deriveValue, \
     withSpecialValue
-
-from txdav.common.datastore.test.util import SQLStoreBuilder
 from txdav.common.datastore.upgrade.migrate import UpgradeToDatabaseService, \
     StoreSpawnerService, swapAMP
 
+import copy
 
 
+
 class CreateStore(Command):
     """
     Create a store in a subprocess.
@@ -55,6 +56,7 @@
     arguments = [('delegateTo', String())]
 
 
+
 class PickleConfig(Command):
     """
     Unpickle some configuration in a subprocess.
@@ -317,6 +319,19 @@
         as well.
         """
 
+        # Need to tweak config and settings to setup dropbox to work
+        self.patch(config, "EnableDropBox", True)
+        self.patch(config, "EnableManagedAttachments", False)
+        self.sqlStore.enableManagedAttachments = False
+
+        txn = self.sqlStore.newTransaction()
+        cs = schema.CALENDARSERVER
+        yield Delete(
+            From=cs,
+            Where=cs.NAME == "MANAGED-ATTACHMENTS"
+        ).on(txn)
+        yield txn.commit()
+
         txn = self.fileStore.newTransaction()
         committed = []
         def maybeCommit():
@@ -409,4 +424,3 @@
             self.fileStore, self.sqlStore, self.stubService,
             parallel=2, spawner=StubSpawner()
         )
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/icommondatastore.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/icommondatastore.py	2013-02-19 03:38:19 UTC (rev 10756)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/icommondatastore.py	2013-02-19 04:08:00 UTC (rev 10757)
@@ -148,6 +148,13 @@
     Invalid APN subscription values passed in.
     """
 
+# IMIP Tokens
+
+class InvalidIMIPTokenValues(ValueError):
+    """
+    Invalid IMIP token values passed in.
+    """
+
 #
 # Interfaces
 #
@@ -240,7 +247,59 @@
         @return: tuples of (token, key, timestamp, userAgent, ipAddr)
         """
 
+    def imipCreateToken(organizer, attendee, icaluid, token=None):
+        """
+        Add an entry in the database; if no token is provided, one will be
+        generated.
 
+        @param organizer: the CUA of the organizer
+        @type organizer: C{str}
+        @param attendee: the mailto: CUA of the attendee
+        @type organizer: C{str}
+        @param icaluid: the icalendar UID of the VEVENT
+        @type organizer: C{str}
+        @param token: the value to use in the "plus address" of the reply-to
+        @type token: C{str}
+        """
+
+    def imipLookupByToken(token):
+        """
+        Returns the organizer, attendee, and icaluid corresponding to the token
+
+        @param token: the token to look up
+        @type token: C{str}
+        """
+
+
+    def imipGetToken(organizer, attendee, icaluid):
+        """
+        Returns the token (if any) corresponding to the given organizer, attendee,
+        and icaluid combination
+
+        @param organizer: the CUA of the organizer
+        @type organizer: C{str}
+        @param attendee: the mailto: CUA of the attendee
+        @type organizer: C{str}
+        @param icaluid: the icalendar UID of the VEVENT
+        @type organizer: C{str}
+        """
+
+
+    def imipRemoveToken(token):
+        """
+        Removes the entry for the given token.
+
+        @param token: the token to remove
+        @type token: C{str}
+        """
+
+
+    def purgeOldIMIPTokens(olderThan):
+        """
+        Removes all tokens whose access time is before olderThan
+        """
+        
+
 class IShareableCollection(Interface):
     """
     A collection resource which may be shared.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130218/5a456eb4/attachment-0001.html>


More information about the calendarserver-changes mailing list