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

source_changes at macosforge.org source_changes at macosforge.org
Fri Oct 26 14:24:44 PDT 2012


Revision: 9991
          http://trac.calendarserver.org//changeset/9991
Author:   gaya at apple.com
Date:     2012-10-26 14:24:44 -0700 (Fri, 26 Oct 2012)
Log Message:
-----------
update from trunk

Modified Paths:
--------------
    CalendarServer/branches/users/gaya/sharedgroups/README
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/root.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/caldav.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/test/test_caldav.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/test/test_util.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/util.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/shell/vfs.py
    CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-apple.plist
    CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-partitioning-primary.plist
    CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-partitioning-secondary.plist
    CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist
    CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd.plist
    CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarpromotion.py
    CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/loadtest/config.plist
    CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/loadtest/ical.py
    CalendarServer/branches/users/gaya/sharedgroups/doc/Admin/ExtendedLogItems.rst
    CalendarServer/branches/users/gaya/sharedgroups/support/Makefile.Apple
    CalendarServer/branches/users/gaya/sharedgroups/support/version.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/resource.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/web2/resource.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/web2/stream.py
    CalendarServer/branches/users/gaya/sharedgroups/twisted/plugins/caldav.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/freebusyurl.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/ical.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/mkcalendar.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/mkcol.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_addressbook_query.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_multiget_common.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_sync_collection.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/resource.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/addressmapping.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/cuaddress.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/delivery.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/implicit.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/scheduler.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_icaldiff.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_implicit.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_itip.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/utils.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/simpleresource.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_icalendar.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_upgrade.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_wrapping.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/timezonexml.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/upgrade.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/base/datastore/subpostgres.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/base/propertystore/sql.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/file.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_file.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/util.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/icalendarstore.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/common.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/test_file.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/iaddressbookstore.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/test/test_sql.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/xml/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/xml/base.py

Added Paths:
-----------
    CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_dkimtool
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/platform/darwin/_sacl.so
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dkimtool.py
    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/test/test_promotion.py
    CalendarServer/branches/users/gaya/sharedgroups/doc/Admin/iSchedule.txt
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/
    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/
    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/imip/
    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/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/good_reply
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.mo
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.mo
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer
    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/ischedule/
    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/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.example.com
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.two.zones
    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/txdav/common/datastore/common.py

Removed Paths:
-------------
    CalendarServer/branches/users/gaya/sharedgroups/conf/servers-test.xml
    CalendarServer/branches/users/gaya/sharedgroups/conf/servers.xml
    CalendarServer/branches/users/gaya/sharedgroups/conf/servertoserver-test.xml
    CalendarServer/branches/users/gaya/sharedgroups/conf/servertoserver.xml
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/mail.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/schedule.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/
    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/caldav.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/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/good_reply
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.mo
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.mo
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer
    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/imip.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/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.example.com
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.two.zones
    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/ischedule.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischeduleservers.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_caldav.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_imip.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_ischeduleservers.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/servers.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/data/mail/
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_mail.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_schedule.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_servers.py

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


Property changes on: CalendarServer/branches/users/gaya/sharedgroups
___________________________________________________________________
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/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/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/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/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/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/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/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/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/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/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-9988

Modified: CalendarServer/branches/users/gaya/sharedgroups/README
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/README	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/README	2012-10-26 21:24:44 UTC (rev 9991)
@@ -110,3 +110,4 @@
 8443 for HTTPS.  You should then be able to connect to the server
 using your web browser (eg. Safari, Firefox) or with a CalDAV client
 (eg. iCal).
+

Copied: CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_dkimtool (from rev 9988, CalendarServer/trunk/bin/calendarserver_dkimtool)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_dkimtool	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/bin/calendarserver_dkimtool	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2012 Apple Inc. All 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.dkimtool import main
+    main()

Added: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/platform/darwin/_sacl.so
===================================================================
(Binary files differ)


Property changes on: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/platform/darwin/_sacl.so
___________________________________________________________________
Added: svn:executable
   + *
Added: svn:mime-type
   + application/octet-stream

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/root.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/root.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/provision/root.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -384,14 +384,6 @@
                             "Your client software (%s) is not allowed to access this service." % (agent,)
                         ))
 
-        # Look for forwarding
-        remote_ip = request.headers.getRawHeaders('x-forwarded-for')
-        if remote_ip and len(remote_ip) == 1:
-            request.forwarded_for = remote_ip[0]
-            if not hasattr(request, "extendedLogItems"):
-                request.extendedLogItems = {}
-            request.extendedLogItems["xff"] = remote_ip[0]
-
         if config.EnableResponseCache and request.method == "PROPFIND" and not getattr(request, "notInCache", False) and len(segments) > 1:
             try:
                 authnUser, authzUser = (yield self.authenticate(request))

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/caldav.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/caldav.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -32,6 +32,7 @@
 from grp import getgrnam
 import OpenSSL
 from OpenSSL.SSL import Error as SSLError
+from os import getuid, getgid
 
 from zope.interface import implements
 
@@ -70,12 +71,12 @@
 from twistedcaldav.config import ConfigurationError
 from twistedcaldav.config import config
 from twistedcaldav.localization import processLocalizationFiles
-from twistedcaldav.mail import IMIPReplyInboxResource
+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
 
-from calendarserver.tap.util import pgServiceFromConfig, getDBPool
+from calendarserver.tap.util import pgServiceFromConfig, getDBPool, MemoryLimitService
 
 from twext.enterprise.ienterprise import POSTGRES_DIALECT
 from twext.enterprise.ienterprise import ORACLE_DIALECT
@@ -208,6 +209,8 @@
         self.transport.write("%s\r\n" % (stats,))
         self.transport.loseConnection()
 
+
+
 class CalDAVStatisticsServer (Factory):
 
     protocol = CalDAVStatisticsProtocol
@@ -216,6 +219,7 @@
         self.logger = logObserver
 
 
+
 class ErrorLoggingMultiService(MultiService, object):
     """ Registers a rotating file logger for error logging, if
         config.ErrorLogEnabled is True. """
@@ -236,6 +240,7 @@
             app.setComponent(ILogObserver, errorLogObserver)
 
 
+
 class CalDAVService (ErrorLoggingMultiService):
 
     # The ConnectionService is a MultiService which bundles all the connection
@@ -247,10 +252,12 @@
         self.logObserver = logObserver # accesslog observer
         MultiService.__init__(self)
 
+
     def privilegedStartService(self):
         MultiService.privilegedStartService(self)
         self.logObserver.start()
 
+
     @inlineCallbacks
     def stopService(self):
         """
@@ -265,6 +272,7 @@
         self.logObserver.stop()
 
 
+
 class CalDAVOptions (Options, LoggingMixIn):
     optParameters = [[
         "config", "f", DEFAULT_CONFIG_FILE, "Path to configuration file."
@@ -329,6 +337,7 @@
                 value, overrideDict[key]
             )
 
+
     def opt_option(self, option):
         """
         Set an option to override a value in the config file. True, False, int,
@@ -349,6 +358,7 @@
 
     opt_o = opt_option
 
+
     def postOptions(self):
         try:
             self.loadConfiguration()
@@ -357,6 +367,7 @@
             print "Invalid configuration: %s" % (e,)
             sys.exit(1)
 
+
     def loadConfiguration(self):
         if not os.path.exists(self["config"]):
             raise ConfigurationError("Config file %s not found. Exiting."
@@ -367,9 +378,11 @@
         config.load(self["config"])
         config.updateDefaults(self.overrides)
 
+
     def checkDirectory(self, dirpath, description, access=None, create=None, wait=False):
         checkDirectory(dirpath, description, access=access, create=create, wait=wait)
 
+
     def checkConfiguration(self):
 
         # Having CalDAV *and* CardDAV both disabled is an illegal configuration
@@ -397,7 +410,6 @@
 
         self.parent["pidfile"] = config.PIDFile
 
-
         #
         # Verify that server root actually exists
         #
@@ -527,7 +539,6 @@
             )
             self.monitor.addProcessObject(process, PARENT_ENVIRONMENT)
 
-
         if (
             config.Notifications.Enabled and
             config.Notifications.InternalNotificationHost == "localhost"
@@ -595,6 +606,7 @@
                                env=PARENT_ENVIRONMENT)
 
 
+
 class ReExecService(MultiService, LoggingMixIn):
     """
     A MultiService which catches SIGHUP and re-exec's the process.
@@ -612,6 +624,7 @@
         self.reactor = reactor
         MultiService.__init__(self)
 
+
     def reExec(self):
         """
         Removes pidfile, registers an exec to happen after shutdown, then
@@ -627,18 +640,22 @@
             sys.executable, [sys.executable] + sys.argv)
         self.reactor.stop()
 
+
     def sighupHandler(self, num, frame):
         self.reactor.callFromThread(self.reExec)
 
+
     def startService(self):
         self.previousHandler = signal.signal(signal.SIGHUP, self.sighupHandler)
         MultiService.startService(self)
 
+
     def stopService(self):
         signal.signal(signal.SIGHUP, self.previousHandler)
         MultiService.stopService(self)
 
 
+
 class CalDAVServiceMaker (LoggingMixIn):
     implements(IPlugin, IServiceMaker)
 
@@ -713,7 +730,6 @@
                 else:
                     return "%s: %s" % (frame.f_code.co_name, frame.f_lineno)
 
-
             return service
 
 
@@ -949,7 +965,6 @@
                         inherit=False
                     ).setServiceParent(connectionService)
 
-
         # Change log level back to what it was before
         setLogLevelForNamespace(None, oldLogLevel)
         return service
@@ -1009,9 +1024,11 @@
         """
         def slaveSvcCreator(pool, store):
             return self.requestProcessingService(options, store)
-        return self.storageService(slaveSvcCreator)
 
+        uid, gid = getSystemIDs(config.UserName, config.GroupName)
+        return self.storageService(slaveSvcCreator, uid=uid, gid=gid)
 
+
     def makeService_Utility(self, options):
         """
         Create a service to be used in a command-line utility
@@ -1023,7 +1040,8 @@
         def toolServiceCreator(pool, store):
             return config.UtilityServiceClass(store)
 
-        return self.storageService(toolServiceCreator)
+        uid, gid = getSystemIDs(config.UserName, config.GroupName)
+        return self.storageService(toolServiceCreator, uid=uid, gid=gid)
 
 
     def storageService(self, createMainService, uid=None, gid=None):
@@ -1176,7 +1194,6 @@
         else:
             uid = os.getuid()
 
-
         controlSocket = ControlSocket()
         controlSocket.addFactory(_LOG_ROUTE, logger)
         if config.ControlSocket:
@@ -1194,6 +1211,11 @@
         s.processMonitor = monitor
         monitor.setServiceParent(s)
 
+        if config.MemoryLimiter.Enabled:
+            memoryLimiter = MemoryLimitService(monitor, config.MemoryLimiter.Seconds,
+                config.MemoryLimiter.Bytes, config.MemoryLimiter.ResidentOnly)
+            memoryLimiter.setServiceParent(s)
+
         for name, pool in config.Memcached.Pools.items():
             if pool.ServerEnabled:
                 self.log_info(
@@ -1228,7 +1250,6 @@
             config.MultiProcess.ProcessCount = processCount
             self.log_info("Configuring %d processes." % (processCount,))
 
-
         # Open the socket(s) to be inherited by the slaves
         inheritFDs = []
         inheritSSLFDs = []
@@ -1367,7 +1388,7 @@
                             tmpSocket.connect(("127.0.0.1", testPort))
                             tmpSocket.shutdown(2)
                         except:
-                            numConnectFailures = numConnectFailures+1
+                            numConnectFailures = numConnectFailures + 1
                     # If the file didn't connect on any expected ports,
                     # consider it stale and remove it.
                     if numConnectFailures == len(testPorts):
@@ -1759,7 +1780,7 @@
         @param name: the name of the process to signal.
         @type signal: C{str}
         """
-        if not self.protocols.has_key(name):
+        if not name in self.protocols:
             return
         proc = self.protocols[name].transport
         try:
@@ -1774,15 +1795,15 @@
         the inherited implementation of startService because ProcessMonitor
         doesn't allow customization of subprocess environment).
         """
-        if self.protocols.has_key(name):
+        if name in self.protocols:
             return
         p = self.protocols[name] = DelayedStartupLoggingProtocol()
         p.service = self
         p.name = name
-        procObj, env, uid, gid= self.processes[name]
+        procObj, env, uid, gid = self.processes[name]
         self.timeStarted[name] = time()
 
-        childFDs = { 0 : "w", 1 : "r", 2 : "r" }
+        childFDs = {0 : "w", 1 : "r", 2 : "r"}
 
         childFDs.update(procObj.getFileDescriptors())
 
@@ -1793,7 +1814,6 @@
             childFDs=childFDs
         )
 
-
     _pendingStarts = 0
 
     def startProcess(self, name):
@@ -1824,12 +1844,12 @@
 
     def __repr__(self):
         l = []
-        for name, (procObj, uid, gid, env) in self.processes.items():
+        for name, (procObj, uid, gid, _ignore_env) in self.processes.items():
             uidgid = ''
             if uid is not None:
                 uidgid = str(uid)
             if gid is not None:
-                uidgid += ':'+str(gid)
+                uidgid += ':' + str(gid)
 
             if uidgid:
                 uidgid = '(' + uidgid + ')'
@@ -1855,6 +1875,7 @@
         """
         Ignore this IProtocol method, since I don't need a transport.
         """
+        pass
 
 
     def dataReceived(self, data):
@@ -1890,8 +1911,8 @@
         segments = self._breakLineIntoSegments(line)
         for segment in segments:
             self.lineReceived(segment)
-            
 
+
     def _breakLineIntoSegments(self, line):
         """
         Break a line into segments no longer than self.MAX_LENGTH.  Each
@@ -1902,16 +1923,17 @@
         @return: array of C{str}
         """
         length = len(line)
-        numSegments = length/self.MAX_LENGTH + (1 if length%self.MAX_LENGTH else 0)
+        numSegments = length / self.MAX_LENGTH + (1 if length % self.MAX_LENGTH else 0)
         segments = []
         for i in range(numSegments):
-            msg = line[i*self.MAX_LENGTH:(i+1)*self.MAX_LENGTH]
+            msg = line[i * self.MAX_LENGTH:(i + 1) * self.MAX_LENGTH]
             if i < numSegments - 1: # not the last segment
                 msg += self.CONTINUED_TEXT
             segments.append(msg)
         return segments
 
 
+
 class DelayedStartupLoggingProtocol(ProcessProtocol):
     """
     Logging protocol that handles lines which are too long.
@@ -2008,3 +2030,35 @@
                 return output.strip()
 
     return None
+
+def getSystemIDs(userName, groupName):
+    """
+    Return the system ID numbers corresponding to either:
+    A) the userName and groupName if non-empty, or
+    B) the real user ID and group ID of the process
+    @param userName: The name of the user to look up the ID of.  An empty
+        value indicates the real user ID of the process should be returned
+        instead.
+    @type userName: C{str}
+    @param groupName: The name of the group to look up the ID of.  An empty
+        value indicates the real group ID of the process should be returned
+        instead.
+    @type groupName: C{str}
+    """
+    if userName:
+        try:
+            uid = getpwnam(userName).pw_uid
+        except KeyError:
+           raise ConfigurationError("Invalid user name: %s" % (userName,))
+    else:
+        uid = getuid()
+
+    if groupName:
+        try:
+            gid = getgrnam(groupName).gr_gid
+        except KeyError:
+            raise ConfigurationError("Invalid group name: %s" % (groupName,))
+    else:
+        gid = getgid()
+
+    return uid, gid

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/test/test_caldav.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/test/test_caldav.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/test/test_caldav.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -20,6 +20,7 @@
 import grp
 
 from os.path import dirname, abspath
+from collections import namedtuple
 
 from zope.interface import implements
 
@@ -46,7 +47,7 @@
 from twext.python.plistlib import writePlist #@UnresolvedImport
 from twext.internet.tcp import MaxAcceptTCPServer, MaxAcceptSSLServer
 
-from twistedcaldav.config import config, ConfigDict
+from twistedcaldav.config import config, ConfigDict, ConfigurationError
 from twistedcaldav.stdconfig import DEFAULT_CONFIG
 
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
@@ -58,7 +59,7 @@
 from calendarserver.tap.caldav import (
     CalDAVOptions, CalDAVServiceMaker, CalDAVService, GroupOwnedUNIXServer,
     DelayedStartupProcessMonitor, DelayedStartupLineLogger, TwistdSlaveProcess,
-    _CONTROL_SERVICE_NAME
+    _CONTROL_SERVICE_NAME, getSystemIDs
 )
 from calendarserver.provision.root import RootResource
 from StringIO import StringIO
@@ -1197,3 +1198,73 @@
         self.assertEquals(output.count("START"), 2)
         self.assertEquals(output.count("STOP"), 2)
 
+
+class SystemIDsTests(TestCase):
+    """
+    Verifies the behavior of calendarserver.tap.caldav.getSystemIDs
+    """
+
+    def _wrappedFunction(self):
+        """
+        Return a copy of the getSystemIDs function with test implementations
+        of the ID lookup functions swapped into the namespace.
+        """
+
+        def _getpwnam(name):
+            if name == "exists":
+                Getpwnam = namedtuple("Getpwnam", ("pw_uid"))
+                return Getpwnam(42)
+            else:
+                raise KeyError(name)
+
+        def _getgrnam(name):
+            if name == "exists":
+                Getgrnam = namedtuple("Getgrnam", ("gr_gid"))
+                return Getgrnam(43)
+            else:
+                raise KeyError(name)
+
+        def _getuid():
+            return 44
+
+        def _getgid():
+            return 45
+
+        return type(getSystemIDs)(getSystemIDs.func_code,
+            {
+                "getpwnam" : _getpwnam,
+                "getgrnam" : _getgrnam,
+                "getuid" : _getuid,
+                "getgid" : _getgid,
+                "KeyError" : KeyError,
+                "ConfigurationError" : ConfigurationError,
+            }
+        )
+
+    def test_getSystemIDs_UserNameNotFound(self):
+        """
+        If userName is passed in but is not found on the system, raise a
+        ConfigurationError
+        """
+        self.assertRaises(ConfigurationError, self._wrappedFunction(),
+            "nonexistent", "exists")
+
+    def test_getSystemIDs_GroupNameNotFound(self):
+        """
+        If groupName is passed in but is not found on the system, raise a
+        ConfigurationError
+        """
+        self.assertRaises(ConfigurationError, self._wrappedFunction(),
+            "exists", "nonexistent")
+
+    def test_getSystemIDs_NamesNotSpecified(self):
+        """
+        If names are not provided, use the IDs of the process
+        """
+        self.assertEquals(self._wrappedFunction()("", ""), (44, 45))
+
+    def test_getSystemIDs_NamesSpecified(self):
+        """
+        If names are provided, use the IDs corresponding to those names
+        """
+        self.assertEquals(self._wrappedFunction()("exists", "exists"), (42, 43))

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/test/test_util.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/test/test_util.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/test/test_util.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -14,10 +14,11 @@
 # limitations under the License.
 ##
 
-from calendarserver.tap.util import computeProcessCount, directoryFromConfig
+from calendarserver.tap.util import computeProcessCount, directoryFromConfig, MemoryLimitService
 from twistedcaldav.test.util import TestCase
 from twistedcaldav.config import config
 from twistedcaldav.directory.augment import AugmentXMLDB
+from twisted.internet.task import Clock
 
 class ProcessCountTestCase(TestCase):
 
@@ -50,6 +51,8 @@
                 computeProcessCount(min, perCPU, perGB, cpuCount=cpu, memSize=mem)
             )
 
+
+
 class UtilTestCase(TestCase):
 
     def test_directoryFromConfig(self):
@@ -62,3 +65,78 @@
             # augmentService set to AugmentXMLDB
             if hasattr(service, "augmentService"):
                 self.assertTrue(isinstance(service.augmentService, AugmentXMLDB))
+
+
+
+# Stub classes for MemoryLimitServiceTestCase
+
+class StubProtocol(object):
+    def __init__(self, transport):
+        self.transport = transport
+
+
+
+class StubProcess(object):
+    def __init__(self, pid):
+        self.pid = pid
+
+
+
+class StubProcessMonitor(object):
+    def __init__(self, processes, protocols):
+        self.processes = processes
+        self.protocols = protocols
+        self.history = []
+
+
+    def stopProcess(self, name):
+        self.history.append(name)
+
+
+
+class MemoryLimitServiceTestCase(TestCase):
+
+    def test_checkMemory(self):
+        """
+        Set up stub objects to verify MemoryLimitService.checkMemory( )
+        only stops the processes whose memory usage exceeds the configured
+        limit, and skips memcached
+        """
+        data = {
+            # PID : (name, resident memory-in-bytes, virtual memory-in-bytes)
+            101 : ("process #1", 10, 1010),
+            102 : ("process #2", 30, 1030),
+            103 : ("process #3", 50, 1050),
+            99  : ("memcached-Default", 10, 1010),
+        }
+
+        processes = []
+        protocols = {}
+        for pid, (name, _ignore_resident, _ignore_virtual) in data.iteritems():
+            protocols[name] = StubProtocol(StubProcess(pid))
+            processes.append(name)
+        processMonitor = StubProcessMonitor(processes, protocols)
+        clock = Clock()
+        service = MemoryLimitService(processMonitor, 10, 15, True, reactor=clock)
+
+        # For testing, use a stub implementation of memory-usage lookup
+        def testMemoryForPID(pid, residentOnly):
+            return data[pid][1 if residentOnly else 2]
+        service._memoryForPID = testMemoryForPID
+
+        # After 5 seconds, nothing should have happened, since the interval is 10 seconds
+        service.startService()
+        clock.advance(5)
+        self.assertEquals(processMonitor.history, [])
+
+        # After 7 more seconds, processes 2 and 3 should have been stopped since their
+        # memory usage exceeds 10 bytes
+        clock.advance(7)
+        self.assertEquals(processMonitor.history, ['process #2', 'process #3'])
+
+        # Now switch to looking at virtual memory, in which case all 3 processes
+        # should be stopped
+        service._residentOnly = False
+        processMonitor.history = []
+        clock.advance(10)
+        self.assertEquals(processMonitor.history, ['process #1', 'process #2', 'process #3'])

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/util.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tap/util.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -23,12 +23,14 @@
     "getRootResource",
     "getDBPool",
     "FakeRequest",
+    "MemoryLimitService",
 ]
 
 import errno
 import os
 from time import sleep
 from socket import fromfd, AF_UNIX, SOCK_STREAM, socketpair
+import psutil
 
 from twext.python.filepath import CachingFilePath as FilePath
 from twext.python.log import Logger
@@ -37,6 +39,7 @@
 from twext.web2.http_headers import Headers
 from twext.web2.static import File as FileResource
 
+from twisted.application.service import Service
 from twisted.cred.portal import Portal
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet import reactor as _reactor
@@ -58,7 +61,8 @@
 from calendarserver.push.applepush import APNSubscriptionResource
 from twistedcaldav.directorybackedaddressbook import DirectoryBackedAddressBookResource
 from twistedcaldav.resource import AuthenticationWrapper
-from twistedcaldav.schedule import IScheduleInboxResource
+from twistedcaldav.scheduling.ischedule.dkim import DKIMUtils, DomainKeyResource
+from twistedcaldav.scheduling.ischedule.resource import IScheduleInboxResource
 from twistedcaldav.simpleresource import SimpleResource, SimpleRedirectResource
 from twistedcaldav.timezones import TimezoneCache
 from twistedcaldav.timezoneservice import TimezoneServiceResource
@@ -68,6 +72,7 @@
 from twext.enterprise.ienterprise import ORACLE_DIALECT
 from twext.enterprise.adbapi2 import ConnectionPool, ConnectionPoolConnection
 
+
 try:
     from twistedcaldav.authkerb import NegotiateCredentialFactory
     NegotiateCredentialFactory  # pacify pyflakes
@@ -125,7 +130,8 @@
         sharedBuffers=config.Postgres.SharedBuffers,
         maxConnections=config.Postgres.MaxConnections,
         options=config.Postgres.Options,
-        uid=uid, gid=gid
+        uid=uid, gid=gid,
+        spawnedDBUser=config.SpawnedDBUser
     )
 
 
@@ -285,7 +291,7 @@
     directories = []
 
     directoryClass = namedClass(config.DirectoryService.type)
-    principalResourceClass       = DirectoryPrincipalProvisioningResource
+    principalResourceClass = DirectoryPrincipalProvisioningResource
 
     log.info("Configuring directory service of type: %s"
         % (config.DirectoryService.type,))
@@ -347,6 +353,7 @@
     return directory
 
 
+
 def getRootResource(config, newStore, resources=None):
     """
     Set up directory service and resource hierarchy based on config.
@@ -372,16 +379,16 @@
     #
     # Default resource classes
     #
-    rootResourceClass               = RootResource
-    calendarResourceClass           = DirectoryCalendarHomeProvisioningResource
-    iScheduleResourceClass          = IScheduleInboxResource
-    timezoneServiceResourceClass    = TimezoneServiceResource
+    rootResourceClass = RootResource
+    calendarResourceClass = DirectoryCalendarHomeProvisioningResource
+    iScheduleResourceClass = IScheduleInboxResource
+    timezoneServiceResourceClass = TimezoneServiceResource
     timezoneStdServiceResourceClass = TimezoneStdServiceResource
-    webCalendarResourceClass        = WebCalendarResource
-    webAdminResourceClass           = WebAdminResource
-    addressBookResourceClass        = DirectoryAddressBookHomeProvisioningResource
+    webCalendarResourceClass = WebCalendarResource
+    webAdminResourceClass = WebAdminResource
+    addressBookResourceClass = DirectoryAddressBookHomeProvisioningResource
     directoryBackedAddressBookResourceClass = DirectoryBackedAddressBookResource
-    apnSubscriptionResourceClass    = APNSubscriptionResource
+    apnSubscriptionResourceClass = APNSubscriptionResource
 
     directory = directoryFromConfig(config)
 
@@ -458,12 +465,10 @@
         if credFactory:
             credentialFactories.append(credFactory)
 
-
     #
     # Setup Resource hierarchy
     #
-    log.info("Setting up document root at: %s"
-                  % (config.DocumentRoot,))
+    log.info("Setting up document root at: %s" % (config.DocumentRoot,))
 
     principalCollection = directory.principalCollection
 
@@ -498,10 +503,10 @@
             # remove /directory from previous runs that may have created it
             try:
                 FilePath(directoryPath).remove()
-                log.info("Deleted: %s" %    directoryPath)
+                log.info("Deleted: %s" % directoryPath)
             except (OSError, IOError), e:
                 if e.errno != errno.ENOENT:
-                    log.error("Could not delete: %s : %r" %  (directoryPath, e,))
+                    log.error("Could not delete: %s : %r" % (directoryPath, e,))
 
     log.info("Setting up root resource: %r" % (rootResourceClass,))
 
@@ -510,7 +515,6 @@
         principalCollections=(principalCollection,),
     )
 
-
     root.putChild("principals", principalCollection)
     if config.EnableCalDAV:
         root.putChild("calendars", calendarCollection)
@@ -533,6 +537,7 @@
             (config.EnableCalDAV, "caldav", "/",),
             (config.EnableCardDAV, "carddav", "/",),
             (config.TimezoneService.Enabled, "timezone", "/stdtimezones",),
+            (config.Scheduling.iSchedule.Enabled, "ischedule", "/ischedule"),
         ):
             if enabled:
                 if config.EnableSSL:
@@ -577,14 +582,16 @@
             root,
         )
         root.putChild("stdtimezones", timezoneStdService)
-        
+
         # TODO: we only want the master to do this
         if _reactor._started:
             _reactor.callLater(0, timezoneStdService.onStartup)
         else:
             addSystemEventTrigger("after", "startup", timezoneStdService.onStartup)
 
-    # iSchedule service is optional
+    #
+    # iSchedule service
+    #
     if config.Scheduling.iSchedule.Enabled:
         log.info("Setting up iSchedule inbox resource: %r"
                       % (iScheduleResourceClass,))
@@ -595,6 +602,18 @@
         )
         root.putChild("ischedule", ischedule)
 
+        # Do DomainKey resources
+        DKIMUtils.validConfiguration(config)
+        if config.Scheduling.iSchedule.DKIM.Enabled:
+            log.info("Setting up domainkey resource: %r" % (DomainKeyResource,))
+            domain = config.Scheduling.iSchedule.DKIM.Domain if config.Scheduling.iSchedule.DKIM.Domain else config.ServerHostName
+            dk = DomainKeyResource(
+                domain,
+                config.Scheduling.iSchedule.DKIM.KeySelector,
+                config.Scheduling.iSchedule.DKIM.PublicKeyFile,
+            )
+            wellKnownResource.putChild("domainkey", dk)
+
     #
     # WebCal
     #
@@ -636,10 +655,9 @@
     log.info("Setting up Timezone Cache")
     TimezoneCache.create()
 
-
     log.info("Configuring authentication wrapper")
 
-    overrides = { }
+    overrides = {}
     if resources:
         for path, cls, args, schemes in resources:
 
@@ -679,6 +697,7 @@
     return logWrapper
 
 
+
 def getDBPool(config):
     """
     Inspect configuration to determine what database connection pool
@@ -755,8 +774,6 @@
 
 
 
-
-
 class FakeRequest(object):
 
     def __init__(self, rootResource, method, path, uri='/'):
@@ -768,6 +785,7 @@
         self._urlsByResource = {}
         self.headers = Headers()
 
+
     @inlineCallbacks
     def _getChild(self, resource, segments):
         if not segments:
@@ -776,6 +794,7 @@
         child, remaining = (yield resource.locateChild(self, segments))
         returnValue((yield self._getChild(child, remaining)))
 
+
     @inlineCallbacks
     def locateResource(self, url):
         url = url.strip("/")
@@ -785,6 +804,7 @@
             self._rememberResource(resource, url)
         returnValue(resource)
 
+
     @inlineCallbacks
     def locateChildResource(self, parent, childName):
         if parent is None or childName is None:
@@ -799,17 +819,20 @@
             self._rememberResource(resource, url)
         returnValue(resource)
 
+
     def _rememberResource(self, resource, url):
         self._resourcesByURL[url] = resource
         self._urlsByResource[resource] = url
         return resource
 
+
     def _forgetResource(self, resource, url):
-        if self._resourcesByURL.has_key(url):
+        if url in self._resourcesByURL:
             del self._resourcesByURL[url]
-        if self._urlsByResource.has_key(resource):
+        if resource in self._urlsByResource:
             del self._urlsByResource[resource]
 
+
     def urlForResource(self, resource):
         url = self._urlsByResource.get(resource, None)
         if url is None:
@@ -818,6 +841,99 @@
             raise NoURLForResourceError(resource)
         return url
 
-    def addResponseFilter(*args, **kwds):
+
+    def addResponseFilter(self, *args, **kwds):
         pass
 
+
+
+def memoryForPID(pid, residentOnly=True):
+    """
+    Return the amount of memory in use for the given process.  If residentOnly is True,
+        then RSS is returned; if False, then virtual memory is returned.
+    @param pid: process id
+    @type pid: C{int}
+    @param residentOnly: Whether only resident memory should be included
+    @type residentOnly: C{boolean}
+    @return: Memory used by process in bytes
+    @rtype: C{int}
+    """
+    memoryInfo = psutil.Process(pid).get_memory_info()
+    return memoryInfo.rss if residentOnly else memoryInfo.vms
+
+
+
+class MemoryLimitService(Service, object):
+    """
+    A service which when paired with a DelayedStartupProcessMonitor will periodically
+    examine the memory usage of the monitored processes and stop any which exceed
+    a configured limit.  Memcached processes are ignored.
+    """
+
+    def __init__(self, processMonitor, intervalSeconds, limitBytes, residentOnly, reactor=None):
+        """
+        @param processMonitor: the DelayedStartupProcessMonitor
+        @param intervalSeconds: how often to check
+        @type intervalSeconds: C{int}
+        @param limitBytes: any monitored process over this limit is stopped
+        @type limitBytes: C{int}
+        @param residentOnly: whether only resident memory should be included
+        @type residentOnly: C{boolean}
+        @param reactor: for testing
+        """
+        self._processMonitor = processMonitor
+        self._seconds = intervalSeconds
+        self._bytes = limitBytes
+        self._residentOnly = residentOnly
+        self._delayedCall = None
+        if reactor is None:
+            from twisted.internet import reactor
+        self._reactor = reactor
+
+        # Unit tests can swap out _memoryForPID
+        self._memoryForPID = memoryForPID
+
+
+    def startService(self):
+        """
+        Start scheduling the memory checks
+        """
+        super(MemoryLimitService, self).startService()
+        self._delayedCall = self._reactor.callLater(self._seconds, self.checkMemory)
+
+
+    def stopService(self):
+        """
+        Stop checking memory
+        """
+        super(MemoryLimitService, self).stopService()
+        if self._delayedCall is not None and self._delayedCall.active():
+            self._delayedCall.cancel()
+            self._delayedCall = None
+
+
+    def checkMemory(self):
+        """
+        Stop any processes monitored by our paired processMonitor whose resident
+        memory exceeds our configured limitBytes.  Reschedule intervalSeconds in
+        the future.
+        """
+        try:
+            for name in self._processMonitor.processes:
+                if name.startswith("memcached"):
+                    continue
+                proto = self._processMonitor.protocols.get(name, None)
+                if proto is not None:
+                    proc = proto.transport
+                    pid = proc.pid
+                    try:
+                        memory = self._memoryForPID(pid, self._residentOnly)
+                    except Exception, e:
+                        log.error("Unable to determine memory usage of PID: %d (%s)" % (pid, e))
+                        continue
+                    if memory > self._bytes:
+                        log.warn("Killing large process: %s PID:%d %s:%d" %
+                            (name, pid, "Resident" if self._residentOnly else "Virtual", memory))
+                        self._processMonitor.stopProcess(name)
+        finally:
+            self._delayedCall = self._reactor.callLater(self._seconds, self.checkMemory)

Copied: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dkimtool.py (from rev 9988, CalendarServer/trunk/calendarserver/tools/dkimtool.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dkimtool.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/dkimtool.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,299 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2012 Apple Inc. All 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 Crypto.PublicKey import RSA
+from StringIO import StringIO
+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
+
+
+def _doKeyGeneration(options):
+
+    key = RSA.generate(options["key-size"])
+    output = key.exportKey()
+    lineBreak = False
+    if options["key"]:
+        open(options["key"], "w").write(output)
+    else:
+        print output
+        lineBreak = True
+
+    output = key.publickey().exportKey()
+    if options["pub-key"]:
+        open(options["pub-key"], "w").write(output)
+    else:
+        if lineBreak:
+            print
+        print output
+        lineBreak = True
+
+    if options["txt"]:
+        output = "".join(output.splitlines()[1:-1])
+        txt = "v=DKIM1; p=%s" % (output,)
+        if lineBreak:
+            print
+        print txt
+
+
+
+ at inlineCallbacks
+def _doRequest(options):
+
+    # Parse the HTTP file
+    request = open(options["request"]).read()
+    method, uri, headers, stream = _parseRequest(request)
+
+    # Setup signing headers
+    sign_headers = options["signing"]
+    if sign_headers is None:
+        sign_headers = []
+        for hdr in ("Host", "Content-Type", "Originator", "Recipient+"):
+            if headers.hasHeader(hdr.rstrip("+")):
+                sign_headers.append(hdr)
+    else:
+        sign_headers = sign_headers.split(":")
+
+    dkim = DKIMRequest(
+        method,
+        uri,
+        headers,
+        stream,
+        options["domain"],
+        options["selector"],
+        options["key"],
+        options["algorithm"],
+        sign_headers,
+        True,
+        True,
+        False,
+        int(options["expire"]),
+    )
+    if options["fake-time"]:
+        dkim.time = "100"
+        dkim.expire = "200"
+        dkim.message_id = "1"
+    yield dkim.sign()
+
+    s = StringIO()
+    _writeRequest(dkim, s)
+    print s.getvalue()
+
+
+
+ at inlineCallbacks
+def _doVerify(options):
+    # Parse the HTTP file
+    verify = open(options["verify"]).read()
+    method, uri, headers, stream = _parseRequest(verify)
+
+    request = ClientRequest(method, uri, headers, stream)
+
+    # Check for local public key
+    if options["pub-key"]:
+        PublicKeyLookup_File.pubkeyfile = options["pub-key"]
+        lookup = (PublicKeyLookup_File,)
+    else:
+        lookup = None
+
+    dkim = DKIMVerifier(request, lookup)
+    if options["fake-time"]:
+        dkim.time = 100
+
+    try:
+        yield dkim.verify()
+    except DKIMVerificationError, e:
+        print "Verification Failed: %s" % (e,)
+    else:
+        print "Verification Succeeded"
+
+
+
+def _parseRequest(request):
+
+    lines = request.splitlines(True)
+
+    method, uri, _ignore_version = lines.pop(0).split()
+
+    hdrs = []
+    body = None
+    for line in lines:
+        if body is not None:
+            body.append(line)
+        elif line.strip() == "":
+            body = []
+        elif line[0] in (" ", "\t"):
+            hdrs[-1] += line
+        else:
+            hdrs.append(line)
+
+    headers = Headers()
+    for hdr in hdrs:
+        name, value = hdr.split(':', 1)
+        headers.addRawHeader(name, value.strip())
+
+    stream = MemoryStream("".join(body))
+
+    return method, uri, headers, stream
+
+
+
+def _writeRequest(request, f):
+
+    f.write("%s %s HTTP/1.1\r\n" % (request.method, request.uri,))
+    for name, valuelist in request.headers.getAllRawHeaders():
+        for value in valuelist:
+            f.write("%s: %s\r\n" % (name, value))
+    f.write("\r\n")
+    f.write(request.stream.read())
+
+
+
+class PublicKeyLookup_File(PublicKeyLookup):
+
+    method = "*"
+    pubkeyfile = None
+
+    def getPublicKey(self):
+        """
+        Do the key lookup using the actual lookup method.
+        """
+        return RSA.importKey(open(self.pubkeyfile).read())
+
+
+
+def usage(e=None):
+    if e:
+        print e
+        print ""
+    try:
+        DKIMToolOptions().opt_help()
+    except SystemExit:
+        pass
+    if e:
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
+
+description = """Usage: dkimtool [options]
+Options:
+    -h            Print this help and exit
+
+    # Key Generation
+    --key-gen          Generate private/public key files
+    --key FILE         Private key file to create [stdout]
+    --pub-key FILE     Public key file to create [stdout]
+    --key-size SIZE    Key size [1024]
+    --txt              Also generate the public key TXT record
+    --fake-time        Use fake t=, x= values when signing and also
+                       ignore expiration on verification
+
+    # Request
+    --request FILE      An HTTP request to sign
+    --algorithm ALGO    Signature algorithm [rsa-sha256]
+    --domain DOMAIN     Signature domain [example.com]
+    --selector SELECTOR Signature selector [dkim]
+    --key FILE          Private key to use
+    --signing HEADERS   List of headers to sign [automatic]
+    --expire SECONDS    When to expire signature [no expiry]
+
+    # Verify
+    --verify FILE       An HTTP request to verify
+    --pkey   FILE       Public key to use in place of
+                        q= lookup
+
+Description:
+    This utility is for testing DKIM signed HTTP requests. Key operations are:
+
+    --key-gen: generate a private/public RSA key.
+
+    --request: sign an HTTP request.
+
+    --verify:  verify a signed HTTP request.
+
+"""
+
+
+class DKIMToolOptions(Options):
+    """
+    Command-line options for 'calendarserver_dkimtool'
+    """
+
+    synopsis = description
+
+    optFlags = [
+        ['verbose', 'v', "Verbose logging."],
+        ['key-gen', 'g', "Generate private/public key files"],
+        ['txt', 't', "Also generate the public key TXT record"],
+        ['fake-time', 'f', "Fake time values for signing/verification"],
+    ]
+
+    optParameters = [
+        ['key', 'k', None, "Private key file to create [default: stdout]"],
+        ['pub-key', 'p', None, 'Public key file to create [default: stdout]'],
+        ['key-size', 'x', 1024, 'Key size'],
+        ['request', 'r', None, 'An HTTP request to sign'],
+        ['algorithm', 'a', RSA256, 'Signature algorithm'],
+        ['domain', 'd', 'example.com', 'Signature domain'],
+        ['selector', 's', 'dkim', 'Signature selector'],
+        ['signing', 'h', None, 'List of headers to sign [automatic]'],
+        ['expire', 'e', 3600, 'When to expire signature'],
+        ['verify', 'w', None, 'An HTTP request to verify'],
+    ]
+
+    def __init__(self):
+        super(DKIMToolOptions, self).__init__()
+        self.outputName = '-'
+
+
+
+ at inlineCallbacks
+def _runInReactor(fn, options):
+
+    try:
+        yield fn(options)
+    except Exception, e:
+        print e
+    finally:
+        reactor.stop()
+
+
+
+def main(argv=sys.argv, stderr=sys.stderr):
+    options = DKIMToolOptions()
+    options.parseOptions(argv[1:])
+
+    if options["key-gen"]:
+        _doKeyGeneration(options)
+    elif options["request"]:
+        reactor.callLater(0, _runInReactor, _doRequest, options)
+        reactor.run()
+    elif options["verify"]:
+        reactor.callLater(0, _runInReactor, _doVerify, options)
+        reactor.run()
+    else:
+        usage("Invalid options")
+
+if __name__ == '__main__':
+    main()

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/shell/vfs.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/shell/vfs.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/shell/vfs.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -256,19 +256,43 @@
 
     @inlineCallbacks
     def list(self):
-        result = set()
+        results = {}
 
         # FIXME: This should be the merged total of calendar homes and address book homes.
         # FIXME: Merge in directory UIDs also?
-        # FIXME: Add directory info (eg. name) to listing
+        # FIXME: Add directory info (eg. name) to list entry
 
-        for txn, home in (yield self.service.store.eachCalendarHome()):
-            result.add(ListEntry(self, PrincipalHomeFolder, home.uid()))
+        def addResult(uid):
+            if uid in results:
+                return
 
-        returnValue(result)
+            record = self.service.directory.recordWithUID(uid)
+            if record:
+                info = {
+                    "Record Type": record.recordType,
+                    "Short Name" : record.shortNames[0],
+                    "Full Name"  : record.fullName,
+                }
+            else:
+                info = {}
 
+            results[uid] = ListEntry(self, PrincipalHomeFolder, uid, **info)
 
+        txn = self.service.store.newTransaction()
+        try:
+            for home in (yield txn.calendarHomes()):
+                addResult(home.uid())
+            for home in (yield txn.addressbookHomes()):
+                addResult(home.uid())
+        finally:
+            (yield txn.abort())
 
+        returnValue(results.itervalues())
+
+        list.fieldNames = ("Record Name", "Short Name", "Full Name")
+
+
+
 class RecordFolder(Folder):
     def _recordForName(self, name):
         recordTypeAttr = "recordType_" + self.recordType
@@ -358,67 +382,76 @@
         if not hasattr(self, "_didInitChildren"):
             txn = self.service.store.newTransaction()
 
-            if (
-                self.record is not None and
-                self.service.config.EnableCalDAV and 
-                self.record.enabledForCalendaring
-            ):
-                create = True
-            else:
-                create = False
+            try:
+                if (
+                    self.record is not None and
+                    self.service.config.EnableCalDAV and 
+                    self.record.enabledForCalendaring
+                ):
+                    create = True
+                else:
+                    create = False
 
-            # Try assuming it exists
-            home = (yield txn.calendarHomeWithUID(self.uid, create=False))
+                # Try assuming it exists
+                home = (yield txn.calendarHomeWithUID(self.uid, create=False))
 
-            if home is None and create:
-                # Doesn't exist, so create it in a different
-                # transaction, to avoid having to commit the live
-                # transaction.
-                txnTemp = self.service.store.newTransaction()
-                home = (yield txnTemp.calendarHomeWithUID(self.uid, create=True))
-                (yield txnTemp.commit())
+                if home is None and create:
+                    # Doesn't exist, so create it in a different
+                    # transaction, to avoid having to commit the live
+                    # transaction.
+                    txnTemp = self.service.store.newTransaction()
+                    try:
+                        home = (yield txnTemp.calendarHomeWithUID(self.uid, create=True))
+                        (yield txnTemp.commit())
 
-                # Fetch the home again. This time we expect it to be there.
-                home = (yield txn.calendarHomeWithUID(self.uid, create=False))
-                assert home
+                        # Fetch the home again. This time we expect it to be there.
+                        home = (yield txn.calendarHomeWithUID(self.uid, create=False))
+                        assert home
+                    finally:
+                        (yield txn.abort())
 
-            if home:
-                self._children["calendars"] = CalendarHomeFolder(
-                    self.service,
-                    self.path + ("calendars",),
-                    home,
-                    self.record,
-                )
+                if home:
+                    self._children["calendars"] = CalendarHomeFolder(
+                        self.service,
+                        self.path + ("calendars",),
+                        home,
+                        self.record,
+                    )
 
-            if (
-                self.record is not None and
-                self.service.config.EnableCardDAV and 
-                self.record.enabledForAddressBooks
-            ):
-                create = True
-            else:
-                create = False
+                if (
+                    self.record is not None and
+                    self.service.config.EnableCardDAV and 
+                    self.record.enabledForAddressBooks
+                ):
+                    create = True
+                else:
+                    create = False
 
-            # Again, assume it exists
-            home = (yield txn.addressbookHomeWithUID(self.uid))
+                # Again, assume it exists
+                home = (yield txn.addressbookHomeWithUID(self.uid))
 
-            if not home and create:
-                # Repeat the above dance.
-                txnTemp = self.service.store.newTransaction()
-                home = (yield txnTemp.addressbookHomeWithUID(self.uid, create=True))
-                (yield txnTemp.commit())
+                if not home and create:
+                    # Repeat the above dance.
+                    txnTemp = self.service.store.newTransaction()
+                    try:
+                        home = (yield txnTemp.addressbookHomeWithUID(self.uid, create=True))
+                        (yield txnTemp.commit())
 
-                # Fetch the home again. This time we expect it to be there.
-                home = (yield txn.addressbookHomeWithUID(self.uid, create=False))
-                assert home
+                        # Fetch the home again. This time we expect it to be there.
+                        home = (yield txn.addressbookHomeWithUID(self.uid, create=False))
+                        assert home
+                    finally:
+                        (yield txn.abort())
 
-            if home:
-                self._children["addressbooks"] = AddressBookHomeFolder(
-                    self.service,
-                    self.path + ("addressbooks",),
-                    home,
-                    self.record,
-                )
+                if home:
+                    self._children["addressbooks"] = AddressBookHomeFolder(
+                        self.service,
+                        self.path + ("addressbooks",),
+                        home,
+                        self.record,
+                    )
+            finally:
+                (yield txn.abort())
 
         self._didInitChildren = True
 
@@ -462,8 +495,19 @@
     @inlineCallbacks
     def list(self):
         calendars = (yield self.home.calendars())
-        returnValue((ListEntry(self, CalendarFolder, c.name()) for c in calendars))
+        result = []
+        for calendar in calendars:
+            displayName = calendar.displayName()
+            if displayName is None:
+                displayName = "(unset)"
 
+            info = {
+                "Display Name": displayName,
+                "Sync Token"  : (yield calendar.syncToken()),
+            }
+            result.append(ListEntry(self, CalendarFolder, calendar.name(), **info))
+        returnValue(result)
+
     @inlineCallbacks
     def describe(self):
         description = ["Calendar home:\n"]

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-apple.plist	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-apple.plist	2012-10-26 21:24:44 UTC (rev 9991)
@@ -408,8 +408,8 @@
         <key>AddressPatterns</key>
         <array>
         </array>
-        <key>Servers</key>
-        <string>servertoserver.xml</string>
+        <key>RemoteServers</key>
+        <string>remoteservers.xml</string>
       </dict>
 
       <!-- iMIP protocol options -->

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-partitioning-primary.plist
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-partitioning-primary.plist	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-partitioning-primary.plist	2012-10-26 21:24:44 UTC (rev 9991)
@@ -26,7 +26,7 @@
 	    <key>Enabled</key>
 	    <true/>
 	    <key>ConfigFile</key>
-	    <string>servers.xml</string>
+	    <string>localservers.xml</string>
 	    <key>MaxClients</key>
 	    <integer>5</integer>
 	</dict>

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-partitioning-secondary.plist
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-partitioning-secondary.plist	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-partitioning-secondary.plist	2012-10-26 21:24:44 UTC (rev 9991)
@@ -26,7 +26,7 @@
 	    <key>Enabled</key>
 	    <true/>
 	    <key>ConfigFile</key>
-	    <string>servers.xml</string>
+	    <string>localservers.xml</string>
 	    <key>MaxClients</key>
 	    <integer>5</integer>
 	</dict>

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist	2012-10-26 21:24:44 UTC (rev 9991)
@@ -760,8 +760,8 @@
         <key>AddressPatterns</key>
         <array>
         </array>
-        <key>Servers</key>
-        <string>servertoserver-test.xml</string>
+        <key>RemoteServers</key>
+        <string>remoteservers-test.xml</string>
       </dict>
 
       <!-- iMIP protocol options -->

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd.plist
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd.plist	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd.plist	2012-10-26 21:24:44 UTC (rev 9991)
@@ -407,8 +407,8 @@
         <key>AddressPatterns</key>
         <array>
         </array>
-        <key>Servers</key>
-        <string>servertoserver.xml</string>
+        <key>RemoteServers</key>
+        <string>remoteservers.xml</string>
       </dict>
 
       <!-- iMIP protocol options -->

Copied: CalendarServer/branches/users/gaya/sharedgroups/conf/localservers-test.xml (from rev 9988, CalendarServer/trunk/conf/localservers-test.xml)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/localservers-test.xml	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/localservers-test.xml	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2011 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE servers SYSTEM "servers.dtd">
+
+<servers>
+  <server>
+    <id>00001</id>
+    <uri>http://localhost:8008</uri>
+    <partitions>
+    	<partition>
+    		<id>00001</id>
+    		<uri>http://localhost:8008</uri>
+    	</partition>
+    	<partition>
+    		<id>00002</id>
+    		<uri>http://localhost:8108</uri>
+    	</partition>
+    </partitions>
+  </server>
+</servers>

Copied: CalendarServer/branches/users/gaya/sharedgroups/conf/localservers.xml (from rev 9988, CalendarServer/trunk/conf/localservers.xml)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/localservers.xml	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/localservers.xml	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2011 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE servers SYSTEM "servers.dtd">
+
+<servers>
+  <!--
+  <server>
+    <id>A</id>
+    <uri>https://caldav1.example.com:8843</uri>
+  </server>
+  <server>
+    <id>B</id>
+    <uri>https://caldav2.example.com:8843</uri>
+    <allowed-from>127.0.0.1</allowed-from>
+    <allowed-from>example.local</allowed-from>
+    <shared-secret>ABC</shared-secret>
+    <partitions>
+    	<partition>
+    		<id>00001</id>
+    		<url>https://machine1.example.com:8443</url>
+    	</partition>
+    	<partition>
+    		<id>00002</id>
+    		<url>https://machine2.example.com:8443</url>
+    	</partition>
+    </partitions>
+  </server>
+  -->
+</servers>

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

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

Deleted: CalendarServer/branches/users/gaya/sharedgroups/conf/servers-test.xml
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/servers-test.xml	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/servers-test.xml	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-Copyright (c) 2011 Apple Inc. All rights reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
- -->
-
-<!DOCTYPE servers SYSTEM "servers.dtd">
-
-<servers>
-  <server>
-    <id>00001</id>
-    <uri>http://localhost:8008</uri>
-    <partitions>
-    	<partition>
-    		<id>00001</id>
-    		<uri>http://localhost:8008</uri>
-    	</partition>
-    	<partition>
-    		<id>00002</id>
-    		<uri>http://localhost:8108</uri>
-    	</partition>
-    </partitions>
-  </server>
-</servers>

Deleted: CalendarServer/branches/users/gaya/sharedgroups/conf/servers.xml
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/servers.xml	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/servers.xml	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-Copyright (c) 2011 Apple Inc. All rights reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
- -->
-
-<!DOCTYPE servers SYSTEM "servers.dtd">
-
-<servers>
-  <!--
-  <server>
-    <id>A</id>
-    <uri>https://caldav1.example.com:8843</uri>
-  </server>
-  <server>
-    <id>B</id>
-    <uri>https://caldav2.example.com:8843</uri>
-    <allowed-from>127.0.0.1</allowed-from>
-    <allowed-from>example.local</allowed-from>
-    <shared-secret>ABC</shared-secret>
-    <partitions>
-    	<partition>
-    		<id>00001</id>
-    		<url>https://machine1.example.com:8443</url>
-    	</partition>
-    	<partition>
-    		<id>00002</id>
-    		<url>https://machine2.example.com:8443</url>
-    	</partition>
-    </partitions>
-  </server>
-  -->
-</servers>

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

Deleted: CalendarServer/branches/users/gaya/sharedgroups/conf/servertoserver.xml
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/servertoserver.xml	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/servertoserver.xml	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-Copyright (c) 2006-2007 Apple Inc. All rights reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
- -->
-
-<!DOCTYPE servers SYSTEM "servertoserver.dtd">
-
-<servers>
-  <!--
-  <server>
-    <uri>https://localhost:8543/inbox</uri>
-    <allow-requests-from/>
-    <allow-requests-to/>
-    <domains>
-    	<domain>example.org</domain>
-    </domains>
-    <hosts>
-    	<host>127.0.0.1</host>
-    </hosts>
-  </server>
-  -->
-</servers>

Modified: CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarpromotion.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarpromotion.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/calendarpromotion.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -24,6 +24,23 @@
 GROUP_NAME = "calendar"
 LOG_DIR = "/var/log/caldavd"
 
+
+def updatePlist(plistData):
+    """
+    Update the passed-in plist data with new values for disabling the XMPPNotifier, and
+    to set the DSN to use the server-specific Postgres.
+
+    @param plistData: the plist data to update in place
+    @type plistData: C{dict}
+    """
+    try:
+        if plistData["Notifications"]["Services"]["XMPPNotifier"]["Enabled"]:
+            plistData["Notifications"]["Services"]["XMPPNotifier"]["Enabled"] = False
+    except KeyError:
+        pass
+    plistData["DSN"] = "/Library/Server/PostgreSQL For Server Services/Socket:caldav:caldav:::"
+
+
 def main():
 
     try:
@@ -45,17 +62,11 @@
     if os.path.exists(plistPath):
         try:
             plistData = readPlist(plistPath)
+            updatePlist(plistData)
+            writePlist(plistData, plistPath)
 
-            # Disable XMPPNotifier now that we're directly talking to APNS
-            try:
-                if plistData["Notifications"]["Services"]["XMPPNotifier"]["Enabled"]:
-                    plistData["Notifications"]["Services"]["XMPPNotifier"]["Enabled"] = False
-                writePlist(plistData, plistPath)
-            except KeyError:
-                pass
-
         except Exception, e:
-            print "Unable to disable XMPP in %s: %s" % (plistPath, e)
+            print "Unable to disable update values in %s: %s" % (plistPath, e)
 
     else:
         # Copy configuration

Copied: CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_promotion.py (from rev 9988, CalendarServer/trunk/contrib/migration/test/test_promotion.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_promotion.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/contrib/migration/test/test_promotion.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,60 @@
+##
+# Copyright (c) 2012 Apple Inc. All 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 twistedcaldav.test.util
+from contrib.migration.calendarpromotion import updatePlist
+
+class PromotionTests(twistedcaldav.test.util.TestCase):
+    """
+    Calendar Server Promotion Tests
+    """
+
+    def test_updatePlist(self):
+        """
+        Verify XMPPNotifier is disabled and DSN is updated
+        """
+
+        orig = {
+            "ignored" : "ignored",
+        }
+        expected = {
+            "ignored" : "ignored",
+            "DSN" : "/Library/Server/PostgreSQL For Server Services/Socket:caldav:caldav:::",
+        }
+        updatePlist(orig)
+        self.assertEquals(orig, expected)
+
+        orig = {
+            "Notifications" : {
+                "Services" : {
+                    "XMPPNotifier" : {
+                        "Enabled" : True
+                    }
+                }
+            }
+        }
+        expected = {
+            "Notifications" : {
+                "Services" : {
+                    "XMPPNotifier" : {
+                        "Enabled" : False
+                    }
+                }
+            },
+            "DSN" : "/Library/Server/PostgreSQL For Server Services/Socket:caldav:caldav:::",
+        }
+        updatePlist(orig)
+        self.assertEquals(orig, expected)

Modified: CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/loadtest/config.plist
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/loadtest/config.plist	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/loadtest/config.plist	2012-10-26 21:24:44 UTC (rev 9991)
@@ -257,55 +257,6 @@
 						</dict>
 					</dict>
 
-					<!-- This profile invites new attendees to existing events.
-					     This profile should no longer be used - use RealisticInviter instead. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Inviter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the frequency at which new invitations will be sent out. -->
-							<key>sendInvitationDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.NormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mu gives the mean of the normal distribution (in seconds). -->
-									<key>mu</key>
-									<integer>60</integer>
-
-									<!-- and sigma gives its standard deviation. -->
-									<key>sigma</key>
-									<integer>5</integer>
-								</dict>
-							</dict>
-
-							<!-- Define the distribution of who will be invited to an event. Each
-								set of credentials loaded by the load tester has an index; samples from this
-								distribution will be added to that index to arrive at the index of some other
-								credentials, which will be the target of the invitation. -->
-							<key>inviteeDistanceDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.UniformIntegerDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- The minimum value (inclusive) of the uniform distribution. -->
-									<key>min</key>
-									<integer>-100</integer>
-									<!-- The maximum value (exclusive) of the uniform distribution. -->
-									<key>max</key>
-									<integer>101</integer>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
 					<!-- This profile invites some number of new attendees to new events. -->
 					<dict>
 						<key>class</key>
@@ -352,10 +303,10 @@
 								<dict>
 									<!-- The minimum value (inclusive) of the uniform distribution. -->
 									<key>min</key>
-									<integer>-100</integer>
+									<integer>0</integer>
 									<!-- The maximum value (exclusive) of the uniform distribution. -->
 									<key>max</key>
-									<integer>101</integer>
+									<integer>99</integer>
 								</dict>
 							</dict>
 
@@ -385,7 +336,7 @@
 									<integer>6</integer>
 									<!-- maximum -->
 									<key>maximum</key>
-									<real>100</real>
+									<real>60</real>
 								</dict>
 							</dict>
 
@@ -520,7 +471,7 @@
 						<key>params</key>
 						<dict>
 							<key>enabled</key>
-							<true/>
+							<false/>
 
 							<!-- Define the interval (in seconds) at which this profile will use
 								its client to create a new task. -->

Modified: CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/loadtest/ical.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/loadtest/ical.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/contrib/performance/loadtest/ical.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -771,7 +771,10 @@
                         if caldavxml.supported_calendar_component_set in nodes:
                             for comp in nodes[caldavxml.supported_calendar_component_set].getchildren():
                                 componentTypes.add(comp.get("name").upper())
-                    
+
+                    if textProps.get(davxml.displayname, None) == "tasks":
+                        # Until we can fix caldavxml.supported_calendar_component_set
+                        break                    
                     changeTag = davxml.sync_token if self.supportSync else csxml.getctag
                     calendars.append(Calendar(
                             nodeType.tag,

Modified: CalendarServer/branches/users/gaya/sharedgroups/doc/Admin/ExtendedLogItems.rst
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/doc/Admin/ExtendedLogItems.rst	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/doc/Admin/ExtendedLogItems.rst	2012-10-26 21:24:44 UTC (rev 9991)
@@ -21,7 +21,7 @@
 
   ``i``
 
-    the port number of the server instance emitting the log
+    the index number of the server instance emitting the log; corresponds to the slave number shown in process title.
 
   ``t``
 
@@ -71,17 +71,27 @@
 
     the value of the X-Forwarded-For header, if present
 
-In the following example, we see a ``CalDAV:calendar-multiget``
-``REPORT`` for 32 resources in a user's calendar, which was handled by
-instance ``8459`` in 183.0ms, with one outstanding request (the one
-being logged):
+  ``fb-cached``
 
+    When doing free-busy queries, this is the number of calendars queried for which free-busy info was already cached
+
+  ``fb-uncached``
+
+    When doing free-busy queries, this is the number of calendars queried for which free-busy info was NOT already cached
+
+  ``cl``
+
+    Content length, in bytes
+
+In the following example, we see a free-busy ``POST``
+requesting availability for two users, which was handled by
+instance ``1`` in 782.6i ms. This instance was only processing one request at the time this was logged (or=1). Of the two calendars targeted by the free-busy query, one already had free-busy info cached, while the other was not cached. (fb-cached=1, fb-uncached=1)
+
 ::
 
-  17.108.160.37 - scastillo [15/Sep/2009:20:10:23 +0000] "REPORT(CalDAV:calendar-multiget) /calendars/__uids__/B8CE9430-965B-11DE-B626-EC2E9DB52B69/calendar/ HTTP/1.1" 207 149285 "-" "DAVKit/4.0 (729); CalendarStore/4.0 (965); iCal/4.0 (1362); Mac OS X/10.6.1 (10B504)" i=8459 t=183.0 or=1 rcount=32
+  10.1.5.43 - user5 [23/Oct/2012:13:42:56 -0700] "POST /calendars/__uids__/B2302CB9-D28F-4CB4-B3D9-0AF0FEDB8110/outbox/ HTTP/1.1" 200 1490 "-" "CalendarStore/5.0.2 (1166); iCal/5.0.2 (1571); Mac OS X/10.7.3 (11D50)" i=1 or=1 t=782.6 fb-uncached=1 fb-cached=1 recipients=2 cl=577
 
 
-
 **Fine-grained request time logging**
 
 If the configuration key EnableExtendedTimingAccessLog is set to true, additional key-value pairs will be logged with each request. The overall request time "t" is broken into four phases, and the elapsed time for each phase is logged. The new keys representing the four request phases are:
@@ -107,4 +117,4 @@
 
 ::
 
-  17.209.103.42 - wsanchez [24/Jul/2012:17:51:29 +0000] "REPORT(CalDAV:calendar-multiget) /calendars/__uids__/F114CA1D-295F-42A5-A5BD-D1A1B19FC049/60E68E32-4C87-4E63-9BF2-12A25E8F2623/ HTTP/1.1" 207 114349 "-" "CalendarStore/5.0.2 (1166); iCal/5.0.2 (1571); Mac OS X/10.7.3 (11D50d)" i=7 or=1 t=764.7 t-req-proc=4.8 t-resp-gen=754.5 t-resp-wr=5.1 t-log=0.2 rcount=2
\ No newline at end of file
+  17.209.103.42 - wsanchez [24/Jul/2012:17:51:29 +0000] "REPORT(CalDAV:calendar-multiget) /calendars/__uids__/F114CA1D-295F-42A5-A5BD-D1A1B19FC049/60E68E32-4C87-4E63-9BF2-12A25E8F2623/ HTTP/1.1" 207 114349 "-" "CalendarStore/5.0.2 (1166); iCal/5.0.2 (1571); Mac OS X/10.7.3 (11D50d)" i=7 or=1 t=764.7 t-req-proc=4.8 t-resp-gen=754.5 t-resp-wr=5.1 t-log=0.2 rcount=2

Copied: CalendarServer/branches/users/gaya/sharedgroups/doc/Admin/iSchedule.txt (from rev 9988, CalendarServer/trunk/doc/Admin/iSchedule.txt)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/doc/Admin/iSchedule.txt	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/doc/Admin/iSchedule.txt	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,55 @@
+iSchedule
+==================
+
+The iSchedule protocol is a server-to-server calendar protocol that allows servers to exchange
+iTIP (iCalendar-based) scheduling messages in real time over the HTTP protocol.
+
+About iSchedule
+----------------
+
+The basic protocol is this:
+
+1) Server detects a calendar user trying to schedule with another calendar user not hosted on that server.
+2) Server extracts "domain" portion of the non-hosted calendar user address and does a DNS SRV query for
+the _ischedules service.
+3) If no result is found, the server has to fall back to other options (e.g., iMIP - email invites).
+4) If the DNS record is found the server extracts a hostname and port and makes an HTTPS request on
+/.well-known/ischedule querying for capabilities of any iSchedule server there.
+5) Once legitimate service is discovered, the server constructs an iTIP message for the invite and
+sends that in an HTTP request to the iSchedule server, supplying Originator and Recipient headers in
+the HTTP request.
+6) The iSchedule server receiving the iTIP message immediately processes it and sends the status back
+in the HTTP response.
+
+Domain-level Security
+---------------------
+
+iSchedule uses DKIM (Domain-Keys Identified Mail) adapted for use with HTTP as a security protocol to
+allow iSchedule servers to verify that an iSchedule client (sender) is authorized to send iSchedule
+messages on behalf of the domain the client is operating in.
+
+To implement DKIM, the iSchedule client creates an HTTP request and adds a DKIM-Signature header containing
+various bits of information that include: a cryptographic hash of the request body, a cryptographic signature
+of select HTTP headers (including the DKIM-Signature header itself). The signature uses a private/public key
+pair. The public key is made available via a DNS TXT record (or through HTTP or a private exchange).
+
+Upon receipt of a DKIM signed iSchedule message, the iSchedule server will verify that the body hash value in
+the DKIM-Signature header matches the actual request body, and that the signature of the selected headers is
+also a match (it retrieves the specified public key for the client domain).
+
+Configuration of Calendar Server
+--------------------------------
+
+Configuring the server for iSchedule is done as follows:
+
+1) Create an RSA private/public key file stored with your server configuration:
+	a) bin/calendarserver_dkimtool -g -k <private path> -p <public path> -t
+	b) Add the TXT record output to your domain's DNS server
+
+2) Edit caldavd.plist and make the following changes:
+	a) Set Schedule.iSchedule to <true/>
+	b) Set Schedule.iSchedule.DKIM.PrivateKeyFile to the RSA private key
+	c) Set Schedule.iSchedule.DKIM.PublicKeyFile to the RSA public key
+	d) Set Schedule.iSchedule.DKIM.Domain to the domain portion of your users'
+	   calendar user addresses if that is different from the ServerHostName value
+	   in caldavd.plist.

Modified: CalendarServer/branches/users/gaya/sharedgroups/support/Makefile.Apple
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/support/Makefile.Apple	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/support/Makefile.Apple	2012-10-26 21:24:44 UTC (rev 9991)
@@ -59,16 +59,17 @@
 PyGreSQL-4.0::          $(BuildDirectory)/PyGreSQL-4.0
 sqlparse-0.1.2::        $(BuildDirectory)/sqlparse-0.1.2
 setproctitle-1.1.6::	$(BuildDirectory)/setproctitle-1.1.6
+psutil-0.6.1::		$(BuildDirectory)/psutil-0.6.1
 $(Project)::            $(BuildDirectory)/$(Project)
 
-build:: PyKerberos pycalendar PyGreSQL-4.0 sqlparse-0.1.2 setproctitle-1.1.6 $(Project)
+build:: PyKerberos pycalendar PyGreSQL-4.0 sqlparse-0.1.2 setproctitle-1.1.6 psutil-0.6.1 $(Project)
 
 setup:
 	$(_v) ./run -g
 
-prep:: setup CalDAVTester.tgz PyKerberos.tgz pycalendar.tgz PyGreSQL-4.0.tgz sqlparse-0.1.2.tgz setproctitle-1.1.6.tgz
+prep:: setup CalDAVTester.tgz PyKerberos.tgz pycalendar.tgz PyGreSQL-4.0.tgz sqlparse-0.1.2.tgz setproctitle-1.1.6.tgz psutil-0.6.1.tgz
 
-PyKerberos pycalendar PyGreSQL-4.0 sqlparse-0.1.2 setproctitle-1.1.6 $(Project)::
+PyKerberos pycalendar PyGreSQL-4.0 sqlparse-0.1.2 setproctitle-1.1.6 psutil-0.6.1 $(Project)::
 	@echo "Building $@..."
 	$(_v) cd $(BuildDirectory)/$@ && $(Environment) $(PYTHON) setup.py build
 
@@ -79,6 +80,7 @@
 	$(_v) cd $(BuildDirectory)/PyGreSQL-4.0       && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
 	$(_v) cd $(BuildDirectory)/sqlparse-0.1.2     && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
 	$(_v) cd $(BuildDirectory)/setproctitle-1.1.6 && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
+	$(_v) cd $(BuildDirectory)/psutil-0.6.1       && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
 	$(_v) for so in $$(find "$(DSTROOT)$(PY_HOME)/lib" -type f -name '*.so'); do $(STRIP) -Sx "$${so}"; done 
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(ETCDIR)$(CALDAVDSUBDIR)"
 	$(_v) $(INSTALL_FILE) "$(Sources)/conf/caldavd-apple.plist" "$(DSTROOT)$(SIPP)$(ETCDIR)$(CALDAVDSUBDIR)/caldavd.plist"

Modified: CalendarServer/branches/users/gaya/sharedgroups/support/version.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/support/version.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/support/version.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -44,7 +44,7 @@
             continue
 
         if branch == "trunk":
-            base_version = "trunk"
+            base_version += "-trunk"
         elif branch.endswith("-dev"):
             base_version += "-dev"
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/resource.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/web2/dav/resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -136,29 +136,29 @@
     # use them.  This (generic) class does not.
 
     def liveProperties(self):
-    
+
         return (
-            (dav_namespace, "resourcetype"              ),
-            (dav_namespace, "getetag"                   ),
-            (dav_namespace, "getcontenttype"            ),
-            (dav_namespace, "getcontentlength"          ),
-            (dav_namespace, "getlastmodified"           ),
-            (dav_namespace, "creationdate"              ),
-            (dav_namespace, "displayname"               ),
-            (dav_namespace, "supportedlock"             ),
-            (dav_namespace, "supported-report-set"      ), # RFC 3253, section 3.1.5
+            (dav_namespace, "resourcetype"),
+            (dav_namespace, "getetag"),
+            (dav_namespace, "getcontenttype"),
+            (dav_namespace, "getcontentlength"),
+            (dav_namespace, "getlastmodified"),
+            (dav_namespace, "creationdate"),
+            (dav_namespace, "displayname"),
+            (dav_namespace, "supportedlock"),
+            (dav_namespace, "supported-report-set"), # RFC 3253, section 3.1.5
            #(dav_namespace, "owner"                     ), # RFC 3744, section 5.1
            #(dav_namespace, "group"                     ), # RFC 3744, section 5.2
-            (dav_namespace, "supported-privilege-set"   ), # RFC 3744, section 5.3
+            (dav_namespace, "supported-privilege-set"), # RFC 3744, section 5.3
             (dav_namespace, "current-user-privilege-set"), # RFC 3744, section 5.4
-            (dav_namespace, "current-user-principal"    ), # RFC 5397, Section 3
-            (dav_namespace, "acl"                       ), # RFC 3744, section 5.5
-            (dav_namespace, "acl-restrictions"          ), # RFC 3744, section 5.6
-            (dav_namespace, "inherited-acl-set"         ), # RFC 3744, section 5.7
-            (dav_namespace, "principal-collection-set"  ), # RFC 3744, section 5.8
-            (dav_namespace, "quota-available-bytes"     ), # RFC 4331, section 3
-            (dav_namespace, "quota-used-bytes"          ), # RFC 4331, section 4
-    
+            (dav_namespace, "current-user-principal"), # RFC 5397, Section 3
+            (dav_namespace, "acl"), # RFC 3744, section 5.5
+            (dav_namespace, "acl-restrictions"), # RFC 3744, section 5.6
+            (dav_namespace, "inherited-acl-set"), # RFC 3744, section 5.7
+            (dav_namespace, "principal-collection-set"), # RFC 3744, section 5.8
+            (dav_namespace, "quota-available-bytes"), # RFC 4331, section 3
+            (dav_namespace, "quota-used-bytes"), # RFC 4331, section 4
+
             (twisted_dav_namespace, "resource-class"),
         )
 
@@ -204,7 +204,7 @@
                 d = self.hasQuota(request)
                 d.addCallback(lambda result: result)
                 return d
-        
+
         return succeed(
             qname in self.liveProperties() or
             self.deadProperties().contains(qname)
@@ -450,7 +450,7 @@
         # Add dynamic live properties that exist
         dynamicLiveProperties = (
             (dav_namespace, "quota-available-bytes"),
-            (dav_namespace, "quota-used-bytes"     ),
+            (dav_namespace, "quota-used-bytes"),
         )
         for dqname in dynamicLiveProperties:
             has = (yield self.hasProperty(dqname, request))
@@ -635,7 +635,7 @@
 
             if privileges is None:
                 return child
-   
+
             d = child.checkPrivileges(
                 request, privileges,
                 inherited_aces=inherited_aces
@@ -716,7 +716,7 @@
             returnValue(None)
 
         # First find all depth 1 children
-        names1= []
+        names1 = []
         namesDeep = []
         collections1 = []
         if names:
@@ -791,14 +791,14 @@
                         child_collections[collection_name] if names else None, privileges,
                         inherited_aces=collection_inherited_aces
                     )
-                
+
         returnValue(None)
 
     @inlineCallbacks
     def checkACLPrivilege(
         self, request, acl, privyset, privileges, inherited_aces
     ):
-        
+
         if acl is None:
             returnValue(False)
 
@@ -875,7 +875,7 @@
         acl = element.ACL(*aces)
 
         return acl
-    
+
     def supportedReports(self):
         """
         See L{IDAVResource.supportedReports}.
@@ -1120,7 +1120,7 @@
         This implementation stores the ACL in the private property
         """
         # C{(L{twisted_private_namespace}, "acl")}.
-        
+
         # Steps for ACL evaluation:
         #  1. Check that ace's on incoming do not match a protected ace
         #  2. Check that ace's on incoming do not match an inherited ace
@@ -1332,7 +1332,7 @@
         if recurse:
             yield self.findChildren(
                 "infinity", request,
-                lambda x, y: resources.append((x,y))
+                lambda x, y: resources.append((x, y))
             )
 
         for resource, uri in resources:
@@ -1377,12 +1377,12 @@
 
             denied += pending # If no matching ACE, then denied
 
-            if denied: 
+            if denied:
                 errors.append((uri, denied))
 
         if errors:
             raise AccessDeniedError(errors,)
-        
+
         returnValue(None)
 
     def supportedPrivileges(self, request):
@@ -1473,20 +1473,20 @@
             if inherited_aces is None:
                 if myURL != "/":
                     parentURL = parentForURL(myURL)
-    
+
                     parent = (yield request.locateResource(parentURL))
-    
+
                     if parent:
                         parent_acl = (yield
                             parent.accessControlList(
                                 request, inheritance=True, expanding=True
                             )
                         )
-    
+
                         # Check disabled
                         if parent_acl is None:
                             returnValue(None)
-    
+
                         for ace in parent_acl.children:
                             if ace.inherited:
                                 aces.append(ace)
@@ -1527,7 +1527,7 @@
         @return: a C{list} of L{Ace}s that child resources of this one
             will inherit.
         """
-        
+
         # Get the parent ACLs with inheritance and preserve the
         # <inheritable> element.
 
@@ -1632,7 +1632,7 @@
             and URI respectively.
         """
         return succeed(authnPrincipal)
-        
+
     def samePrincipal(self, principal1, principal2):
         """
         Check whether the two principals are exactly the same in terms of
@@ -1662,7 +1662,7 @@
                 return True
         else:
             return False
-                
+
     def matchPrincipal(self, principal1, principal2, request):
         """
         Check whether the principal1 is a principal in the set defined
@@ -1758,7 +1758,7 @@
             returnValue(isContained)
         returnValue(False)
 
-        
+
     def validPrincipal(self, ace_principal, request):
         """
         Check whether the supplied principal is valid for this resource.
@@ -1808,7 +1808,7 @@
 
         # Must have the principal resource type and must match the
         # principal-URL
-        
+
         def _matchPrincipalURL(resource):
             return (
                 isPrincipalResource(resource) and
@@ -1963,9 +1963,9 @@
         for a in acl.children:
             if self.samePrincipal(a.principal, ace.principal):
                 return a
-        
+
         return None
-    
+
     def principalSearchPropertySet(self):
         """
         @return: a L{element.PrincipalSearchPropertySet} element describing the
@@ -1980,7 +1980,7 @@
     ##
     # Quota
     ##
-    
+
     """
     The basic policy here is to define a private 'quota-root' property
     on a collection.  That property will contain the maximum allowed
@@ -2012,7 +2012,7 @@
             quota-used-bytes, or C{None} if quota is not defined on
             the resource.
         """
-        
+
         # See if already cached
         if hasattr(request, "quota"):
             if request.quota.has_key(self):
@@ -2031,7 +2031,7 @@
                             available = 0
                         request.quota[self] = (available, used)
                         return (available, used)
-    
+
                     d = qroot_resource.currentQuotaUse(request)
                     d.addCallback(gotUsage)
                     return d
@@ -2039,11 +2039,11 @@
             request.quota[self] = None
             return None
 
-            
+
         d = self.quotaRootResource(request)
         d.addCallback(gotQuotaRootResource)
         return d
-    
+
     def hasQuota(self, request):
         """
         Check whether this resource is under quota control by checking
@@ -2053,9 +2053,9 @@
         """
 
         def gotQuotaRootResource(qroot_resource):
-            
+
             return qroot_resource is not None
-        
+
         d = self.quotaRootResource(request)
         d.addCallback(gotQuotaRootResource)
         return d
@@ -2065,7 +2065,7 @@
         @return: a C{True} if this resource has quota root, C{False} otherwise.
         """
         return self.hasDeadProperty(TwistedQuotaRootProperty)
-    
+
     def quotaRoot(self, request):
         """
         @return: a C{int} containing the maximum allowed bytes if this
@@ -2076,7 +2076,7 @@
             return int(str(self.readDeadProperty(TwistedQuotaRootProperty)))
         else:
             return None
-    
+
     @inlineCallbacks
     def quotaRootResource(self, request):
         """
@@ -2095,7 +2095,11 @@
             returnValue(None)
         while (url != "/"):
             url = parentForURL(url)
+            if url is None:
+                break
             parent = (yield request.locateResource(url))
+            if parent is None:
+                break
             if parent.hasQuotaRoot(request):
                 returnValue(parent)
 
@@ -2111,14 +2115,14 @@
         assert maxsize is None or isinstance(maxsize, int), (
             "maxsize must be an int or None"
         )
-        
+
         if maxsize is not None:
             self.writeDeadProperty(TwistedQuotaRootProperty(str(maxsize)))
         else:
             # Remove both the root and the cached used value
             self.removeDeadProperty(TwistedQuotaRootProperty)
             self.removeDeadProperty(TwistedQuotaUsedProperty)
-    
+
     def quotaSize(self, request):
         """
         Get the size of this resource (if its a collection get total
@@ -2141,7 +2145,7 @@
         @return: C{True} if there is sufficient quota remaining on all
             quota roots, C{False} otherwise.
         """
-        
+
         def _defer(quotaroot):
             if quotaroot:
                 # Check quota on this root (if it has one)
@@ -2149,7 +2153,7 @@
                 if quota is not None:
                     if available > quota[0]:
                         return False
-    
+
             return True
 
         d = self.quotaRootResource(request)
@@ -2165,8 +2169,8 @@
             (positive) or removed (negative) that should be used to
             adjust the cached total.
         """
-        
-        
+
+
         def _defer(quotaroot):
             if quotaroot:
                 # Check quota on this root (if it has one)
@@ -2189,7 +2193,7 @@
         assert self.hasQuotaRoot(request), (
             "Quota use only on quota root collection"
         )
-        
+
         # Try to get the cached value property
         if self.hasDeadProperty(TwistedQuotaUsedProperty):
             return succeed(
@@ -2217,11 +2221,11 @@
             or C{None} if not quota controlled.
         """
         assert self.isCollection(), "Only collections can have a quota root"
-        
+
         # Get current value
         def _defer(size):
             size += adjust
-            
+
             # Sanity check the resulting size
             if size >= 0:
                 self.writeDeadProperty(TwistedQuotaUsedProperty(str(size)))
@@ -2237,7 +2241,7 @@
         d = self.currentQuotaUse(request)
         d.addCallback(_defer)
         return d
-        
+
     ##
     # HTTP
     ##
@@ -2255,7 +2259,7 @@
                 request.unparseURL(
                     path=urllib.quote(
                         urllib.unquote(request.path),
-                        safe=':/')+'/'
+                        safe=':/') + '/'
                 )
             )
 
@@ -2307,12 +2311,12 @@
     ##
 
     def liveProperties(self):
-        
+
         return super(DAVPrincipalResource, self).liveProperties() + (
             (dav_namespace, "alternate-URI-set"),
-            (dav_namespace, "principal-URL"    ),
-            (dav_namespace, "group-member-set" ),
-            (dav_namespace, "group-membership" ),
+            (dav_namespace, "principal-URL"),
+            (dav_namespace, "group-member-set"),
+            (dav_namespace, "group-membership"),
         )
 
     def davComplianceClasses(self):
@@ -2347,7 +2351,7 @@
                             element.HRef(p.principalURL())
                             for p in members
                         ])
-                    
+
                     d = self.groupMembers()
                     d.addCallback(callback)
                     return d
@@ -2358,7 +2362,7 @@
                             element.HRef(g.principalURL())
                             for g in memberships
                         ])
-                    
+
                     d = self.groupMemberships()
                     d.addCallback(callback)
                     return d

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/web2/resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/web2/resource.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/web2/resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -34,6 +34,7 @@
 
 from twext.web2 import iweb, http, server, responsecode
 
+from twisted.internet.defer import maybeDeferred
 class RenderMixin(object):
     """
     Mix-in class for L{iweb.IResource} which provides a dispatch mechanism for
@@ -106,9 +107,20 @@
             returnValue(response)
 
         yield self.checkPreconditions(request)
-        returnValue((yield method(request)))
+        result = maybeDeferred(method, request)
+        result.addErrback(self.methodRaisedException)
+        returnValue((yield result))
 
 
+    def methodRaisedException(self, failure):
+        """
+        An C{http_METHOD} method raised an exception; this is an errback for
+        that exception.  By default, simply propagate the error up; subclasses
+        may override this for top-level exception handling.
+        """
+        return failure
+
+
     def http_OPTIONS(self, request):
         """
         Respond to a OPTIONS request.

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/web2/stream.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/web2/stream.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/web2/stream.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -763,7 +763,8 @@
             return
         
         if isinstance(data, Deferred):
-            self.deferred = data.addCallbacks(self._doWrite, self.stopProducing)
+            self.deferred = data
+            self.deferred.addCallbacks(self._doWrite, self.stopProducing)
         else:
             self._doWrite(data)
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twisted/plugins/caldav.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twisted/plugins/caldav.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twisted/plugins/caldav.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -33,6 +33,7 @@
     return property(getProperty)
 
 
+
 class TAP(object):
     implements(IPlugin, IServiceMaker)
 
@@ -40,8 +41,8 @@
         self.serviceMakerClass = serviceMakerClass
         self._serviceMaker = None
 
-    options     = serviceMakerProperty("options")
-    tapname     = serviceMakerProperty("tapname")
+    options = serviceMakerProperty("options")
+    tapname = serviceMakerProperty("tapname")
     description = serviceMakerProperty("description")
 
     def makeService(self, options):
@@ -51,7 +52,7 @@
         return self._serviceMaker.makeService(options)
 
 
-TwistedCalDAV     = TAP("calendarserver.tap.caldav.CalDAVServiceMaker")
-CalDAVNotifier    = TAP("twistedcaldav.notify.NotificationServiceMaker")
-CalDAVMailGateway = TAP("twistedcaldav.mail.MailGatewayServiceMaker")
+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/__init__.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/__init__.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -41,6 +41,8 @@
 import twistedcaldav.carddavxml
 import twistedcaldav.mkcolxml
 import twistedcaldav.customxml
+import twistedcaldav.timezonexml
+import twistedcaldav.scheduling.ischedule.xml
 
 twistedcaldav # Shhh.. pyflakes
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/directory.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/directory/directory.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -28,14 +28,15 @@
     "UnknownRecordTypeError",
 ]
 
+import cPickle as pickle
 import datetime
+import grp
+import itertools
 import os
+import pwd
 import signal
 import sys
 import types
-import pwd, grp
-import cPickle as pickle
-import itertools
 
 
 from zope.interface import implements
@@ -51,7 +52,7 @@
 from twistedcaldav.directory.idirectory import IDirectoryService, IDirectoryRecord
 from twistedcaldav.directory.util import uuidFromName, normalizeUUID
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
-from twistedcaldav import servers
+from twistedcaldav.scheduling.ischedule.localservers import Servers
 from twistedcaldav.memcacher import Memcacher
 from twistedcaldav import memcachepool
 from twisted.python.filepath import FilePath
@@ -80,7 +81,7 @@
 
     searchContext_location = "location"
     searchContext_attendee = "attendee"
-    
+
     def _generatedGUID(self):
         if not hasattr(self, "_guid"):
             realmName = self.realmName
@@ -106,6 +107,7 @@
     def setRealm(self, realmName):
         self.realmName = realmName
 
+
     def available(self):
         """
         By default, the directory is available.  This may return a boolean or a
@@ -144,9 +146,9 @@
         try:
             from twistedcaldav.authkerb import NegotiateCredentials
         except ImportError:
-            NegotiateCredentials=None
-        
-        if NegotiateCredentials and isinstance(credentials.credentials, 
+            NegotiateCredentials = None
+
+        if NegotiateCredentials and isinstance(credentials.credentials,
                                                NegotiateCredentials):
             # If we get here with Kerberos, then authentication has already succeeded
             return (
@@ -164,20 +166,24 @@
                     credentials.authzPrincipal,
                 )
             else:
-                raise UnauthorizedLogin("Incorrect credentials for %s" % (credentials.credentials.username,)) 
+                raise UnauthorizedLogin("Incorrect credentials for %s" % (credentials.credentials.username,))
 
+
     def recordTypes(self):
         raise NotImplementedError("Subclass must implement recordTypes()")
 
+
     def listRecords(self, recordType):
         raise NotImplementedError("Subclass must implement listRecords()")
 
+
     def recordWithShortName(self, recordType, shortName):
         for record in self.listRecords(recordType):
             if shortName in record.shortNames:
                 return record
         return None
 
+
     def recordWithUID(self, uid):
         uid = normalizeUUID(uid)
         for record in self.allRecords():
@@ -185,6 +191,7 @@
                 return record
         return None
 
+
     def recordWithGUID(self, guid):
         guid = normalizeUUID(guid)
         for record in self.allRecords():
@@ -192,12 +199,14 @@
                 return record
         return None
 
+
     def recordWithAuthID(self, authID):
         for record in self.allRecords():
             if authID in record.authIDs:
                 return record
         return None
 
+
     def recordWithCalendarUserAddress(self, address):
         address = normalizeCUAddr(address)
         record = None
@@ -213,6 +222,7 @@
 
         return record if record and record.enabledForCalendaring else None
 
+
     def recordWithCachedGroupsAlias(self, recordType, alias):
         """
         @param recordType: the type of the record to look up.
@@ -225,11 +235,13 @@
         # The default implementation uses guid
         return succeed(self.recordWithGUID(alias))
 
+
     def allRecords(self):
         for recordType in self.recordTypes():
             for record in self.listRecords(recordType):
                 yield record
 
+
     def recordsMatchingFieldsWithCUType(self, fields, operand="or",
         cuType=None):
         if cuType:
@@ -240,6 +252,7 @@
         return self.recordsMatchingFields(fields, operand=operand,
             recordType=recordType)
 
+
     def recordTypesForSearchContext(self, context):
         """
         Map calendarserver-principal-search REPORT context value to applicable record types
@@ -339,12 +352,12 @@
                 return False
             elif type(fieldValue) in types.StringTypes:
                 fieldValue = (fieldValue,)
-            
+
             for testValue in fieldValue:
                 if caseless:
                     testValue = testValue.lower()
                     value = value.lower()
-    
+
                 if matchType == 'starts-with':
                     if testValue.startswith(value):
                         return True
@@ -357,7 +370,7 @@
                 else: # exact
                     if testValue == value:
                         return True
-                    
+
             return False
 
         def recordMatches(record):
@@ -404,6 +417,7 @@
 
         return succeed(yieldMatches(recordType))
 
+
     def getGroups(self, guids):
         """
         This implementation returns all groups, not just the ones specified
@@ -411,12 +425,15 @@
         """
         return succeed(self.listRecords(self.recordType_groups))
 
+
     def getResourceInfo(self):
         return ()
 
+
     def isAvailable(self):
         return True
 
+
     def getParams(self, params, defaults, ignore=None):
         """ Checks configuration parameters for unexpected/ignored keys, and
             applies default values. """
@@ -441,6 +458,7 @@
             raise DirectoryConfigurationError("Invalid directory service parameter(s): %s" % (", ".join(list(keys)),))
         return result
 
+
     def parseResourceInfo(self, plist, guid, recordType, shortname):
         """
         Parse ResourceInfo plist and extract information that the server needs.
@@ -503,6 +521,7 @@
         """
         raise NotImplementedError("Subclass must implement createRecord")
 
+
     def updateRecord(self, recordType, guid=None, shortNames=(), authIDs=set(),
         fullName=None, firstName=None, lastName=None, emailAddresses=set(),
         uid=None, password=None, **kwargs):
@@ -511,12 +530,14 @@
         """
         raise NotImplementedError("Subclass must implement updateRecord")
 
+
     def destroyRecord(self, recordType, guid=None):
         """
         Remove a directory record from the directory
         """
         raise NotImplementedError("Subclass must implement destroyRecord")
 
+
     def createRecords(self, data):
         """
         Create directory records in bulk
@@ -555,12 +576,14 @@
         self.expireSeconds = expireSeconds
         self.lockSeconds = lockSeconds
 
+
     def setGroupsFor(self, guid, memberships):
         self.log_debug("set groups-for %s : %s" % (guid, memberships))
         return self.set("groups-for:%s" %
             (str(guid)), memberships,
             expireTime=self.expireSeconds)
 
+
     def getGroupsFor(self, guid):
         self.log_debug("get groups-for %s" % (guid,))
         def _value(value):
@@ -572,29 +595,35 @@
         d.addCallback(_value)
         return d
 
+
     def deleteGroupsFor(self, guid):
         self.log_debug("delete groups-for %s" % (guid,))
         return self.delete("groups-for:%s" % (str(guid),))
 
+
     def setPopulatedMarker(self):
         self.log_debug("set group-cacher-populated")
         return self.set("group-cacher-populated", str(datetime.datetime.now()))
 
+
     @inlineCallbacks
     def isPopulated(self):
         self.log_debug("is group-cacher-populated")
         value = (yield self.get("group-cacher-populated"))
         returnValue(value is not None)
 
+
     def acquireLock(self):
         self.log_debug("add group-cacher-lock")
         return self.add("group-cacher-lock", "1", expireTime=self.lockSeconds)
 
+
     def releaseLock(self):
         self.log_debug("delete group-cacher-lock")
         return self.delete("group-cacher-lock")
 
 
+
 class GroupMembershipCacheUpdater(LoggingMixIn):
     """
     Responsible for updating memcached with group memberships.  This will run
@@ -668,7 +697,7 @@
             seen.add(guid)
             for member in groups[guid]:
                 members.add(member)
-                if groups.has_key(member): # it's a group then
+                if member in groups: # it's a group then
                     self.expandedMembers(groups, member, members=members,
                                          seen=seen)
         return members
@@ -922,9 +951,6 @@
 
 
 
-
-
-
 class GroupMembershipCacherOptions(Options):
     optParameters = [[
         "config", "f", DEFAULT_CONFIG_FILE, "Path to configuration file."
@@ -935,6 +961,7 @@
 
         self.overrides = {}
 
+
     def _coerceOption(self, configDict, key, value):
         """
         Coerce the given C{val} to type of C{configDict[key]}
@@ -959,6 +986,7 @@
 
         return value
 
+
     def _setOverride(self, configDict, path, value, overrideDict):
         """
         Set the value at path in configDict
@@ -1043,15 +1071,18 @@
         else:
             self.updateMethod = self.updater.updateCache
 
+
     def startService(self):
         self.previousHandler = signal.signal(signal.SIGHUP, self.sighupHandler)
         self.log_warn("Starting group membership cacher service")
         service.Service.startService(self)
         return self.update()
 
+
     def sighupHandler(self, num, frame):
         self.reactor.callFromThread(self.update)
 
+
     def stopService(self):
         signal.signal(signal.SIGHUP, self.previousHandler)
         self.log_warn("Stopping group membership cacher service")
@@ -1060,6 +1091,7 @@
             self.nextUpdate.cancel()
             self.nextUpdate = None
 
+
     @inlineCallbacks
     def update(self):
         """
@@ -1099,6 +1131,8 @@
                     self.update)
         returnValue(False)
 
+
+
 class GroupMembershipCacherServiceMaker(LoggingMixIn):
     """
     Configures and returns a GroupMembershipCacherService
@@ -1156,6 +1190,8 @@
 
         return cacherService
 
+
+
 def diffAssignments(old, new):
     """
     Compare two proxy assignment lists and return their differences in the form of
@@ -1185,6 +1221,7 @@
     return changed, removed
 
 
+
 class DirectoryRecord(LoggingMixIn):
     implements(IDirectoryRecord)
 
@@ -1201,6 +1238,7 @@
             self.partitionID,
         )
 
+
     def __init__(
         self, service, recordType, guid=None,
         shortNames=(), authIDs=set(), fullName=None,
@@ -1216,7 +1254,7 @@
     ):
         assert service.realmName is not None
         assert recordType
-        assert shortNames and isinstance(shortNames, tuple) 
+        assert shortNames and isinstance(shortNames, tuple)
 
         guid = normalizeUUID(guid)
 
@@ -1226,30 +1264,29 @@
         if fullName is None:
             fullName = ""
 
-        self.service                = service
-        self.recordType             = recordType
-        self.guid                   = guid
-        self.uid                    = uid
-        self.enabled                = False
-        self.serverID               = ""
-        self.partitionID            = ""
-        self.shortNames             = shortNames
-        self.authIDs                = authIDs
-        self.fullName               = fullName
-        self.firstName              = firstName
-        self.lastName               = lastName
-        self.emailAddresses         = emailAddresses
-        self.enabledForCalendaring  = enabledForCalendaring
-        self.autoSchedule           = autoSchedule
-        self.autoScheduleMode       = autoScheduleMode
+        self.service = service
+        self.recordType = recordType
+        self.guid = guid
+        self.uid = uid
+        self.enabled = False
+        self.serverID = ""
+        self.partitionID = ""
+        self.shortNames = shortNames
+        self.authIDs = authIDs
+        self.fullName = fullName
+        self.firstName = firstName
+        self.lastName = lastName
+        self.emailAddresses = emailAddresses
+        self.enabledForCalendaring = enabledForCalendaring
+        self.autoSchedule = autoSchedule
+        self.autoScheduleMode = autoScheduleMode
         self.enabledForAddressBooks = enabledForAddressBooks
-        self.enabledForLogin        = enabledForLogin
-        self.extProxies             = extProxies
-        self.extReadOnlyProxies     = extReadOnlyProxies
-        self.extras                 = kwargs
+        self.enabledForLogin = enabledForLogin
+        self.extProxies = extProxies
+        self.extReadOnlyProxies = extReadOnlyProxies
+        self.extras = kwargs
 
 
-
     def get_calendarUserAddresses(self):
         """
         Dynamically construct a calendarUserAddresses attribute which describes
@@ -1280,6 +1317,7 @@
                 return diff
         return 0
 
+
     def __hash__(self):
         h = hash(self.__class__.__name__)
         for attr in ("service", "recordType", "shortNames", "guid",
@@ -1288,6 +1326,7 @@
 
         return h
 
+
     def cacheToken(self):
         """
         Generate a token that can be uniquely used to identify the state of this record for use
@@ -1303,8 +1342,9 @@
             self.enabledForCalendaring,
         ))
 
+
     def addAugmentInformation(self, augment):
-        
+
         if augment:
             self.enabled = augment.enabled
             self.serverID = augment.serverID
@@ -1350,6 +1390,7 @@
                                % (username,))
                 self.enabledForAddressBooks = False
 
+
     def isLoginEnabled(self):
         """
         Returns True if the user should be allowed to log in, based on the
@@ -1358,6 +1399,7 @@
         """
         return self.enabledForLogin
 
+
     def members(self):
         return ()
 
@@ -1393,6 +1435,7 @@
         """
         return self.service.groupMembershipCache.getGroupsFor(self.cachedGroupsAlias())
 
+
     def cachedGroupsAlias(self):
         """
         The GroupMembershipCache uses keys based on this value.  Normally it's
@@ -1404,6 +1447,7 @@
         """
         return self.guid
 
+
     def externalProxies(self):
         """
         Return the set of proxies defined in the directory service, as opposed
@@ -1411,6 +1455,7 @@
         """
         return set(self.extProxies)
 
+
     def externalReadOnlyProxies(self):
         """
         Return the set of read-only proxies defined in the directory service,
@@ -1418,12 +1463,14 @@
         """
         return set(self.extReadOnlyProxies)
 
+
     def memberGUIDs(self):
         """
         Return the set of GUIDs that are members of this group
         """
         return set()
 
+
     def verifyCredentials(self, credentials):
         return False
 
@@ -1438,6 +1485,7 @@
     def getCUType(self):
         return self._cuTypes.get(self.recordType, "UNKNOWN")
 
+
     @classmethod
     def fromCUType(cls, cuType):
         for key, val in cls._cuTypes.iteritems():
@@ -1445,72 +1493,84 @@
                 return key
         return None
 
+
     def serverURI(self):
         """
         URL of the server hosting this record. Return None if hosted on this server.
         """
         if config.Servers.Enabled and self.serverID:
-            return servers.Servers.getServerURIById(self.serverID)
+            return Servers.getServerURIById(self.serverID)
         else:
             return None
-    
+
+
     def server(self):
         """
         Server hosting this record. Return None if hosted on this server.
         """
         if config.Servers.Enabled and self.serverID:
-            return servers.Servers.getServerById(self.serverID)
+            return Servers.getServerById(self.serverID)
         else:
             return None
-    
+
+
     def partitionURI(self):
         """
         URL of the server hosting this record. Return None if hosted on this server.
         """
         if config.Servers.Enabled and self.serverID:
-            s = servers.Servers.getServerById(self.serverID)
+            s = Servers.getServerById(self.serverID)
             if s:
                 return s.getPartitionURIForId(self.partitionID)
         return None
-    
+
+
     def locallyHosted(self):
         """
         Hosted on this server/partition instance.
         """
-        
+
         if config.Servers.Enabled and self.serverID:
-            s = servers.Servers.getServerById(self.serverID)
+            s = Servers.getServerById(self.serverID)
             if s:
                 return s.thisServer and (not s.isPartitioned() or not self.partitionID or self.partitionID == config.ServerPartitionID)
         return True
 
+
     def effectivePartitionID(self):
         """
         Record partition ID taking into account whether the server is partitioned.
         """
         if config.Servers.Enabled and self.serverID:
-            s = servers.Servers.getServerById(self.serverID)
+            s = Servers.getServerById(self.serverID)
             if s and s.isPartitioned():
                 return self.partitionID
         return ""
-        
+
+
     def thisServer(self):
         if config.Servers.Enabled and self.serverID:
-            s = servers.Servers.getServerById(self.serverID)
+            s = Servers.getServerById(self.serverID)
             if s:
                 return s.thisServer
         return True
-        
+
+
+
 class DirectoryError(RuntimeError):
     """
     Generic directory error.
     """
 
+
+
 class DirectoryConfigurationError(DirectoryError):
     """
     Invalid directory configuration.
     """
 
+
+
 class UnknownRecordTypeError(DirectoryError):
     """
     Unknown directory record type.
@@ -1527,4 +1587,3 @@
     DirectoryRecord.CheckSACL = CheckSACL
 except ImportError:
     DirectoryRecord.CheckSACL = None
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/freebusyurl.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/freebusyurl.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/freebusyurl.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -41,8 +41,8 @@
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.ical import Property
 from twistedcaldav.resource import CalDAVResource, ReadOnlyNoCopyResourceMixIn
-from twistedcaldav.schedule import deliverSchedulePrivilegeSet
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
+from twistedcaldav.scheduling.caldav.delivery import ScheduleViaCalDAV
+from twistedcaldav.scheduling.caldav.resource import deliverSchedulePrivilegeSet
 from twistedcaldav.scheduling.cuaddress import LocalCalendarUser
 from twistedcaldav.scheduling.scheduler import Scheduler
 
@@ -69,9 +69,11 @@
 
         self.parent = parent
 
+
     def __repr__(self):
         return "<%s (free-busy URL resource): %s>" % (self.__class__.__name__, joinURL(self.parent.url(), "freebusy"))
 
+
     def defaultAccessControlList(self):
         privs = (
             davxml.Privilege(davxml.Read()),
@@ -101,21 +103,27 @@
             )
         return davxml.ACL(*aces)
 
+
     def resourceType(self):
         return davxml.ResourceType.freebusyurl
 
+
     def contentType(self):
         return MimeType("text", "calendar", charset="utf-8")
 
+
     def isCollection(self):
         return False
 
+
     def isCalendarCollection(self):
         return False
 
+
     def isPseudoCalendarCollection(self):
         return False
 
+
     def render(self, request):
         output = """<html>
 <head>
@@ -130,31 +138,33 @@
         response.headers.setHeader("content-type", MimeType("text", "html"))
         return response
 
+
     def http_GET(self, request):
         """
         The free-busy URL POST method.
         """
         return self._processFBURL(request)
 
+
     def http_POST(self, request):
         """
         The free-busy URL POST method.
         """
         return self._processFBURL(request)
 
+
     @inlineCallbacks
     def _processFBURL(self, request):
-        
         #
         # Check authentication and access controls
         #
         yield self.authorize(request, (davxml.Read(),))
-        
+
         # Extract query parameters from the URL
         args = ('start', 'end', 'duration', 'token', 'format', 'user',)
         for arg in args:
             setattr(self, arg, request.args.get(arg, [None])[0])
-        
+
         # Some things we do not handle
         if self.token or self.user:
             raise HTTPError(ErrorResponse(
@@ -162,7 +172,7 @@
                 (calendarserver_namespace, "supported-query-parameter"),
                 "Invalid query parameter",
             ))
-        
+
         # Check format
         if self.format:
             self.format = self.format.split(";")[0]
@@ -174,7 +184,7 @@
                 ))
         else:
             self.format = "text/calendar"
-            
+
         # Start/end/duration must be valid iCalendar DATE-TIME UTC or DURATION values
         try:
             if self.start:
@@ -193,7 +203,7 @@
                 (calendarserver_namespace, "valid-query-parameters"),
                 "Invalid query parameters",
             ))
-        
+
         # Sanity check start/end/duration
 
         # End and duration cannot both be present
@@ -203,7 +213,7 @@
                 (calendarserver_namespace, "valid-query-parameters"),
                 "Invalid query parameters",
             ))
-        
+
         # Duration must be positive
         if self.duration and self.duration.getTotalSeconds() < 0:
             raise HTTPError(ErrorResponse(
@@ -211,7 +221,7 @@
                 (calendarserver_namespace, "valid-query-parameters"),
                 "Invalid query parameters",
             ))
-        
+
         # Now fill in the missing pieces
         if self.start is None:
             self.start = PyCalendarDateTime.getNowUTC()
@@ -220,7 +230,7 @@
             self.end = self.start + self.duration
         if self.end is None:
             self.end = self.start + PyCalendarDuration(days=config.FreeBusyURL.TimePeriod)
-            
+
         # End > start
         if self.end <= self.start:
             raise HTTPError(ErrorResponse(
@@ -228,12 +238,12 @@
                 (calendarserver_namespace, "valid-query-parameters"),
                 "Invalid query parameters",
             ))
-        
+
         # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long)
-        
+
         # Now lookup the principal details for the targeted user
         principal = self.parent.principalForRecord()
-        
+
         # Pick the first mailto cu address or the first other type
         cuaddr = None
         for item in principal.calendarUserAddresses():
@@ -254,14 +264,14 @@
             inbox = None
         if inbox is None:
             raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,)))
-            
+
         scheduler = Scheduler(request, self)
         scheduler.timeRange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
         scheduler.timeRange.start = self.start
         scheduler.timeRange.end = self.end
-        
+
         scheduler.organizer = LocalCalendarUser(cuaddr, principal, inbox, inboxURL)
-        
+
         attendeeProp = Property("ATTENDEE", scheduler.organizer.cuaddr)
 
         requestor = ScheduleViaCalDAV(scheduler, (), [], True)
@@ -273,13 +283,14 @@
             attendeeProp,
             True,
         ))
-        
+
         response = Response()
         response.stream = MemoryStream(str(fbresult))
         response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,)))
-    
+
         returnValue(response)
 
+
     ##
     # ACL
     ##

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/ical.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/ical.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -2364,6 +2364,21 @@
             [component.removeProperty(p) for p in tuple(component.properties("ATTENDEE")) if p.value().lower() != attendee.lower()]
 
 
+    def removeAllButTheseAttendees(self, attendees):
+        """
+        Remove all ATTENDEE properties except for the ones specified.
+        """
+
+        assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
+
+        attendees = set([attendee.lower() for attendee in attendees])
+
+        for component in self.subcomponents():
+            if component.name() in ignoredComponents:
+                continue
+            [component.removeProperty(p) for p in tuple(component.properties("ATTENDEE")) if p.value().lower() not in attendees]
+
+
     def hasAlarm(self):
         """
         Test whether the component has a VALARM as an immediate sub-component.
@@ -3149,6 +3164,59 @@
 # Utilities
 ##
 
+def normalizeCUAddress(cuaddr, lookupFunction, principalFunction, toUUID=True):
+    # Check that we can lookup this calendar user address - if not
+    # we cannot do anything with it
+    _ignore_name, guid, cuaddrs = lookupFunction(normalizeCUAddr(cuaddr), principalFunction, config)
+
+    if toUUID:
+        # Always re-write value to urn:uuid
+        if guid:
+            return "urn:uuid:%s" % (guid,)
+
+    # If it is already a non-UUID address leave it be
+    elif cuaddr.startswith("urn:uuid:"):
+
+        # Pick the first mailto,
+        # or failing that the first path one,
+        # or failing that the first http one,
+        # or failing that the first one
+        first_mailto = None
+        first_path = None
+        first_http = None
+        first = None
+        for addr in cuaddrs:
+            if addr.startswith("mailto:"):
+                first_mailto = addr
+                break
+            elif addr.startswith("/"):
+                if not first_path:
+                    first_path = addr
+            elif addr.startswith("http:"):
+                if not first_http:
+                    first_http = addr
+            elif not first:
+                first = addr
+
+        if first_mailto:
+            newaddr = first_mailto
+        elif first_path:
+            newaddr = first_path
+        elif first_http:
+            newaddr = first_http
+        elif first:
+            newaddr = first
+        else:
+            newaddr = None
+
+        # Make the change
+        if newaddr:
+            return newaddr
+
+    return cuaddr
+
+
+
 #
 # This function is from "Python Cookbook, 2d Ed., by Alex Martelli, Anna
 # Martelli Ravenscroft, and David Ascher (O'Reilly Media, 2005) 0-596-00797-3."

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/mail.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/mail.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/mail.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,1966 +0,0 @@
-# -*- test-case-name: twistedcaldav.test.test_mail -*-
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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.
-##
-
-"""
-Mail Gateway for Calendar Server
-"""
-
-from __future__ import with_statement
-
-import datetime
-import email.utils
-import os
-import urlparse
-import uuid
-
-from cStringIO import StringIO
-
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-
-from pycalendar.datetime import PyCalendarDateTime
-from pycalendar.duration import PyCalendarDuration
-
-from zope.interface import implements
-
-from twisted.application import internet, service
-from twisted.internet import protocol, defer, ssl, reactor as _reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, 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.template import (
-    XMLString, TEMPLATE_NAMESPACE, Element, renderer, flattenString, tags
-)
-from twisted.web.microdom import parseString
-from twisted.web.microdom import Text as DOMText, Element as DOMElement
-
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.internet.adaptendpoint import connect
-
-from twext.web2 import server, responsecode
-from twext.web2.channel.http import HTTPFactory
-from txdav.xml import element as davxml
-from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.http import Response, HTTPError
-from twext.web2.http_headers import MimeType
-
-from twext.python.log import Logger, LoggingMixIn
-
-from twistedcaldav import ical, caldavxml
-from twistedcaldav import memcachepool
-from twistedcaldav.config import config
-from twistedcaldav.directory.util import transactionFromRequest
-from twistedcaldav.ical import Property
-from twistedcaldav.localization import translationTo, getLanguage, _
-from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.schedule import deliverSchedulePrivilegeSet
-from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.scheduling.scheduler import IMIPScheduler
-from twistedcaldav.sql import AbstractSQLDatabase
-from twistedcaldav.util import AuthorizedHTTPGetter
-from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
-
-from calendarserver.tap.util import getRootResource, directoryFromConfig
-
-
-__all__ = [
-    "IMIPInboxResource",
-    "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
-
-#
-# Mail gateway service config
-#
-
-class MailGatewayOptions(Options):
-    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 seperated lists. Only
-        one option may be given for each --option flag, however multiple
-        --option flags may be specified.
-        """
-
-        if "=" in option:
-            path, value = option.split('=')
-            self._setOverride(
-                DEFAULT_CONFIG,
-                path.split('/'),
-                value,
-                self.overrides
-            )
-        else:
-            self.opt_option('%s=True' % (option,))
-
-    opt_o = opt_option
-
-    def postOptions(self):
-        config.load(self['config'])
-        config.updateDefaults(self.overrides)
-        self.parent['pidfile'] = None
-
-
-
-class IMIPInboxResource(CalDAVResource):
-    """
-    IMIP-delivery Inbox resource.
-
-    Extends L{DAVResource} to provide IMIP delivery functionality.
-    """
-
-    def __init__(self, parent, store):
-        """
-        @param parent: the parent resource of this one.
-        @param store: the store to use for transactions.
-        """
-        assert parent is not None
-
-        CalDAVResource.__init__(
-            self, principalCollections=parent.principalCollections()
-        )
-
-        self.parent = parent
-        self._newStore = store
-
-
-    def accessControlList(self, request, inheritance=True,
-        expanding=False, inherited_aces=None):
-
-        if not hasattr(self, "iMIPACL"):
-            guid = config.Scheduling.iMIP.GUID
-            self.iMIPACL = davxml.ACL(
-                davxml.ACE(
-                    davxml.Principal(
-                        davxml.HRef.fromString("/principals/__uids__/%s/"
-                                               % (guid,))
-                    ),
-                    davxml.Grant(
-                        davxml.Privilege(caldavxml.ScheduleDeliver()),
-                    ),
-                ),
-            )
-
-        return succeed(self.iMIPACL)
-
-    def resourceType(self):
-        return davxml.ResourceType.ischeduleinbox
-
-    def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8");
-
-    def isCollection(self):
-        return False
-
-    def isCalendarCollection(self):
-        return False
-
-    def isPseudoCalendarCollection(self):
-        return False
-
-    def deadProperties(self):
-        if not hasattr(self, "_dead_properties"):
-            self._dead_properties = NonePropertyStore(self)
-        return self._dead_properties
-
-    def etag(self):
-        return succeed(None)
-
-    def checkPreconditions(self, request):
-        return None
-
-    def render(self, request):
-        output = """<html>
-<head>
-<title>IMIP Delivery Resource</title>
-</head>
-<body>
-<h1>IMIP Delivery Resource.</h1>
-</body
-</html>"""
-
-        response = Response(200, {}, output)
-        response.headers.setHeader("content-type", MimeType("text", "html"))
-        return response
-
-    ##
-    # File
-    ##
-
-    def createSimilarFile(self, path):
-        log.err("Attempt to create clone %r of resource %r" % (path, self))
-        raise HTTPError(responsecode.NOT_FOUND)
-
-    ##
-    # ACL
-    ##
-
-    def defaultAccessControlList(self):
-        privs = (
-            davxml.Privilege(davxml.Read()),
-            davxml.Privilege(caldavxml.ScheduleDeliver()),
-        )
-        if config.Scheduling.CalDAV.OldDraftCompatibility:
-            privs += (davxml.Privilege(caldavxml.Schedule()),)
-        return davxml.ACL(
-            # DAV:Read, CalDAV:schedule-deliver for all principals (includes
-            # anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(*privs),
-                davxml.Protected(),
-            ),
-        )
-
-    def supportedPrivileges(self, request):
-        return succeed(deliverSchedulePrivilegeSet)
-
-
-class IMIPReplyInboxResource(IMIPInboxResource):
-
-    def renderHTTP(self, request):
-        """
-        Set up a transaction which will be used and committed by implicit
-        scheduling.
-        """
-        self.transaction = transactionFromRequest(request, self._newStore)
-        return super(IMIPReplyInboxResource, self).renderHTTP(request, self.transaction)
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The IMIP reply POST method (inbound)
-        """
-
-        # Check authentication and access controls
-        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
-
-        # Inject using the IMIPScheduler.
-        scheduler = IMIPScheduler(request, self)
-
-        # Do the POST processing treating this as a non-local schedule
-        result = (yield scheduler.doSchedulingViaPOST(self.transaction, use_request_headers=True))
-        returnValue(result.response())
-
-
-class IMIPInvitationInboxResource(IMIPInboxResource):
-
-    def __init__(self, parent, store, mailer):
-        super(IMIPInvitationInboxResource, self).__init__(parent, store)
-        self.mailer = mailer
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The IMIP invitation POST method (outbound)
-        """
-
-        # Check authentication and access controls
-        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
-
-        # Compute token, add to db, generate email and send it
-        calendar = (yield ical.Component.fromIStream(request.stream))
-        originator = request.headers.getRawHeaders("originator")[0]
-        recipient = request.headers.getRawHeaders("recipient")[0]
-        language = getLanguage(config)
-
-        if not (yield self.mailer.outbound(originator,
-            recipient, calendar, language=language)):
-            returnValue(Response(code=responsecode.BAD_REQUEST))
-
-        returnValue(Response(code=responsecode.OK))
-
-
-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.
-
-    SCHEMA:
-
-    Token Database:
-
-    ROW: TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP
-
-    """
-
-    dbType = "MAILGATEWAYTOKENS"
-    dbFilename = "mailgatewaytokens.sqlite"
-    dbFormatVersion = "1"
-
-
-    def __init__(self, path):
-        if path != ":memory:":
-            path = os.path.join(path, MailGatewayTokensDatabase.dbFilename)
-        super(MailGatewayTokensDatabase, self).__init__(path, True)
-
-    def createToken(self, organizer, attendee, icaluid, token=None):
-        if token is None:
-            token = str(uuid.uuid4())
-        self._db_execute(
-            """
-            insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
-            values (:1, :2, :3, :4, :5)
-            """, token, organizer, attendee, icaluid, datetime.date.today()
-        )
-        self._db_commit()
-        return token
-
-    def lookupByToken(self, token):
-        results = list(
-            self._db_execute(
-                """
-                select ORGANIZER, ATTENDEE, ICALUID from TOKENS
-                where TOKEN = :1
-                """, token
-            )
-        )
-
-        if len(results) != 1:
-            return None
-
-        return results[0]
-
-    def getToken(self, organizer, attendee, icaluid):
-        token = self._db_value_for_sql(
-            """
-            select TOKEN from TOKENS
-            where ORGANIZER = :1 and ATTENDEE = :2 and ICALUID = :3
-            """, organizer, attendee, icaluid
-        )
-        if token is not None:
-            # update the datestamp on the token to keep it from being purged
-            self._db_execute(
-                """
-                update TOKENS set DATESTAMP = :1 WHERE TOKEN = :2
-                """, datetime.date.today(), token
-            )
-            return str(token)
-        else:
-            return None
-
-    def deleteToken(self, token):
-        self._db_execute(
-            """
-            delete from TOKENS where TOKEN = :1
-            """, token
-        )
-        self._db_commit()
-
-    def purgeOldTokens(self, before):
-        self._db_execute(
-            """
-            delete from TOKENS where DATESTAMP < :1
-            """, before
-        )
-        self._db_commit()
-
-
-    def lowercase(self):
-        """
-        Lowercase mailto: addresses (and uppercase urn:uuid: addresses!) so
-        they can be located via normalized names.
-        """
-        rows = self._db_execute(
-            """
-            select ORGANIZER, ATTENDEE from TOKENS
-            """
-        )
-        for row in rows:
-            organizer = row[0]
-            attendee = row[1]
-            if organizer.lower().startswith("mailto:"):
-                self._db_execute(
-                    """
-                    update TOKENS set ORGANIZER = :1 WHERE ORGANIZER = :2
-                    """, organizer.lower(), organizer
-                )
-            else:
-                from txdav.base.datastore.util import normalizeUUIDOrNot
-                self._db_execute(
-                    """
-                    update TOKENS set ORGANIZER = :1 WHERE ORGANIZER = :2
-                    """, normalizeUUIDOrNot(organizer), organizer
-                )
-            # ATTENDEEs are always mailto: so unconditionally lower().
-            self._db_execute(
-                """
-                update TOKENS set ATTENDEE = :1 WHERE ATTENDEE = :2
-                """, attendee.lower(), attendee
-            )
-        self._db_commit()
-
-
-    def _db_version(self):
-        """
-        @return: the schema version assigned to this index.
-        """
-        return MailGatewayTokensDatabase.dbFormatVersion
-
-    def _db_type(self):
-        """
-        @return: the collection type assigned to this index.
-        """
-        return MailGatewayTokensDatabase.dbType
-
-    def _db_init_data_tables(self, q):
-        """
-        Initialise the underlying database tables.
-        @param q:           a database cursor to use.
-        """
-
-        #
-        # TOKENS table
-        #
-        q.execute(
-            """
-            create table TOKENS (
-                TOKEN       text,
-                ORGANIZER   text,
-                ATTENDEE    text,
-                ICALUID     text,
-                DATESTAMP   date
-            )
-            """
-        )
-        q.execute(
-            """
-            create index TOKENSINDEX on TOKENS (TOKEN)
-            """
-        )
-
-    def _db_upgrade_data_tables(self, q, old_version):
-        """
-        Upgrade the data from an older version of the DB.
-        @param q: a database cursor to use.
-        @param old_version: existing DB's version number
-        @type old_version: str
-        """
-        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):
-    """
-    ISchedule Inbox
-    """
-
-    def __init__(self, settings, mailer):
-        service.MultiService.__init__(self)
-        self.settings = settings
-        self.mailer = mailer
-
-        # 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, post = text.split('@')
-            pre, token = pre.split('+')
-            return token
-        except ValueError:
-            return None
-
-
-    def processDSN(self, calBody, msgId, fn):
-        calendar = ical.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
-        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 = ical.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']
-            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 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")
-
-        subjectFormat, labels = localizedLabels(language, canceled, inviteState)
-        details.update(labels)
-
-        details['subject'] = subjectFormat % {'summary' : details['summary']}
-
-        plainText = self.renderPlainText(details, (orgCN, orgEmail),
-                                         attendees, canceled)
-
-        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)
-
-        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: html text (C{str}, representing utf-8 encoded bytes)).
-        """
-        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]
-
-        return 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_error("IMAP factory connection failed")
-        self.retry(connector)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/mkcalendar.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/mkcalendar.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/mkcalendar.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -106,7 +106,6 @@
     
         if got_an_error:
             # Force a transaction error and proper clean-up
-            self.transactionError()
             errors.error()
             raise HTTPError(MultiStatusResponse([errors.response()]))
         

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/mkcol.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/mkcol.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/mkcol.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -182,7 +182,6 @@
 
         if got_an_error:
             # Clean up
-            self.transactionError()
             errors.error()
             raise HTTPError(Response(
                     code=responsecode.FORBIDDEN,

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_addressbook_query.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_addressbook_query.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_addressbook_query.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -63,11 +63,11 @@
 
     xmlfilter = addressbook_query.filter
     filter = addressbookqueryfilter.Filter(xmlfilter)
-    query  = addressbook_query.props
+    query = addressbook_query.props
     limit = addressbook_query.limit
 
     assert query is not None
-    
+
     if query.qname() == ("DAV:", "allprop"):
         propertiesForResource = report_common.allPropertiesForResource
         generate_address_data = False
@@ -78,7 +78,7 @@
 
     elif query.qname() == ("DAV:", "prop"):
         propertiesForResource = report_common.propertyListForResource
-       
+
         # Verify that any address-data element matches what we can handle
         result, message, generate_address_data = report_common.validPropertyListAddressDataTypeVersion(query)
         if not result:
@@ -88,7 +88,7 @@
                 (carddav_namespace, "supported-address-data"),
                 "Invalid address-data",
             ))
-        
+
     else:
         raise AssertionError("We shouldn't be here")
 
@@ -101,10 +101,10 @@
             "Invalid filter element",
         ))
 
-    matchcount = [0,]
-    max_number_of_results = [config.MaxQueryWithDataResults if generate_address_data else None,]
-    limited = [False,]
-    
+    matchcount = [0, ]
+    max_number_of_results = [config.MaxQueryWithDataResults if generate_address_data else None, ]
+    limited = [False, ]
+
     if limit:
         clientLimit = int(str(limit.childOfType(NResults)))
         if max_number_of_results[0] is None or clientLimit < max_number_of_results[0]:
@@ -118,15 +118,15 @@
         @param addrresource: the L{CalDAVResource} for an address book collection.
         @param uri: the uri for the address book collecton resource.
         """
-        
+
         def checkMaxResults():
             matchcount[0] += 1
             if max_number_of_results[0] is not None and matchcount[0] > max_number_of_results[0]:
                 raise NumberOfMatchesWithinLimits(max_number_of_results[0])
-           
-        
+
+
         @inlineCallbacks
-        def queryAddressBookObjectResource(resource, uri, name, vcard, query_ok = False):
+        def queryAddressBookObjectResource(resource, uri, name, vcard, query_ok=False):
             """
             Run a query on the specified vcard.
             @param resource: the L{CalDAVResource} for the vcard.
@@ -134,7 +134,7 @@
             @param name: the name of the resource.
             @param vcard: the L{Component} vcard read from the resource.
             """
-            
+
             if query_ok or filter.match(vcard):
                 # Check size of results is within limit
                 checkMaxResults()
@@ -143,7 +143,7 @@
                     href = davxml.HRef.fromString(joinURL(uri, name))
                 else:
                     href = davxml.HRef.fromString(uri)
-            
+
                 try:
                     yield report_common.responseForHref(request, responses, href, resource, propertiesForResource, query, vcard=vcard)
                 except ConcurrentModification:
@@ -153,21 +153,21 @@
                     # case, we ignore the now missing resource rather
                     # than raise an error for the entire report.
                     log.err("Missing resource during sync: %s" % (href,))
-              
-            
+
+
         @inlineCallbacks
         def queryDirectoryBackedAddressBook(directoryBackedAddressBook, addressBookFilter):
             """
             """
-            records, limited[0] = (yield directoryBackedAddressBook.directory.vCardRecordsForAddressBookQuery( addressBookFilter, query, max_number_of_results[0] ))
+            records, limited[0] = (yield directoryBackedAddressBook.directory.vCardRecordsForAddressBookQuery(addressBookFilter, query, max_number_of_results[0]))
             for vCardRecord in records:
-                
+
                 # match against original filter
                 if filter.match((yield vCardRecord.vCard())):
- 
+
                     # Check size of results is within limit
                     checkMaxResults()
-                   
+
                     try:
                         yield report_common.responseForHref(request, responses, vCardRecord.hRef(), vCardRecord, propertiesForResource, query, vcard=(yield vCardRecord.vCard()))
                     except ConcurrentModification:
@@ -177,70 +177,70 @@
                         # case, we ignore the now missing resource rather
                         # than raise an error for the entire report.
                         log.err("Missing resource during sync: %s" % (vCardRecord.hRef(),))
- 
-            
+
+
         directoryAddressBookLock = None
-        try:                
+        try:
 
             if addrresource.isDirectoryBackedAddressBookCollection() and addrresource.directory.cacheQuery:
-                
+
                 directory = addrresource.directory
                 if directory.liveQuery:
                     # if liveQuery and cacheQuery, get vCards into the directory address book on disk
-                    directoryAddressBookLock, limited[0] = (yield  directory.cacheVCardsForAddressBookQuery( filter, query, max_number_of_results[0] ) )
- 
+                    directoryAddressBookLock, limited[0] = (yield  directory.cacheVCardsForAddressBookQuery(filter, query, max_number_of_results[0]))
+
                 elif directory.maxDSQueryRecords and directory.maxDSQueryRecords < max_number_of_results[0]:
                     max_number_of_results[0] = directory.maxDSQueryRecords
-                   
-                
+
+
             elif not addrresource.isAddressBookCollection():
- 
+
                 #do UID lookup on last part of uri
                 resource_name = urllib.unquote(uri[uri.rfind("/") + 1:])
                 if resource_name.endswith(".vcf") and len(resource_name) > 4:
-    
+
                     # see if parent is directory backed address book
-                    parent = (yield  addrresource.locateParent( request, uri ) )
-    
+                    parent = (yield  addrresource.locateParent(request, uri))
+
                     if parent.isDirectoryBackedAddressBookCollection() and parent.directory.cacheQuery:
-                        
+
                         directory = parent.directory
                         if directory.liveQuery:
-                            vCardFilter = carddavxml.Filter( *[carddavxml.PropertyFilter(
-                                                        carddavxml.TextMatch.fromString(resource_name[:-4]), 
+                            vCardFilter = carddavxml.Filter(*[carddavxml.PropertyFilter(
+                                                        carddavxml.TextMatch.fromString(resource_name[:-4]),
                                                         name="UID", # attributes
-                                                        ),] )
+                                                        ), ])
                             vCardFilter = addressbookqueryfilter.Filter(vCardFilter)
-                            
-                            directoryAddressBookLock, limited[0] = (yield  directory.cacheVCardsForAddressBookQuery( vCardFilter, query, max_number_of_results[0] ) )
 
+                            directoryAddressBookLock, limited[0] = (yield  directory.cacheVCardsForAddressBookQuery(vCardFilter, query, max_number_of_results[0]))
+
                         elif directory.maxDSQueryRecords and directory.maxDSQueryRecords < max_number_of_results[0]:
                             max_number_of_results[0] = directory.maxDSQueryRecords
-   
-    
-        
+
+
+
             # Check whether supplied resource is an address book or an address book object resource
             if addrresource.isAddressBookCollection():
-    
+
                 if addrresource.isDirectoryBackedAddressBookCollection() and addrresource.directory.liveQuery and not addrresource.directory.cacheQuery:
-                    yield  maybeDeferred( queryDirectoryBackedAddressBook, addrresource, filter )
-                
+                    yield  maybeDeferred(queryDirectoryBackedAddressBook, addrresource, filter)
+
                 else:
 
                     # Do some optimisation of access control calculation by determining any inherited ACLs outside of
                     # the child resource loop and supply those to the checkPrivileges on each child.
                     filteredaces = (yield addrresource.inheritedACEsforChildren(request))
-                
+
                     # Check for disabled access
                     if filteredaces is not None:
                         # See whether the filter is valid for an index only query
                         index_query_ok = addrresource.index().searchValid(filter)
-                    
+
                         # Get list of children that match the search and have read access
                         names = [name for name, ignore_uid in (yield addrresource.index().search(filter))] #@UnusedVariable
                         if not names:
                             return
-                          
+
                         # Now determine which valid resources are readable and which are not
                         ok_resources = []
                         yield addrresource.findChildrenFaster(
@@ -255,47 +255,47 @@
                         )
                         for child, child_uri in ok_resources:
                             child_uri_name = child_uri[child_uri.rfind("/") + 1:]
-                            
+
                             if generate_address_data or not index_query_ok:
                                 vcard = yield child.vCard()
                                 assert vcard is not None, "vCard %s is missing from address book collection %r" % (child_uri_name, self)
                             else:
                                 vcard = None
-                            
-                            yield queryAddressBookObjectResource(child, uri, child_uri_name, vcard, query_ok = index_query_ok)
-                        
+
+                            yield queryAddressBookObjectResource(child, uri, child_uri_name, vcard, query_ok=index_query_ok)
+
             else:
-                
+
                 handled = False;
                 resource_name = urllib.unquote(uri[uri.rfind("/") + 1:])
                 if resource_name.endswith(".vcf") and len(resource_name) > 4:
-                    
+
                     # see if parent is directory backed address book
-                    parent = (yield  addrresource.locateParent( request, uri ) )
-    
+                    parent = (yield  addrresource.locateParent(request, uri))
+
                     if parent.isDirectoryBackedAddressBookCollection() and parent.directory.liveQuery and not parent.directory.cacheQuery:
- 
-                        vCardFilter = carddavxml.Filter( *[carddavxml.PropertyFilter(
-                                                    carddavxml.TextMatch.fromString(resource_name[:-4]), 
+
+                        vCardFilter = carddavxml.Filter(*[carddavxml.PropertyFilter(
+                                                    carddavxml.TextMatch.fromString(resource_name[:-4]),
                                                     name="UID", # attributes
-                                                    ),] )
+                                                    ), ])
                         vCardFilter = addressbookqueryfilter.Filter(vCardFilter)
-                        
-                        yield  maybeDeferred( queryDirectoryBackedAddressBook, parent, vCardFilter )
+
+                        yield  maybeDeferred(queryDirectoryBackedAddressBook, parent, vCardFilter)
                         handled = True
 
                 if not handled:
                     vcard = yield addrresource.vCard()
                     yield queryAddressBookObjectResource(addrresource, uri, None, vcard)
-        
+
             if limited[0]:
                 raise NumberOfMatchesWithinLimits(matchcount[0])
 
         finally:
             if directoryAddressBookLock:
                 yield directoryAddressBookLock.release()
-                
-    
+
+
     # Run report taking depth into account
     try:
         depth = request.headers.getHeader("depth", "0")
@@ -309,7 +309,7 @@
                         #davxml.ResponseDescription("Results limited by %s at %d" % resultsWereLimited),
                         davxml.ResponseDescription("Results limited to %d items" % e.maxLimit()),
                     ))
-    
+
     if not hasattr(request, "extendedLogItems"):
         request.extendedLogItems = {}
     request.extendedLogItems["responses"] = len(responses)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_multiget_common.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_multiget_common.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_multiget_common.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -38,7 +38,7 @@
 from twistedcaldav.config import config
 from twistedcaldav.method import report_common
 from txdav.common.icommondatastore import ConcurrentModification
-from twistedcaldav.method.report_common import COLLECTION_TYPE_CALENDAR,\
+from twistedcaldav.method.report_common import COLLECTION_TYPE_CALENDAR, \
     COLLECTION_TYPE_ADDRESSBOOK
 from twistedcaldav.query import addressbookqueryfilter
 
@@ -53,7 +53,7 @@
     # Make sure target resource is of the right type
     if not self.isCollection():
         parent = (yield self.locateParent(request, request.uri))
-        
+
         if collection_type == COLLECTION_TYPE_CALENDAR:
             if not parent.isPseudoCalendarCollection():
                 log.err("calendar-multiget report is not allowed on a resource outside of a calendar collection %s" % (self,))
@@ -66,12 +66,12 @@
     responses = []
 
     propertyreq = multiget.property
-    resources  = multiget.resources
+    resources = multiget.resources
 
     if not hasattr(request, "extendedLogItems"):
         request.extendedLogItems = {}
     request.extendedLogItems["rcount"] = len(resources)
-    
+
     hasData = False
     if propertyreq.qname() == ("DAV:", "allprop"):
         propertiesForResource = report_common.allPropertiesForResource
@@ -81,7 +81,7 @@
 
     elif propertyreq.qname() == ("DAV:", "prop"):
         propertiesForResource = report_common.propertyListForResource
-        
+
         if collection_type == COLLECTION_TYPE_CALENDAR:
             # Verify that any calendar-data element matches what we can handle
             result, message, hasData = report_common.validPropertyListCalendarDataTypeVersion(propertyreq)
@@ -133,11 +133,11 @@
         # Do some optimisation of access control calculation by determining any inherited ACLs outside of
         # the child resource loop and supply those to the checkPrivileges on each child.
         filteredaces = (yield self.inheritedACEsforChildren(request))
-    
+
         # Check for disabled access
         if filteredaces is None:
             disabled = True
-            
+
         # Check private events access status
         isowner = (yield self.isOwner(request))
 
@@ -147,7 +147,7 @@
         # Do some optimisation of access control calculation by determining any inherited ACLs outside of
         # the child resource loop and supply those to the checkPrivileges on each child.
         filteredaces = (yield self.inheritedACEsforChildren(request))
-    
+
         # Check for disabled access
         if filteredaces is None:
             disabled = True
@@ -167,7 +167,7 @@
 
         @inlineCallbacks
         def doResponse():
-            
+
             # Special for addressbooks
             if collection_type == COLLECTION_TYPE_ADDRESSBOOK:
                 if self.isDirectoryBackedAddressBookCollection() and self.directory.liveQuery:
@@ -185,7 +185,7 @@
                     valid_names.append(name)
             if not valid_names:
                 returnValue(None)
-        
+
             # Now determine which valid resources are readable and which are not
             ok_resources = []
             bad_resources = []
@@ -234,12 +234,12 @@
             # Indicate error for all missing resources
             for href in missing_resources:
                 responses.append(davxml.StatusResponse(davxml.HRef.fromString(href), davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
-    
+
         @inlineCallbacks
         def doDirectoryAddressBookResponse():
-            
+
             directoryAddressBookLock = None
-            try: 
+            try:
                 # Verify that requested resources are immediate children of the request-URI
                 # and get vCardFilters ;similar to "normal" case below but do not call getChild()
                 vCardFilters = []
@@ -250,22 +250,22 @@
                     if self._isChildURI(request, resource_uri) and resource_name.endswith(".vcf") and len(resource_name) > 4:
                         valid_hrefs.append(href)
                         vCardFilters.append(carddavxml.PropertyFilter(
-                                                carddavxml.TextMatch.fromString(resource_name[:-4]), 
+                                                carddavxml.TextMatch.fromString(resource_name[:-4]),
                                                 name="UID", # attributes
                                             ))
                     elif not self.directory.cacheQuery:
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
-                       
+
                 # exit if not valid           
                 if not vCardFilters or not valid_hrefs:
-                    returnValue( None )
-                     
-                addressBookFilter = carddavxml.Filter( *vCardFilters )
+                    returnValue(None)
+
+                addressBookFilter = carddavxml.Filter(*vCardFilters)
                 addressBookFilter = addressbookqueryfilter.Filter(addressBookFilter)
                 if self.directory.cacheQuery:
                     # add vcards to directory address book and run "normal case" below
                     limit = config.DirectoryAddressBook.MaxQueryResults
-                    directoryAddressBookLock, limited = (yield  self.directory.cacheVCardsForAddressBookQuery(addressBookFilter, propertyreq, limit) )
+                    directoryAddressBookLock, limited = (yield  self.directory.cacheVCardsForAddressBookQuery(addressBookFilter, propertyreq, limit))
                     if limited:
                         log.err("Too many results in multiget report: %d" % len(resources))
                         raise HTTPError(ErrorResponse(
@@ -276,7 +276,7 @@
                 else:
                     #get vCards and filter
                     limit = config.DirectoryAddressBook.MaxQueryResults
-                    vCardRecords, limited = (yield self.directory.vCardRecordsForAddressBookQuery( addressBookFilter, propertyreq, limit ))
+                    vCardRecords, limited = (yield self.directory.vCardRecordsForAddressBookQuery(addressBookFilter, propertyreq, limit))
                     if limited:
                         log.err("Too many results in multiget report: %d" % len(resources))
                         raise HTTPError(ErrorResponse(
@@ -284,7 +284,7 @@
                             (dav_namespace, "number-of-matches-within-limits"),
                             "Too many results",
                         ))
-                   
+
                     for href in valid_hrefs:
                         matchingRecord = None
                         for vCardRecord in vCardRecords:
@@ -304,30 +304,30 @@
             yield doResponse()
         else:
             for href in resources:
-    
+
                 resource_uri = str(href)
-    
+
                 # Do href checks
                 if requestURIis == "calendar":
                     pass
                 elif requestURIis == "addressbook":
                     pass
-        
+
                 # TODO: we can optimize this one in a similar manner to the calendar case
                 elif requestURIis == "collection":
                     name = unquote(resource_uri[resource_uri.rfind("/") + 1:])
                     if not self._isChildURI(request, resource_uri, False):
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
                         continue
-     
+
                     child = (yield request.locateResource(resource_uri))
-    
+
                     if not child or not child.exists():
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
                         continue
-    
+
                     parent = (yield child.locateParent(request, resource_uri))
-    
+
                     if collection_type == COLLECTION_TYPE_CALENDAR:
                         if not parent.isCalendarCollection() or not (yield parent.index().resourceExists(name)):
                             responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
@@ -336,18 +336,18 @@
                         if not parent.isAddressBookCollection() or not (yield parent.index().resourceExists(name)):
                             responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
                             continue
-                    
+
                     # Check privileges on parent - must have at least DAV:read
                     try:
                         yield parent.checkPrivileges(request, (davxml.Read(),))
                     except AccessDeniedError:
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
                         continue
-                    
+
                     # Cache the last parent's inherited aces for checkPrivileges optimization
                     if lastParent != parent:
                         lastParent = parent
-                
+
                         # Do some optimisation of access control calculation by determining any inherited ACLs outside of
                         # the child resource loop and supply those to the checkPrivileges on each child.
                         filteredaces = (yield parent.inheritedACEsforChildren(request))
@@ -359,9 +359,9 @@
                     if (resource_uri != request.uri) or not self.exists():
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
                         continue
-    
+
                     parent = (yield self.locateParent(request, resource_uri))
-    
+
                     if collection_type == COLLECTION_TYPE_CALENDAR:
                         if not parent.isPseudoCalendarCollection() or not (yield parent.index().resourceExists(name)):
                             responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
@@ -371,21 +371,21 @@
                             responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
                             continue
                     child = self
-            
+
                     # Do some optimisation of access control calculation by determining any inherited ACLs outside of
                     # the child resource loop and supply those to the checkPrivileges on each child.
                     filteredaces = (yield parent.inheritedACEsforChildren(request))
 
                     # Check private events access status
                     isowner = (yield parent.isOwner(request))
-        
+
                 # Check privileges - must have at least DAV:read
                 try:
                     yield child.checkPrivileges(request, (davxml.Read(),), inherited_aces=filteredaces)
                 except AccessDeniedError:
                     responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
                     continue
-        
+
                 yield report_common.responseForHref(request, responses, href, child, propertiesForResource, propertyreq, isowner=isowner)
 
     returnValue(MultiStatusResponse(responses))

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_sync_collection.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_sync_collection.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/method/report_sync_collection.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -46,7 +46,7 @@
     """
     Generate a sync-collection REPORT.
     """
-    
+
     # These resource support the report
     if not config.EnableSyncReport or element.Report(element.SyncCollection(),) not in self.supportedReports():
         log.err("sync-collection report is only allowed on calendar/inbox/addressbook/notification collection resources %s" % (self,))
@@ -55,7 +55,7 @@
             element.SupportedReport(),
             "Report not supported on this resource",
         ))
-   
+
     responses = []
 
     # Process Depth and sync-level for backwards compatibility
@@ -66,13 +66,13 @@
     else:
         depth = request.headers.getHeader("depth", None)
         descriptor = "Depth header without DAV:sync-level"
-    
+
     if depth not in ("1", "infinity"):
         log.err("sync-collection report with invalid depth header: %s" % (depth,))
         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid %s value" % (descriptor,)))
-        
-    propertyreq = sync_collection.property.children if sync_collection.property else None 
-    
+
+    propertyreq = sync_collection.property.children if sync_collection.property else None
+
     @inlineCallbacks
     def _namedPropertiesForResource(request, props, resource, forbidden=False):
         """
@@ -87,7 +87,7 @@
             responsecode.FORBIDDEN : [],
             responsecode.NOT_FOUND : [],
         }
-        
+
         for property in props:
             if isinstance(property, element.WebDAVElement):
                 qname = property.qname()
@@ -110,9 +110,9 @@
                         properties_by_status[status].append(propertyName(qname))
                 else:
                     properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
-        
+
         returnValue(properties_by_status)
-    
+
     # Do some optimization of access control calculation by determining any inherited ACLs outside of
     # the child resource loop and supply those to the checkPrivileges on each child.
     filteredaces = (yield self.inheritedACEsforChildren(request))
@@ -173,11 +173,11 @@
     for name in removed:
         href = element.HRef.fromString(joinURL(request.uri, name))
         responses.append(element.StatusResponse(element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.NOT_FOUND)))
-    
+
     for name in notallowed:
         href = element.HRef.fromString(joinURL(request.uri, name))
         responses.append(element.StatusResponse(element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.NOT_ALLOWED)))
-    
+
     if not hasattr(request, "extendedLogItems"):
         request.extendedLogItems = {}
     request.extendedLogItems["responses"] = len(responses)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/resource.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -318,8 +318,14 @@
         otherResource.associateWithTransaction(self._associatedTransaction)
 
 
-    def transactionError(self):
+    def methodRaisedException(self, failure):
+        """
+        An C{http_METHOD} method raised an exception.  Any type of exception,
+        including those that result in perfectly valid HTTP responses, should
+        abort the transaction.
+        """
         self._transactionError = True
+        return super(CalDAVResource, self).methodRaisedException(failure)
 
 
     @inlineCallbacks
@@ -528,6 +534,7 @@
         res = (yield self._hasGlobalProperty(property, request))
         returnValue(res)
 
+
     def _hasSharedProperty(self, qname, request):
 
         # Always have default alarms on shared calendars
@@ -542,6 +549,7 @@
         p = self.deadProperties().contains(qname)
         return p
 
+
     def _hasGlobalProperty(self, property, request):
         """
         Need to special case schedule-calendar-transp for backwards compatability.
@@ -596,6 +604,7 @@
         p = self.deadProperties().get(qname)
         return p
 
+
     @inlineCallbacks
     def _readGlobalProperty(self, qname, property, request):
 
@@ -1011,7 +1020,8 @@
         """
         See L{ICalDAVResource.isSpecialCollection}.
         """
-        if not self.isCollection(): return False
+        if not self.isCollection():
+            return False
 
         try:
             resourcetype = self.resourceType()
@@ -2129,6 +2139,7 @@
 
         return props
 
+
     def url(self):
         return joinURL(self.parent.url(), self.name, "/")
 
@@ -2578,7 +2589,7 @@
         from twistedcaldav.storebridge import StoreScheduleInboxResource
         self._provisionedChildren["inbox"] = StoreScheduleInboxResource.maybeCreateInbox
 
-        from twistedcaldav.schedule import ScheduleOutboxResource
+        from twistedcaldav.scheduling.caldav.resource import ScheduleOutboxResource
         self._provisionedChildren["outbox"] = ScheduleOutboxResource
 
         if config.EnableDropBox:
@@ -2894,7 +2905,7 @@
         if defaultAddressBook is None or not defaultAddressBook.exists():
             addressbooks = yield self._newStoreHome.addressbooks()
             ownedAddressBooks = [addressbook for addressbook in addressbooks if addressbook.owned()]
-            ownedAddressBooks.sort(key=lambda ab:ab.name())
+            ownedAddressBooks.sort(key=lambda ab: ab.name())
 
             # These are only unshared children
             # FIXME: the back-end should re-provision a default addressbook here.

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/schedule.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/schedule.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,606 +0,0 @@
-# -*- test-case-name: twistedcaldav.directory.test.test_calendar -*-
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-CalDAV scheduling resources.
-"""
-
-__all__ = [
-    "ScheduleInboxResource",
-    "ScheduleOutboxResource",
-    "IScheduleInboxResource",
-]
-
-
-from twext.web2 import responsecode
-from txdav.xml import element as davxml
-from txdav.xml.rfc2518 import HRef
-from twext.web2.dav.http import ErrorResponse, MultiStatusResponse
-from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.dav.resource import davPrivilegeSet
-from twext.web2.dav.util import joinURL, normalizeURL
-from twext.web2.http import HTTPError
-from twext.web2.http import Response
-from twext.web2.http_headers import MimeType
-
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
-
-from twistedcaldav import caldavxml, customxml
-from twistedcaldav.caldavxml import caldav_namespace, Opaque,\
-    CalendarFreeBusySet, ScheduleCalendarTransp
-from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.ical import allowedComponents
-from twistedcaldav.extensions import DAVResource
-from twistedcaldav.resource import CalDAVResource, ReadOnlyNoCopyResourceMixIn
-from twistedcaldav.resource import isCalendarCollectionResource
-from twistedcaldav.scheduling.scheduler import CalDAVScheduler, IScheduleScheduler
-
-from txdav.base.propertystore.base import PropertyName
-
-def _schedulePrivilegeSet(deliver):
-    edited = False
-
-    top_supported_privileges = []
-
-    for supported_privilege in davPrivilegeSet.childrenOfType(davxml.SupportedPrivilege):
-        all_privilege = supported_privilege.childOfType(davxml.Privilege)
-        if isinstance(all_privilege.children[0], davxml.All):
-            all_description = supported_privilege.childOfType(davxml.Description)
-            all_supported_privileges = list(supported_privilege.childrenOfType(davxml.SupportedPrivilege))
-            all_supported_privileges.append(
-                davxml.SupportedPrivilege(
-                    davxml.Privilege(caldavxml.ScheduleDeliver() if deliver else caldavxml.ScheduleSend()),
-                    davxml.Description("schedule privileges for current principal", **{"xml:lang": "en"}),
-                ),
-            )
-            if config.Scheduling.CalDAV.OldDraftCompatibility:
-                all_supported_privileges.append(
-                    davxml.SupportedPrivilege(
-                        davxml.Privilege(caldavxml.Schedule()),
-                        davxml.Description("old-style schedule privileges for current principal", **{"xml:lang": "en"}),
-                    ),
-                )
-            top_supported_privileges.append(
-                davxml.SupportedPrivilege(all_privilege, all_description, *all_supported_privileges)
-            )
-            edited = True
-        else:
-            top_supported_privileges.append(supported_privilege)
-
-    assert edited, "Structure of davPrivilegeSet changed in a way that I don't know how to extend for schedulePrivilegeSet"
-
-    return davxml.SupportedPrivilegeSet(*top_supported_privileges)
-
-deliverSchedulePrivilegeSet = _schedulePrivilegeSet(True)
-sendSchedulePrivilegeSet = _schedulePrivilegeSet(False)
-
-class CalendarSchedulingCollectionResource (CalDAVResource):
-    """
-    CalDAV principal resource.
-
-    Extends L{DAVResource} to provide CalDAV scheduling collection
-    functionality.
-    """
-    def __init__(self, parent):
-        """
-        @param parent: the parent resource of this one.
-        """
-        assert parent is not None
-
-        super(CalendarSchedulingCollectionResource, self).__init__(principalCollections=parent.principalCollections())
-
-        self.parent = parent
-
-    def isCollection(self):
-        return True
-
-    def isCalendarCollection(self):
-        return False
-
-    def isPseudoCalendarCollection(self):
-        return True
-
-    def supportedReports(self):
-        result = super(CalDAVResource, self).supportedReports()
-        result.append(davxml.Report(caldavxml.CalendarQuery(),))
-        result.append(davxml.Report(caldavxml.CalendarMultiGet(),))
-        # free-busy report not allowed
-        if config.EnableSyncReport:
-            # Only allowed on calendar/inbox/addressbook collections
-            result.append(davxml.Report(davxml.SyncCollection(),))
-        return result
-
-class ScheduleInboxResource (CalendarSchedulingCollectionResource):
-    """
-    CalDAV schedule Inbox resource.
-
-    Extends L{DAVResource} to provide CalDAV functionality.
-    """
-
-    def liveProperties(self):
-        
-        return super(ScheduleInboxResource, self).liveProperties() + (
-            caldavxml.CalendarFreeBusySet.qname(),
-            caldavxml.ScheduleDefaultCalendarURL.qname(),
-            customxml.ScheduleDefaultTasksURL.qname(),
-        )
-
-    def resourceType(self):
-        return davxml.ResourceType.scheduleInbox
-
-    @inlineCallbacks
-    def readProperty(self, property, request):
-        if type(property) is tuple:
-            qname = property
-        else:
-            qname = property.qname()
-
-        if qname == caldavxml.CalendarFreeBusySet.qname():
-            # Always return at least an empty list
-            if not self.hasDeadProperty(property):
-                top = self.parent.url()
-                values = []
-                for cal in (yield self.parent._newStoreHome.calendars()):
-                    prop = cal.properties().get(PropertyName.fromString(ScheduleCalendarTransp.sname())) 
-                    if prop == ScheduleCalendarTransp(Opaque()):
-                        values.append(HRef(joinURL(top, cal.name())))
-                returnValue(CalendarFreeBusySet(*values))
-        elif qname in (caldavxml.ScheduleDefaultCalendarURL.qname(), customxml.ScheduleDefaultTasksURL.qname()):
-            result = (yield self.readDefaultCalendarProperty(request, qname))
-            returnValue(result)
-            
-        result = (yield super(ScheduleInboxResource, self).readProperty(property, request))
-        returnValue(result)
-
-    @inlineCallbacks
-    def writeProperty(self, property, request):
-        assert isinstance(property, davxml.WebDAVElement)
-
-        # Strictly speaking CS:calendar-availability is a live property in the sense that the
-        # server enforces what can be stored, however it need not actually
-        # exist so we cannot list it in liveProperties on this resource, since its
-        # its presence there means that hasProperty will always return True for it.
-        if property.qname() == customxml.CalendarAvailability.qname():
-            if not property.valid():
-                raise HTTPError(ErrorResponse(
-                    responsecode.CONFLICT,
-                    (caldav_namespace, "valid-calendar-data"),
-                    description="Invalid property"
-                ))
-
-        elif property.qname() == caldavxml.CalendarFreeBusySet.qname():
-            # Verify that the calendars added in the PROPPATCH are valid. We do not check
-            # whether existing items in the property are still valid - only new ones.
-            property.children = [davxml.HRef(normalizeURL(str(href))) for href in property.children]
-            new_calendars = set([str(href) for href in property.children])
-            if not self.hasDeadProperty(property):
-                old_calendars = set()
-            else:
-                old_calendars = set([normalizeURL(str(href)) for href in self.readDeadProperty(property).children])
-            added_calendars = new_calendars.difference(old_calendars)
-            for href in added_calendars:
-                cal = (yield request.locateResource(str(href)))
-                if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
-                    # Validate that href's point to a valid calendar.
-                    raise HTTPError(ErrorResponse(
-                        responsecode.CONFLICT,
-                        (caldav_namespace, "valid-calendar-url"),
-                        "Invalid URI",
-                    ))
-            for href in tuple(new_calendars):
-                cal = (yield request.locateResource(str(href)))
-                if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
-                    new_calendars.remove(href)
-            property.children = [davxml.HRef(href) for href in new_calendars]
-
-        elif property.qname() in (caldavxml.ScheduleDefaultCalendarURL.qname(), customxml.ScheduleDefaultTasksURL.qname()):
-            property = (yield self.writeDefaultCalendarProperty(request, property))
-
-        yield super(ScheduleInboxResource, self).writeProperty(property, request)
-
-    def processFreeBusyCalendar(self, uri, addit):
-        uri = normalizeURL(uri)
-
-        if not self.hasDeadProperty(caldavxml.CalendarFreeBusySet.qname()):
-            fbset = set()
-        else:
-            fbset = set([normalizeURL(str(href)) for href in self.readDeadProperty(caldavxml.CalendarFreeBusySet.qname()).children])
-        if addit:
-            if uri not in fbset:
-                fbset.add(uri)
-                self.writeDeadProperty(caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in fbset]))
-        else:
-            if uri in fbset:
-                fbset.remove(uri)
-                self.writeDeadProperty(caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in fbset]))
-
-    @inlineCallbacks
-    def readDefaultCalendarProperty(self, request, qname):
-        """
-        Read either the default VEVENT or VTODO calendar property. Try to pick one if not present.
-        """
-        
-        tasks = qname == customxml.ScheduleDefaultTasksURL.qname()
-
-        # Must have a valid default
-        try:
-            defaultCalendarProperty = self.readDeadProperty(qname)
-        except HTTPError:
-            defaultCalendarProperty = None
-        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():
-                returnValue(defaultCalendarProperty) 
-        
-        # Default is not valid - we have to try to pick one
-        defaultCalendarProperty = (yield self.pickNewDefaultCalendar(request, tasks=tasks))
-        returnValue(defaultCalendarProperty)
-
-    @inlineCallbacks
-    def writeDefaultCalendarProperty(self, request, property):
-        """
-        Write either the default VEVENT or VTODO calendar property, validating and canonicalizing the value
-        """
-        tasks = property.qname() == customxml.ScheduleDefaultTasksURL
-        componentType = "VTODO" if tasks else "VEVENT"
-        prop_to_set = customxml.ScheduleDefaultTasksURL if tasks else caldavxml.ScheduleDefaultCalendarURL
-        error_element = (calendarserver_namespace, "valid-schedule-default-tasks-URL") if tasks else (caldav_namespace, "valid-schedule-default-calendar-URL")
-
-        # Verify that the calendar added in the PROPPATCH is valid.
-        property.children = [davxml.HRef(normalizeURL(str(href))) for href in property.children]
-        new_calendar = [str(href) for href in property.children]
-        cal = None
-        if len(new_calendar) == 1:
-            calURI = str(new_calendar[0])
-            cal = (yield request.locateResource(str(new_calendar[0])))
-
-        # 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):
-            # Validate that href's point to a valid calendar.
-            raise HTTPError(ErrorResponse(
-                responsecode.CONFLICT,
-                error_element,
-                "Invalid URI",
-            ))
-        else:
-            # Canonicalize the URL to __uids__ form
-            calURI = (yield cal.canonicalURL(request))
-            property = prop_to_set(davxml.HRef(calURI))
-            returnValue(property)
-
-    @inlineCallbacks
-    def pickNewDefaultCalendar(self, request, tasks=False):
-        """
-        First see if default provisioned calendar exists in the calendar home and pick that. Otherwise
-        pick another from the calendar home.
-        """
-        
-        componentType = "VTODO" if tasks else "VEVENT"
-        test_name = "tasks" if tasks else "calendar"
-        prop_to_set = customxml.ScheduleDefaultTasksURL if tasks else caldavxml.ScheduleDefaultCalendarURL
-
-        calendarHomeURL = self.parent.url()
-        defaultCalendarURL = joinURL(calendarHomeURL, test_name)
-        defaultCalendar = (yield request.locateResource(defaultCalendarURL))
-        if defaultCalendar is None or not defaultCalendar.exists():
-            # Really, the dead property shouldn't be necessary, and this should
-            # be entirely computed by a back-end method like 'defaultCalendar()'
-            
-            @inlineCallbacks
-            def _findDefault():
-                for calendarName in (yield self.parent._newStoreHome.listCalendars()):  # These are only unshared children
-                    if calendarName == "inbox":
-                        continue
-                    calendar = (yield self.parent._newStoreHome.calendarWithName(calendarName))
-                    if not calendar.owned():
-                        continue
-                    if not calendar.isSupportedComponent(componentType):
-                        continue
-                    break
-                else:
-                    calendarName = None
-                returnValue(calendarName)
-            
-            foundName = yield _findDefault()
-            if foundName is None:
-                # Create a default and try and get its name again
-                yield self.parent._newStoreHome.ensureDefaultCalendarsExist()
-                foundName = yield _findDefault()
-                if foundName is None:
-                    # Failed to even create a default - bad news...
-                    raise RuntimeError("No valid calendars to use as a default %s calendar." % (componentType,))
-
-            defaultCalendarURL = joinURL(calendarHomeURL, foundName)
-
-        prop = prop_to_set(davxml.HRef(defaultCalendarURL))
-        self.writeDeadProperty(prop)
-        returnValue(prop)
-
-    @inlineCallbacks
-    def defaultCalendar(self, request, componentType):
-        """
-        Find the default calendar for the supplied iCalendar component type. If one does
-        not exist, automatically provision it. 
-        """
-
-        # Check any default calendar property first - this will create if none exists
-        default = (yield self.readProperty(caldavxml.ScheduleDefaultCalendarURL.qname(), request))
-        if len(default.children) == 1:
-            defaultURL = str(default.children[0])
-            default = (yield request.locateResource(defaultURL))
-        else:
-            default = None
-
-        # Check that default handles the component type
-        if default is not None:
-            if not default.isSupportedComponent(componentType):
-                default = None
-        
-        # Must have a default - provision one if not
-        if default is None:
-            
-            # Try to find a calendar supporting the required component type. If there are multiple, pick
-            # the one with the oldest created timestamp as that will likely be the initial provision.
-            for calendarName in (yield self.parent._newStoreHome.listCalendars()):  # These are only unshared children
-                if calendarName == "inbox":
-                    continue
-                calendar = (yield self.parent._newStoreHome.calendarWithName(calendarName))
-                if not calendar.isSupportedComponent(componentType):
-                    continue
-                if default is None or calendar.created() < default.created():
-                    default = calendar
-            
-            # If none can be found, provision one
-            if default is None:
-                new_name = "%ss" % (componentType.lower()[1:],)
-                default = yield self.parent._newStoreHome.createCalendarWithName(new_name)
-                yield default.setSupportedComponents(componentType.upper())
-            
-            # Need L{DAVResource} object to return not new store object
-            default = (yield request.locateResource(joinURL(self.parent.url(), default.name())))
-        
-        returnValue(default)
-
-    @inlineCallbacks
-    def isDefaultCalendar(self, request, calendar):
-        """
-        Is the supplied calendar one of the possible default calendars.
-        """
-        assert calendar.isCalendarCollection()
-        
-        # Not allowed to delete the default calendar
-        for default_prop in (caldavxml.ScheduleDefaultCalendarURL, customxml.ScheduleDefaultTasksURL,):
-            default = (yield self.readProperty(default_prop.qname(), request))
-            if default and len(default.children) == 1:
-                defaultURL = normalizeURL(str(default.children[0]))
-                myURL = (yield calendar.canonicalURL(request))
-                if defaultURL == myURL:
-                    returnValue(default_prop)
-
-        returnValue(None)
-
-    ##
-    # ACL
-    ##
-
-    def supportedPrivileges(self, request):
-        return succeed(deliverSchedulePrivilegeSet)
-
-    def defaultAccessControlList(self):
-        
-        privs = (
-            davxml.Privilege(caldavxml.ScheduleDeliver()),
-        )
-        if config.Scheduling.CalDAV.OldDraftCompatibility:
-            privs += (davxml.Privilege(caldavxml.Schedule()),)
-
-        return davxml.ACL(
-            # CalDAV:schedule-deliver for any authenticated user
-            davxml.ACE(
-                davxml.Principal(davxml.Authenticated()),
-                davxml.Grant(*privs),
-            ),
-        )
-
-class ScheduleOutboxResource (CalendarSchedulingCollectionResource):
-    """
-    CalDAV schedule Outbox resource.
-
-    Extends L{DAVResource} to provide CalDAV functionality.
-    """
-
-    def resourceType(self):
-        return davxml.ResourceType.scheduleOutbox
-
-    def getSupportedComponentSet(self):
-        return caldavxml.SupportedCalendarComponentSet(
-            *[caldavxml.CalendarComponent(name=item) for item in allowedComponents]
-        )
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The CalDAV POST method.
-    
-        This uses a generator function yielding either L{waitForDeferred} objects or L{Response} objects.
-        This allows for code that follows a 'linear' execution pattern rather than having to use nested
-        L{Deferred} callbacks. The logic is easier to follow this way plus we don't run into deep nesting
-        issues which the other approach would have with large numbers of recipients.
-        """
-        # Check authentication and access controls
-        yield self.authorize(request, (caldavxml.ScheduleSend(),))
-
-        # This is a local CALDAV scheduling operation.
-        scheduler = CalDAVScheduler(request, self)
-
-        # Do the POST processing treating
-        result = (yield scheduler.doSchedulingViaPOST(self._associatedTransaction))
-        returnValue(result.response())
-
-
-    ##
-    # ACL
-    ##
-
-    def supportedPrivileges(self, request):
-        return succeed(sendSchedulePrivilegeSet)
-
-    def defaultAccessControlList(self):
-        if config.EnableProxyPrincipals:
-            myPrincipal = self.parent.principalForRecord()
-    
-            privs = (
-                davxml.Privilege(caldavxml.ScheduleSend()),
-            )
-            if config.Scheduling.CalDAV.OldDraftCompatibility:
-                privs += (davxml.Privilege(caldavxml.Schedule()),)
-    
-            return davxml.ACL(
-                # CalDAV:schedule for associated write proxies
-                davxml.ACE(
-                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-write"))),
-                    davxml.Grant(*privs),
-                    davxml.Protected(),
-                ),
-            )
-        else:
-            return super(ScheduleOutboxResource, self).defaultAccessControlList()
-
-    def report_urn_ietf_params_xml_ns_caldav_calendar_query(self, request, calendar_query):
-        return succeed(MultiStatusResponse(()))
-        
-    def report_urn_ietf_params_xml_ns_caldav_calendar_multiget(self, request, multiget):
-        responses = [davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)) for href in multiget.resources]
-        return succeed(MultiStatusResponse((responses)))
-
-class IScheduleInboxResource (ReadOnlyNoCopyResourceMixIn, DAVResource):
-    """
-    iSchedule Inbox resource.
-
-    Extends L{DAVResource} to provide iSchedule inbox functionality.
-    """
-
-    def __init__(self, parent, store):
-        """
-        @param parent: the parent resource of this one.
-        """
-        assert parent is not None
-
-        DAVResource.__init__(self, principalCollections=parent.principalCollections())
-
-        self.parent = parent
-        self._newStore = store
-
-    def deadProperties(self):
-        if not hasattr(self, "_dead_properties"):
-            self._dead_properties = NonePropertyStore(self)
-        return self._dead_properties
-
-    def etag(self):
-        return succeed(None)
-
-    def checkPreconditions(self, request):
-        return None
-
-    def resourceType(self):
-        return davxml.ResourceType.ischeduleinbox
-
-    def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8");
-
-    def isCollection(self):
-        return False
-
-    def isCalendarCollection(self):
-        return False
-
-    def isPseudoCalendarCollection(self):
-        return False
-
-    def principalForCalendarUserAddress(self, address):
-        for principalCollection in self.principalCollections():
-            principal = principalCollection.principalForCalendarUserAddress(address)
-            if principal is not None:
-                return principal
-        return None
-
-    def render(self, request):
-        output = """<html>
-<head>
-<title>Server To Server Inbox Resource</title>
-</head>
-<body>
-<h1>Server To Server Inbox Resource.</h1>
-</body
-</html>"""
-
-        response = Response(200, {}, output)
-        response.headers.setHeader("content-type", MimeType("text", "html"))
-        return response
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The server-to-server POST method.
-        """
-
-        # Check authentication and access controls
-        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
-
-        # This is a server-to-server scheduling operation.
-        scheduler = IScheduleScheduler(request, self)
-
-        # Need a transaction to work with
-        txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
-        request._newStoreTransaction = txn
-         
-        # Do the POST processing treating this as a non-local schedule
-        try:
-            result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
-        except Exception, e:
-            yield txn.abort()
-            raise e
-        else:
-            yield txn.commit()
-        returnValue(result.response())
-
-    ##
-    # ACL
-    ##
-
-    def supportedPrivileges(self, request):
-        return succeed(deliverSchedulePrivilegeSet)
-
-    def defaultAccessControlList(self):
-        privs = (
-            davxml.Privilege(davxml.Read()),
-            davxml.Privilege(caldavxml.ScheduleDeliver()),
-        )
-        if config.Scheduling.CalDAV.OldDraftCompatibility:
-            privs += (davxml.Privilege(caldavxml.Schedule()),)
-
-        return davxml.ACL(
-            # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(*privs),
-                davxml.Protected(),
-            ),
-        )

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/addressmapping.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/addressmapping.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/addressmapping.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -20,11 +20,11 @@
 
 from twistedcaldav.config import config
 from twistedcaldav.memcacher import Memcacher
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
+from twistedcaldav.scheduling.caldav.delivery import ScheduleViaCalDAV
 from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.imip import ScheduleViaIMip
-from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
-from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser, EmailCalendarUser, InvalidCalendarUser,\
+from twistedcaldav.scheduling.imip.delivery import ScheduleViaIMip
+from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
+from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser, EmailCalendarUser, InvalidCalendarUser, \
     calendarUserFromPrincipal
 
 __all__ = [
@@ -42,15 +42,16 @@
     """
     Class that maps a calendar user address into a delivery service type.
     """
-    
+
     def __init__(self):
-        
+
         # We are going to cache mappings whilst running
         self.cache = Memcacher("ScheduleAddressMapper", no_invalidation=True)
 
+
     @inlineCallbacks
     def getCalendarUser(self, cuaddr, principal):
-        
+
         # If we have a principal always treat the user as local or partitioned
         if principal:
             returnValue(calendarUserFromPrincipal(cuaddr, principal))
@@ -66,31 +67,34 @@
         else:
             returnValue(InvalidCalendarUser(cuaddr))
 
+
     @inlineCallbacks
     def getCalendarUserServiceType(self, cuaddr):
 
         # Try cache first
         cuaddr_type = (yield self.cache.get(str(cuaddr)))
         if cuaddr_type is None:
-            
+
             serviceTypes = (ScheduleViaCalDAV,)
             if config.Scheduling[DeliveryService.serviceType_ischedule]["Enabled"]:
                 serviceTypes += (ScheduleViaISchedule,)
             if config.Scheduling[DeliveryService.serviceType_imip]["Enabled"]:
                 serviceTypes += (ScheduleViaIMip,)
             for service in serviceTypes:
-                if service.matchCalendarUserAddress(cuaddr):
+                matched = (yield service.matchCalendarUserAddress(cuaddr))
+                if matched:
                     yield self.cache.set(str(cuaddr), service.serviceType())
                     returnValue(service.serviceType())
 
         returnValue(cuaddr_type)
 
+
     def isCalendarUserInMyDomain(self, cuaddr):
 
         # Check whether it is a possible local address
         def _gotResult(serviceType):
             return serviceType == DeliveryService.serviceType_caldav
-            
+
         d = self.getCalendarUserServiceType(cuaddr)
         d.addCallback(_gotResult)
         return d

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/__init__.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/__init__.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/caldav/__init__.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/delivery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/delivery.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,319 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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.python.log import Logger
-from twext.web2.dav.http import ErrorResponse
-
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
-from twisted.python.failure import Failure
-from twext.web2 import responsecode
-from txdav.xml import element as davxml
-from twext.web2.dav.resource import AccessDeniedError
-from twext.web2.dav.util import joinURL
-from twext.web2.http import HTTPError
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.method import report_common
-from twistedcaldav.resource import isCalendarCollectionResource
-from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser, \
-    PartitionedCalendarUser, OtherServerCalendarUser
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.scheduling.processing import ImplicitProcessor, ImplicitProcessorException
-
-import hashlib
-import uuid
-
-
-"""
-Handles the sending of scheduling messages to the server itself. This will cause
-actual processing of the delivery of the message to the recipient's inbox, via the
-L{ImplicitProcessor} class.
-"""
-
-__all__ = [
-    "ScheduleViaCalDAV",
-]
-
-log = Logger()
-
-class ScheduleViaCalDAV(DeliveryService):
-
-    def __init__(self, scheduler, recipients, responses, freebusy):
-
-        self.scheduler = scheduler
-        self.recipients = recipients
-        self.responses = responses
-        self.freebusy = freebusy
-
-
-    @classmethod
-    def serviceType(cls):
-        return DeliveryService.serviceType_caldav
-
-
-    @classmethod
-    def matchCalendarUserAddress(cls, cuaddr):
-
-        # Check for local address matches first
-        if cuaddr.startswith("mailto:") and config.Scheduling[cls.serviceType()]["EmailDomain"]:
-            addr = cuaddr[7:].split("?")[0]
-            domain = config.Scheduling[cls.serviceType()]["EmailDomain"]
-            _ignore_account, addrDomain = addr.split("@")
-            if addrDomain == domain:
-                return succeed(True)
-
-        elif (cuaddr.startswith("http://") or cuaddr.startswith("https://")) and config.Scheduling[cls.serviceType()]["HTTPDomain"]:
-            splits = cuaddr.split(":")[0][2:].split("?")
-            domain = config.Scheduling[cls.serviceType()]["HTTPDomain"]
-            if splits[0].endswith(domain):
-                return succeed(True)
-
-        elif cuaddr.startswith("/"):
-            # Assume relative HTTP URL - i.e. on this server
-            return succeed(True)
-
-        # Do default match
-        return super(ScheduleViaCalDAV, cls).matchCalendarUserAddress(cuaddr)
-
-
-    @inlineCallbacks
-    def generateSchedulingResponses(self):
-
-        # Extract the ORGANIZER property and UID value from the calendar data for use later
-        organizerProp = self.scheduler.calendar.getOrganizerProperty()
-        uid = self.scheduler.calendar.resourceUID()
-
-        organizerPrincipal = None
-        if type(self.scheduler.organizer) in (LocalCalendarUser, PartitionedCalendarUser, OtherServerCalendarUser,):
-            organizerPrincipal = davxml.Principal(davxml.HRef(self.scheduler.organizer.principal.principalURL()))
-
-        for recipient in self.recipients:
-
-            #
-            # Check access controls
-            #
-            if organizerPrincipal:
-                try:
-                    yield recipient.inbox.checkPrivileges(self.scheduler.request, (caldavxml.ScheduleDeliver(),), principal=organizerPrincipal)
-                except AccessDeniedError:
-                    log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
-                    err = HTTPError(ErrorResponse(
-                        responsecode.NOT_FOUND,
-                        (caldav_namespace, "recipient-permissions"),
-                        "Access to inbox denied",
-                    ))
-                    self.responses.add(
-                        recipient.cuaddr,
-                        Failure(exc_value=err),
-                        reqstatus=iTIPRequestStatus.NO_AUTHORITY
-                    )
-
-                    # Process next recipient
-                    continue
-            else:
-                # TODO: need to figure out how best to do server-to-server authorization.
-                # First thing would be to check for DAV:unauthenticated privilege.
-                # Next would be to allow the calendar user address of the organizer/originator to be used
-                # as a principal.
-                pass
-
-            # Different behavior for free-busy vs regular invite
-            if self.freebusy:
-                # Look for special delegate extended free-busy request
-                event_details = [] if self.scheduler.calendar.getExtendedFreeBusy() else None
-
-                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid, event_details)
-            else:
-                yield self.generateResponse(recipient, self.responses)
-
-
-    @inlineCallbacks
-    def generateResponse(self, recipient, responses):
-        # Hash the iCalendar data for use as the last path element of the URI path
-        name = "%s-%s.ics" % (hashlib.md5(self.scheduler.calendar.resourceUID()).hexdigest(), str(uuid.uuid4())[:8],)
-
-        # Get a resource for the new item
-        childURL = joinURL(recipient.inboxURL, name)
-        child = (yield self.scheduler.request.locateResource(childURL))
-
-        # Do implicit scheduling message processing.
-        try:
-            processor = ImplicitProcessor()
-            _ignore_processed, autoprocessed, store_inbox, changes = (yield processor.doImplicitProcessing(
-                self.scheduler.request,
-                self.scheduler.calendar,
-                self.scheduler.originator,
-                recipient
-            ))
-        except ImplicitProcessorException, e:
-            log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
-            err = HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "recipient-permissions"),
-                "Could not store data in inbox",
-            ))
-            responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=e.msg)
-            returnValue(False)
-
-        if store_inbox:
-            # Copy calendar to inbox
-            try:
-                from twistedcaldav.method.put_common import StoreCalendarObjectResource
-                yield StoreCalendarObjectResource(
-                             request=self.scheduler.request,
-                             destination=child,
-                             destination_uri=childURL,
-                             destinationparent=recipient.inbox,
-                             destinationcal=True,
-                             calendar=self.scheduler.calendar,
-                             isiTIP=True,
-                             internal_request=True,
-                         ).run()
-            except:
-                # FIXME: Bare except
-                log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
-                err = HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (caldav_namespace, "recipient-permissions"),
-                    "Could not store data in inbox",
-                ))
-                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
-                returnValue(False)
-            else:
-                # Store CALDAV:originator property
-                child.writeDeadProperty(caldavxml.Originator(davxml.HRef(self.scheduler.originator.cuaddr)))
-
-                # Store CALDAV:recipient property
-                child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient.cuaddr)))
-
-                # Store CS:schedule-changes property if present
-                if changes:
-                    child.writeDeadProperty(changes)
-
-        responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.MESSAGE_DELIVERED)
-        if autoprocessed:
-            if not hasattr(self.scheduler.request, "extendedLogItems"):
-                self.scheduler.request.extendedLogItems = {}
-            self.scheduler.request.extendedLogItems["itip.auto"] = self.scheduler.request.extendedLogItems.get("itip.auto", 0) + 1
-        returnValue(True)
-
-
-    @inlineCallbacks
-    def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid, event_details):
-
-        # Extract the ATTENDEE property matching current recipient from the calendar data
-        cuas = recipient.principal.calendarUserAddresses()
-        attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
-
-        remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
-
-        try:
-            fbresult = (yield self.generateAttendeeFreeBusyResponse(
-                recipient,
-                organizerProp,
-                organizerPrincipal,
-                uid,
-                attendeeProp,
-                remote,
-                event_details,
-            ))
-        except:
-            log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
-            err = HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "recipient-permissions"),
-                "Could not determine free busy information",
-            ))
-            responses.add(
-                recipient.cuaddr,
-                Failure(exc_value=err),
-                reqstatus=iTIPRequestStatus.NO_AUTHORITY
-            )
-            returnValue(False)
-        else:
-            responses.add(
-                recipient.cuaddr,
-                responsecode.OK,
-                reqstatus=iTIPRequestStatus.SUCCESS,
-                calendar=fbresult
-            )
-            returnValue(True)
-
-
-    @inlineCallbacks
-    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None):
-
-        # Find the current recipients calendar-free-busy-set
-        fbset = (yield recipient.principal.calendarFreeBusyURIs(self.scheduler.request))
-
-        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-        fbinfo = ([], [], [])
-
-        # Process the availability property from the Inbox.
-        has_prop = (yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
-        if has_prop:
-            availability = (yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
-            availability = availability.calendar()
-            report_common.processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
-
-        # Check to see if the recipient is the same calendar user as the organizer.
-        # Needed for masked UID stuff.
-        if isinstance(self.scheduler.organizer, LocalCalendarUser):
-            same_calendar_user = self.scheduler.organizer.principal.principalURL() == recipient.principal.principalURL()
-        else:
-            same_calendar_user = False
-
-        # Now process free-busy set calendars
-        matchtotal = 0
-        for calendarResourceURL in fbset:
-            if not calendarResourceURL.endswith('/'):
-                calendarResourceURL += '/'
-            calendarResource = (yield self.scheduler.request.locateResource(calendarResourceURL))
-            if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
-                # We will ignore missing calendars. If the recipient has failed to
-                # properly manage the free busy set that should not prevent us from working.
-                continue
-
-            matchtotal = (yield report_common.generateFreeBusyInfo(
-                self.scheduler.request,
-                calendarResource,
-                fbinfo,
-                self.scheduler.timeRange,
-                matchtotal,
-                excludeuid=self.scheduler.excludeUID,
-                organizer=self.scheduler.organizer.cuaddr,
-                organizerPrincipal=organizerPrincipal,
-                same_calendar_user=same_calendar_user,
-                servertoserver=remote,
-                event_details=event_details,
-            ))
-
-        # Build VFREEBUSY iTIP reply for this recipient
-        fbresult = report_common.buildFreeBusyResult(
-            fbinfo,
-            self.scheduler.timeRange,
-            organizer=organizerProp,
-            attendee=attendeeProp,
-            uid=uid,
-            method="REPLY",
-            event_details=event_details,
-        )
-
-        returnValue(fbresult)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/delivery.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/caldav/delivery.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/delivery.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,319 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All 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.python.log import Logger
+from twext.web2.dav.http import ErrorResponse
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.python.failure import Failure
+from twext.web2 import responsecode
+from txdav.xml import element as davxml
+from twext.web2.dav.resource import AccessDeniedError
+from twext.web2.dav.util import joinURL
+from twext.web2.http import HTTPError
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.method import report_common
+from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser, \
+    PartitionedCalendarUser, OtherServerCalendarUser
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from twistedcaldav.scheduling.processing import ImplicitProcessor, ImplicitProcessorException
+
+import hashlib
+import uuid
+
+
+"""
+Handles the sending of scheduling messages to the server itself. This will cause
+actual processing of the delivery of the message to the recipient's inbox, via the
+L{ImplicitProcessor} class.
+"""
+
+__all__ = [
+    "ScheduleViaCalDAV",
+]
+
+log = Logger()
+
+class ScheduleViaCalDAV(DeliveryService):
+
+    def __init__(self, scheduler, recipients, responses, freebusy):
+
+        self.scheduler = scheduler
+        self.recipients = recipients
+        self.responses = responses
+        self.freebusy = freebusy
+
+
+    @classmethod
+    def serviceType(cls):
+        return DeliveryService.serviceType_caldav
+
+
+    @classmethod
+    def matchCalendarUserAddress(cls, cuaddr):
+
+        # Check for local address matches first
+        if cuaddr.startswith("mailto:") and config.Scheduling[cls.serviceType()]["EmailDomain"]:
+            addr = cuaddr[7:].split("?")[0]
+            domain = config.Scheduling[cls.serviceType()]["EmailDomain"]
+            _ignore_account, addrDomain = addr.split("@")
+            if addrDomain == domain:
+                return succeed(True)
+
+        elif (cuaddr.startswith("http://") or cuaddr.startswith("https://")) and config.Scheduling[cls.serviceType()]["HTTPDomain"]:
+            splits = cuaddr.split(":")[0][2:].split("?")
+            domain = config.Scheduling[cls.serviceType()]["HTTPDomain"]
+            if splits[0].endswith(domain):
+                return succeed(True)
+
+        elif cuaddr.startswith("/"):
+            # Assume relative HTTP URL - i.e. on this server
+            return succeed(True)
+
+        # Do default match
+        return super(ScheduleViaCalDAV, cls).matchCalendarUserAddress(cuaddr)
+
+
+    @inlineCallbacks
+    def generateSchedulingResponses(self):
+
+        # Extract the ORGANIZER property and UID value from the calendar data for use later
+        organizerProp = self.scheduler.calendar.getOrganizerProperty()
+        uid = self.scheduler.calendar.resourceUID()
+
+        organizerPrincipal = None
+        if type(self.scheduler.organizer) in (LocalCalendarUser, PartitionedCalendarUser, OtherServerCalendarUser,):
+            organizerPrincipal = davxml.Principal(davxml.HRef(self.scheduler.organizer.principal.principalURL()))
+
+        for recipient in self.recipients:
+
+            #
+            # Check access controls
+            #
+            if organizerPrincipal:
+                try:
+                    yield recipient.inbox.checkPrivileges(self.scheduler.request, (caldavxml.ScheduleDeliver(),), principal=organizerPrincipal)
+                except AccessDeniedError:
+                    log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
+                    err = HTTPError(ErrorResponse(
+                        responsecode.NOT_FOUND,
+                        (caldav_namespace, "recipient-permissions"),
+                        "Access to inbox denied",
+                    ))
+                    self.responses.add(
+                        recipient.cuaddr,
+                        Failure(exc_value=err),
+                        reqstatus=iTIPRequestStatus.NO_AUTHORITY
+                    )
+
+                    # Process next recipient
+                    continue
+            else:
+                # TODO: need to figure out how best to do server-to-server authorization.
+                # First thing would be to check for DAV:unauthenticated privilege.
+                # Next would be to allow the calendar user address of the organizer/originator to be used
+                # as a principal.
+                pass
+
+            # Different behavior for free-busy vs regular invite
+            if self.freebusy:
+                # Look for special delegate extended free-busy request
+                event_details = [] if self.scheduler.calendar.getExtendedFreeBusy() else None
+
+                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid, event_details)
+            else:
+                yield self.generateResponse(recipient, self.responses)
+
+
+    @inlineCallbacks
+    def generateResponse(self, recipient, responses):
+        # Hash the iCalendar data for use as the last path element of the URI path
+        name = "%s-%s.ics" % (hashlib.md5(self.scheduler.calendar.resourceUID()).hexdigest(), str(uuid.uuid4())[:8],)
+
+        # Get a resource for the new item
+        childURL = joinURL(recipient.inboxURL, name)
+        child = (yield self.scheduler.request.locateResource(childURL))
+
+        # Do implicit scheduling message processing.
+        try:
+            processor = ImplicitProcessor()
+            _ignore_processed, autoprocessed, store_inbox, changes = (yield processor.doImplicitProcessing(
+                self.scheduler.request,
+                self.scheduler.calendar,
+                self.scheduler.originator,
+                recipient
+            ))
+        except ImplicitProcessorException, e:
+            log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
+            err = HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (caldav_namespace, "recipient-permissions"),
+                "Could not store data in inbox",
+            ))
+            responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=e.msg)
+            returnValue(False)
+
+        if store_inbox:
+            # Copy calendar to inbox
+            try:
+                from twistedcaldav.method.put_common import StoreCalendarObjectResource
+                yield StoreCalendarObjectResource(
+                             request=self.scheduler.request,
+                             destination=child,
+                             destination_uri=childURL,
+                             destinationparent=recipient.inbox,
+                             destinationcal=True,
+                             calendar=self.scheduler.calendar,
+                             isiTIP=True,
+                             internal_request=True,
+                         ).run()
+            except:
+                # FIXME: Bare except
+                log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
+                err = HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (caldav_namespace, "recipient-permissions"),
+                    "Could not store data in inbox",
+                ))
+                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
+                returnValue(False)
+            else:
+                # Store CALDAV:originator property
+                child.writeDeadProperty(caldavxml.Originator(davxml.HRef(self.scheduler.originator.cuaddr)))
+
+                # Store CALDAV:recipient property
+                child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient.cuaddr)))
+
+                # Store CS:schedule-changes property if present
+                if changes:
+                    child.writeDeadProperty(changes)
+
+        responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.MESSAGE_DELIVERED)
+        if autoprocessed:
+            if not hasattr(self.scheduler.request, "extendedLogItems"):
+                self.scheduler.request.extendedLogItems = {}
+            self.scheduler.request.extendedLogItems["itip.auto"] = self.scheduler.request.extendedLogItems.get("itip.auto", 0) + 1
+        returnValue(True)
+
+
+    @inlineCallbacks
+    def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid, event_details):
+
+        # Extract the ATTENDEE property matching current recipient from the calendar data
+        cuas = recipient.principal.calendarUserAddresses()
+        attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
+
+        remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
+
+        try:
+            fbresult = (yield self.generateAttendeeFreeBusyResponse(
+                recipient,
+                organizerProp,
+                organizerPrincipal,
+                uid,
+                attendeeProp,
+                remote,
+                event_details,
+            ))
+        except:
+            log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
+            err = HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (caldav_namespace, "recipient-permissions"),
+                "Could not determine free busy information",
+            ))
+            responses.add(
+                recipient.cuaddr,
+                Failure(exc_value=err),
+                reqstatus=iTIPRequestStatus.NO_AUTHORITY
+            )
+            returnValue(False)
+        else:
+            responses.add(
+                recipient.cuaddr,
+                responsecode.OK,
+                reqstatus=iTIPRequestStatus.SUCCESS,
+                calendar=fbresult
+            )
+            returnValue(True)
+
+
+    @inlineCallbacks
+    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None):
+
+        # Find the current recipients calendar-free-busy-set
+        fbset = (yield recipient.principal.calendarFreeBusyURIs(self.scheduler.request))
+
+        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+        fbinfo = ([], [], [])
+
+        # Process the availability property from the Inbox.
+        has_prop = (yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
+        if has_prop:
+            availability = (yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
+            availability = availability.calendar()
+            report_common.processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
+
+        # Check to see if the recipient is the same calendar user as the organizer.
+        # Needed for masked UID stuff.
+        if isinstance(self.scheduler.organizer, LocalCalendarUser):
+            same_calendar_user = self.scheduler.organizer.principal.principalURL() == recipient.principal.principalURL()
+        else:
+            same_calendar_user = False
+
+        # Now process free-busy set calendars
+        matchtotal = 0
+        for calendarResourceURL in fbset:
+            if not calendarResourceURL.endswith('/'):
+                calendarResourceURL += '/'
+            calendarResource = (yield self.scheduler.request.locateResource(calendarResourceURL))
+            if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
+                # We will ignore missing calendars. If the recipient has failed to
+                # properly manage the free busy set that should not prevent us from working.
+                continue
+
+            matchtotal = (yield report_common.generateFreeBusyInfo(
+                self.scheduler.request,
+                calendarResource,
+                fbinfo,
+                self.scheduler.timeRange,
+                matchtotal,
+                excludeuid=self.scheduler.excludeUID,
+                organizer=self.scheduler.organizer.cuaddr,
+                organizerPrincipal=organizerPrincipal,
+                same_calendar_user=same_calendar_user,
+                servertoserver=remote,
+                event_details=event_details,
+            ))
+
+        # Build VFREEBUSY iTIP reply for this recipient
+        fbresult = report_common.buildFreeBusyResult(
+            fbinfo,
+            self.scheduler.timeRange,
+            organizer=organizerProp,
+            attendee=attendeeProp,
+            uid=uid,
+            method="REPLY",
+            event_details=event_details,
+        )
+
+        returnValue(fbresult)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/resource.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,511 +0,0 @@
-# -*- test-case-name: twistedcaldav.directory.test.test_calendar -*-
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-CalDAV scheduling resources.
-"""
-
-__all__ = [
-    "ScheduleInboxResource",
-    "ScheduleOutboxResource",
-    "deliverSchedulePrivilegeSet",
-]
-
-
-from twext.web2 import responsecode
-from txdav.xml import element as davxml
-from txdav.xml.rfc2518 import HRef
-from twext.web2.dav.http import ErrorResponse, MultiStatusResponse
-from twext.web2.dav.resource import davPrivilegeSet
-from twext.web2.dav.util import joinURL, normalizeURL
-from twext.web2.http import HTTPError
-
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
-
-from twistedcaldav import caldavxml, customxml
-from twistedcaldav.caldavxml import caldav_namespace, Opaque, \
-    CalendarFreeBusySet, ScheduleCalendarTransp
-from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.ical import allowedComponents
-from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.resource import isCalendarCollectionResource
-from twistedcaldav.scheduling.caldav.scheduler import CalDAVScheduler
-
-from txdav.base.propertystore.base import PropertyName
-
-def _schedulePrivilegeSet(deliver):
-    edited = False
-
-    top_supported_privileges = []
-
-    for supported_privilege in davPrivilegeSet.childrenOfType(davxml.SupportedPrivilege):
-        all_privilege = supported_privilege.childOfType(davxml.Privilege)
-        if isinstance(all_privilege.children[0], davxml.All):
-            all_description = supported_privilege.childOfType(davxml.Description)
-            all_supported_privileges = list(supported_privilege.childrenOfType(davxml.SupportedPrivilege))
-            all_supported_privileges.append(
-                davxml.SupportedPrivilege(
-                    davxml.Privilege(caldavxml.ScheduleDeliver() if deliver else caldavxml.ScheduleSend()),
-                    davxml.Description("schedule privileges for current principal", **{"xml:lang": "en"}),
-                ),
-            )
-            if config.Scheduling.CalDAV.OldDraftCompatibility:
-                all_supported_privileges.append(
-                    davxml.SupportedPrivilege(
-                        davxml.Privilege(caldavxml.Schedule()),
-                        davxml.Description("old-style schedule privileges for current principal", **{"xml:lang": "en"}),
-                    ),
-                )
-            top_supported_privileges.append(
-                davxml.SupportedPrivilege(all_privilege, all_description, *all_supported_privileges)
-            )
-            edited = True
-        else:
-            top_supported_privileges.append(supported_privilege)
-
-    assert edited, "Structure of davPrivilegeSet changed in a way that I don't know how to extend for schedulePrivilegeSet"
-
-    return davxml.SupportedPrivilegeSet(*top_supported_privileges)
-
-deliverSchedulePrivilegeSet = _schedulePrivilegeSet(True)
-sendSchedulePrivilegeSet = _schedulePrivilegeSet(False)
-
-class CalendarSchedulingCollectionResource (CalDAVResource):
-    """
-    CalDAV principal resource.
-
-    Extends L{DAVResource} to provide CalDAV scheduling collection
-    functionality.
-    """
-    def __init__(self, parent):
-        """
-        @param parent: the parent resource of this one.
-        """
-        assert parent is not None
-
-        super(CalendarSchedulingCollectionResource, self).__init__(principalCollections=parent.principalCollections())
-
-        self.parent = parent
-
-
-    def isCollection(self):
-        return True
-
-
-    def isCalendarCollection(self):
-        return False
-
-
-    def isPseudoCalendarCollection(self):
-        return True
-
-
-    def supportedReports(self):
-        result = super(CalDAVResource, self).supportedReports()
-        result.append(davxml.Report(caldavxml.CalendarQuery(),))
-        result.append(davxml.Report(caldavxml.CalendarMultiGet(),))
-        # free-busy report not allowed
-        if config.EnableSyncReport:
-            # Only allowed on calendar/inbox/addressbook collections
-            result.append(davxml.Report(davxml.SyncCollection(),))
-        return result
-
-
-
-class ScheduleInboxResource (CalendarSchedulingCollectionResource):
-    """
-    CalDAV schedule Inbox resource.
-
-    Extends L{DAVResource} to provide CalDAV functionality.
-    """
-
-    def liveProperties(self):
-
-        return super(ScheduleInboxResource, self).liveProperties() + (
-            caldavxml.CalendarFreeBusySet.qname(),
-            caldavxml.ScheduleDefaultCalendarURL.qname(),
-            customxml.ScheduleDefaultTasksURL.qname(),
-        )
-
-
-    def resourceType(self):
-        return davxml.ResourceType.scheduleInbox
-
-
-    @inlineCallbacks
-    def readProperty(self, property, request):
-        if type(property) is tuple:
-            qname = property
-        else:
-            qname = property.qname()
-
-        if qname == caldavxml.CalendarFreeBusySet.qname():
-            # Always return at least an empty list
-            if not self.hasDeadProperty(property):
-                top = self.parent.url()
-                values = []
-                for cal in (yield self.parent._newStoreHome.calendars()):
-                    prop = cal.properties().get(PropertyName.fromString(ScheduleCalendarTransp.sname()))
-                    if prop == ScheduleCalendarTransp(Opaque()):
-                        values.append(HRef(joinURL(top, cal.name())))
-                returnValue(CalendarFreeBusySet(*values))
-        elif qname in (caldavxml.ScheduleDefaultCalendarURL.qname(), customxml.ScheduleDefaultTasksURL.qname()):
-            result = (yield self.readDefaultCalendarProperty(request, qname))
-            returnValue(result)
-
-        result = (yield super(ScheduleInboxResource, self).readProperty(property, request))
-        returnValue(result)
-
-
-    @inlineCallbacks
-    def writeProperty(self, property, request):
-        assert isinstance(property, davxml.WebDAVElement)
-
-        # Strictly speaking CS:calendar-availability is a live property in the sense that the
-        # server enforces what can be stored, however it need not actually
-        # exist so we cannot list it in liveProperties on this resource, since its
-        # its presence there means that hasProperty will always return True for it.
-        if property.qname() == customxml.CalendarAvailability.qname():
-            if not property.valid():
-                raise HTTPError(ErrorResponse(
-                    responsecode.CONFLICT,
-                    (caldav_namespace, "valid-calendar-data"),
-                    description="Invalid property"
-                ))
-
-        elif property.qname() == caldavxml.CalendarFreeBusySet.qname():
-            # Verify that the calendars added in the PROPPATCH are valid. We do not check
-            # whether existing items in the property are still valid - only new ones.
-            property.children = [davxml.HRef(normalizeURL(str(href))) for href in property.children]
-            new_calendars = set([str(href) for href in property.children])
-            if not self.hasDeadProperty(property):
-                old_calendars = set()
-            else:
-                old_calendars = set([normalizeURL(str(href)) for href in self.readDeadProperty(property).children])
-            added_calendars = new_calendars.difference(old_calendars)
-            for href in added_calendars:
-                cal = (yield request.locateResource(str(href)))
-                if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
-                    # Validate that href's point to a valid calendar.
-                    raise HTTPError(ErrorResponse(
-                        responsecode.CONFLICT,
-                        (caldav_namespace, "valid-calendar-url"),
-                        "Invalid URI",
-                    ))
-            for href in tuple(new_calendars):
-                cal = (yield request.locateResource(str(href)))
-                if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
-                    new_calendars.remove(href)
-            property.children = [davxml.HRef(href) for href in new_calendars]
-
-        elif property.qname() in (caldavxml.ScheduleDefaultCalendarURL.qname(), customxml.ScheduleDefaultTasksURL.qname()):
-            property = (yield self.writeDefaultCalendarProperty(request, property))
-
-        yield super(ScheduleInboxResource, self).writeProperty(property, request)
-
-
-    def processFreeBusyCalendar(self, uri, addit):
-        uri = normalizeURL(uri)
-
-        if not self.hasDeadProperty(caldavxml.CalendarFreeBusySet.qname()):
-            fbset = set()
-        else:
-            fbset = set([normalizeURL(str(href)) for href in self.readDeadProperty(caldavxml.CalendarFreeBusySet.qname()).children])
-        if addit:
-            if uri not in fbset:
-                fbset.add(uri)
-                self.writeDeadProperty(caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in fbset]))
-        else:
-            if uri in fbset:
-                fbset.remove(uri)
-                self.writeDeadProperty(caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in fbset]))
-
-
-    @inlineCallbacks
-    def readDefaultCalendarProperty(self, request, qname):
-        """
-        Read either the default VEVENT or VTODO calendar property. Try to pick one if not present.
-        """
-
-        tasks = qname == customxml.ScheduleDefaultTasksURL.qname()
-
-        # Must have a valid default
-        try:
-            defaultCalendarProperty = self.readDeadProperty(qname)
-        except HTTPError:
-            defaultCalendarProperty = None
-        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():
-                returnValue(defaultCalendarProperty)
-
-        # Default is not valid - we have to try to pick one
-        defaultCalendarProperty = (yield self.pickNewDefaultCalendar(request, tasks=tasks))
-        returnValue(defaultCalendarProperty)
-
-
-    @inlineCallbacks
-    def writeDefaultCalendarProperty(self, request, property):
-        """
-        Write either the default VEVENT or VTODO calendar property, validating and canonicalizing the value
-        """
-        tasks = property.qname() == customxml.ScheduleDefaultTasksURL
-        componentType = "VTODO" if tasks else "VEVENT"
-        prop_to_set = customxml.ScheduleDefaultTasksURL if tasks else caldavxml.ScheduleDefaultCalendarURL
-        error_element = (calendarserver_namespace, "valid-schedule-default-tasks-URL") if tasks else (caldav_namespace, "valid-schedule-default-calendar-URL")
-
-        # Verify that the calendar added in the PROPPATCH is valid.
-        property.children = [davxml.HRef(normalizeURL(str(href))) for href in property.children]
-        new_calendar = [str(href) for href in property.children]
-        cal = None
-        if len(new_calendar) == 1:
-            calURI = str(new_calendar[0])
-            cal = (yield request.locateResource(str(new_calendar[0])))
-
-        # 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):
-            # Validate that href's point to a valid calendar.
-            raise HTTPError(ErrorResponse(
-                responsecode.CONFLICT,
-                error_element,
-                "Invalid URI",
-            ))
-        else:
-            # Canonicalize the URL to __uids__ form
-            calURI = (yield cal.canonicalURL(request))
-            property = prop_to_set(davxml.HRef(calURI))
-            returnValue(property)
-
-
-    @inlineCallbacks
-    def pickNewDefaultCalendar(self, request, tasks=False):
-        """
-        First see if default provisioned calendar exists in the calendar home and pick that. Otherwise
-        pick another from the calendar home.
-        """
-
-        componentType = "VTODO" if tasks else "VEVENT"
-        test_name = "tasks" if tasks else "calendar"
-        prop_to_set = customxml.ScheduleDefaultTasksURL if tasks else caldavxml.ScheduleDefaultCalendarURL
-
-        calendarHomeURL = self.parent.url()
-        defaultCalendarURL = joinURL(calendarHomeURL, test_name)
-        defaultCalendar = (yield request.locateResource(defaultCalendarURL))
-        if defaultCalendar is None or not defaultCalendar.exists():
-            # Really, the dead property shouldn't be necessary, and this should
-            # be entirely computed by a back-end method like 'defaultCalendar()'
-
-            @inlineCallbacks
-            def _findDefault():
-                for calendarName in (yield self.parent._newStoreHome.listCalendars()):  # These are only unshared children
-                    if calendarName == "inbox":
-                        continue
-                    calendar = (yield self.parent._newStoreHome.calendarWithName(calendarName))
-                    if not calendar.owned():
-                        continue
-                    if not calendar.isSupportedComponent(componentType):
-                        continue
-                    break
-                else:
-                    calendarName = None
-                returnValue(calendarName)
-
-            foundName = yield _findDefault()
-            if foundName is None:
-                # Create a default and try and get its name again
-                yield self.parent._newStoreHome.ensureDefaultCalendarsExist()
-                foundName = yield _findDefault()
-                if foundName is None:
-                    # Failed to even create a default - bad news...
-                    raise RuntimeError("No valid calendars to use as a default %s calendar." % (componentType,))
-
-            defaultCalendarURL = joinURL(calendarHomeURL, foundName)
-
-        prop = prop_to_set(davxml.HRef(defaultCalendarURL))
-        self.writeDeadProperty(prop)
-        returnValue(prop)
-
-
-    @inlineCallbacks
-    def defaultCalendar(self, request, componentType):
-        """
-        Find the default calendar for the supplied iCalendar component type. If one does
-        not exist, automatically provision it.
-        """
-
-        # Check any default calendar property first - this will create if none exists
-        default = (yield self.readProperty(caldavxml.ScheduleDefaultCalendarURL.qname(), request))
-        if len(default.children) == 1:
-            defaultURL = str(default.children[0])
-            default = (yield request.locateResource(defaultURL))
-        else:
-            default = None
-
-        # Check that default handles the component type
-        if default is not None:
-            if not default.isSupportedComponent(componentType):
-                default = None
-
-        # Must have a default - provision one if not
-        if default is None:
-
-            # Try to find a calendar supporting the required component type. If there are multiple, pick
-            # the one with the oldest created timestamp as that will likely be the initial provision.
-            for calendarName in (yield self.parent._newStoreHome.listCalendars()):  # These are only unshared children
-                if calendarName == "inbox":
-                    continue
-                calendar = (yield self.parent._newStoreHome.calendarWithName(calendarName))
-                if not calendar.isSupportedComponent(componentType):
-                    continue
-                if default is None or calendar.created() < default.created():
-                    default = calendar
-
-            # If none can be found, provision one
-            if default is None:
-                new_name = "%ss" % (componentType.lower()[1:],)
-                default = yield self.parent._newStoreHome.createCalendarWithName(new_name)
-                yield default.setSupportedComponents(componentType.upper())
-
-            # Need L{DAVResource} object to return not new store object
-            default = (yield request.locateResource(joinURL(self.parent.url(), default.name())))
-
-        returnValue(default)
-
-
-    @inlineCallbacks
-    def isDefaultCalendar(self, request, calendar):
-        """
-        Is the supplied calendar one of the possible default calendars.
-        """
-        assert calendar.isCalendarCollection()
-
-        # Not allowed to delete the default calendar
-        for default_prop in (caldavxml.ScheduleDefaultCalendarURL, customxml.ScheduleDefaultTasksURL,):
-            default = (yield self.readProperty(default_prop.qname(), request))
-            if default and len(default.children) == 1:
-                defaultURL = normalizeURL(str(default.children[0]))
-                myURL = (yield calendar.canonicalURL(request))
-                if defaultURL == myURL:
-                    returnValue(default_prop)
-
-        returnValue(None)
-
-
-    ##
-    # ACL
-    ##
-
-    def supportedPrivileges(self, request):
-        return succeed(deliverSchedulePrivilegeSet)
-
-
-    def defaultAccessControlList(self):
-
-        privs = (
-            davxml.Privilege(caldavxml.ScheduleDeliver()),
-        )
-        if config.Scheduling.CalDAV.OldDraftCompatibility:
-            privs += (davxml.Privilege(caldavxml.Schedule()),)
-
-        return davxml.ACL(
-            # CalDAV:schedule-deliver for any authenticated user
-            davxml.ACE(
-                davxml.Principal(davxml.Authenticated()),
-                davxml.Grant(*privs),
-            ),
-        )
-
-
-
-class ScheduleOutboxResource (CalendarSchedulingCollectionResource):
-    """
-    CalDAV schedule Outbox resource.
-
-    Extends L{DAVResource} to provide CalDAV functionality.
-    """
-
-    def resourceType(self):
-        return davxml.ResourceType.scheduleOutbox
-
-
-    def getSupportedComponentSet(self):
-        return caldavxml.SupportedCalendarComponentSet(
-            *[caldavxml.CalendarComponent(name=item) for item in allowedComponents]
-        )
-
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The CalDAV POST method.
-
-        This uses a generator function yielding either L{waitForDeferred} objects or L{Response} objects.
-        This allows for code that follows a 'linear' execution pattern rather than having to use nested
-        L{Deferred} callbacks. The logic is easier to follow this way plus we don't run into deep nesting
-        issues which the other approach would have with large numbers of recipients.
-        """
-        # Check authentication and access controls
-        yield self.authorize(request, (caldavxml.ScheduleSend(),))
-
-        # This is a local CALDAV scheduling operation.
-        scheduler = CalDAVScheduler(request, self)
-
-        # Do the POST processing treating
-        result = (yield scheduler.doSchedulingViaPOST(self._associatedTransaction))
-        returnValue(result.response())
-
-
-    ##
-    # ACL
-    ##
-
-    def supportedPrivileges(self, request):
-        return succeed(sendSchedulePrivilegeSet)
-
-
-    def defaultAccessControlList(self):
-        if config.EnableProxyPrincipals:
-            myPrincipal = self.parent.principalForRecord()
-
-            privs = (
-                davxml.Privilege(caldavxml.ScheduleSend()),
-            )
-            if config.Scheduling.CalDAV.OldDraftCompatibility:
-                privs += (davxml.Privilege(caldavxml.Schedule()),)
-
-            return davxml.ACL(
-                # CalDAV:schedule for associated write proxies
-                davxml.ACE(
-                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-write"))),
-                    davxml.Grant(*privs),
-                    davxml.Protected(),
-                ),
-            )
-        else:
-            return super(ScheduleOutboxResource, self).defaultAccessControlList()
-
-
-    def report_urn_ietf_params_xml_ns_caldav_calendar_query(self, request, calendar_query):
-        return succeed(MultiStatusResponse(()))
-
-
-    def report_urn_ietf_params_xml_ns_caldav_calendar_multiget(self, request, multiget):
-        responses = [davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)) for href in multiget.resources]
-        return succeed(MultiStatusResponse((responses)))

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/resource.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/caldav/resource.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/resource.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,511 @@
+# -*- test-case-name: twistedcaldav.directory.test.test_calendar -*-
+##
+# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+CalDAV scheduling resources.
+"""
+
+__all__ = [
+    "ScheduleInboxResource",
+    "ScheduleOutboxResource",
+    "deliverSchedulePrivilegeSet",
+]
+
+
+from twext.web2 import responsecode
+from txdav.xml import element as davxml
+from txdav.xml.rfc2518 import HRef
+from twext.web2.dav.http import ErrorResponse, MultiStatusResponse
+from twext.web2.dav.resource import davPrivilegeSet
+from twext.web2.dav.util import joinURL, normalizeURL
+from twext.web2.http import HTTPError
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
+from twistedcaldav import caldavxml, customxml
+from twistedcaldav.caldavxml import caldav_namespace, Opaque, \
+    CalendarFreeBusySet, ScheduleCalendarTransp
+from twistedcaldav.config import config
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.ical import allowedComponents
+from twistedcaldav.resource import CalDAVResource
+from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.scheduling.caldav.scheduler import CalDAVScheduler
+
+from txdav.base.propertystore.base import PropertyName
+
+def _schedulePrivilegeSet(deliver):
+    edited = False
+
+    top_supported_privileges = []
+
+    for supported_privilege in davPrivilegeSet.childrenOfType(davxml.SupportedPrivilege):
+        all_privilege = supported_privilege.childOfType(davxml.Privilege)
+        if isinstance(all_privilege.children[0], davxml.All):
+            all_description = supported_privilege.childOfType(davxml.Description)
+            all_supported_privileges = list(supported_privilege.childrenOfType(davxml.SupportedPrivilege))
+            all_supported_privileges.append(
+                davxml.SupportedPrivilege(
+                    davxml.Privilege(caldavxml.ScheduleDeliver() if deliver else caldavxml.ScheduleSend()),
+                    davxml.Description("schedule privileges for current principal", **{"xml:lang": "en"}),
+                ),
+            )
+            if config.Scheduling.CalDAV.OldDraftCompatibility:
+                all_supported_privileges.append(
+                    davxml.SupportedPrivilege(
+                        davxml.Privilege(caldavxml.Schedule()),
+                        davxml.Description("old-style schedule privileges for current principal", **{"xml:lang": "en"}),
+                    ),
+                )
+            top_supported_privileges.append(
+                davxml.SupportedPrivilege(all_privilege, all_description, *all_supported_privileges)
+            )
+            edited = True
+        else:
+            top_supported_privileges.append(supported_privilege)
+
+    assert edited, "Structure of davPrivilegeSet changed in a way that I don't know how to extend for schedulePrivilegeSet"
+
+    return davxml.SupportedPrivilegeSet(*top_supported_privileges)
+
+deliverSchedulePrivilegeSet = _schedulePrivilegeSet(True)
+sendSchedulePrivilegeSet = _schedulePrivilegeSet(False)
+
+class CalendarSchedulingCollectionResource (CalDAVResource):
+    """
+    CalDAV principal resource.
+
+    Extends L{DAVResource} to provide CalDAV scheduling collection
+    functionality.
+    """
+    def __init__(self, parent):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
+
+        super(CalendarSchedulingCollectionResource, self).__init__(principalCollections=parent.principalCollections())
+
+        self.parent = parent
+
+
+    def isCollection(self):
+        return True
+
+
+    def isCalendarCollection(self):
+        return False
+
+
+    def isPseudoCalendarCollection(self):
+        return True
+
+
+    def supportedReports(self):
+        result = super(CalDAVResource, self).supportedReports()
+        result.append(davxml.Report(caldavxml.CalendarQuery(),))
+        result.append(davxml.Report(caldavxml.CalendarMultiGet(),))
+        # free-busy report not allowed
+        if config.EnableSyncReport:
+            # Only allowed on calendar/inbox/addressbook collections
+            result.append(davxml.Report(davxml.SyncCollection(),))
+        return result
+
+
+
+class ScheduleInboxResource (CalendarSchedulingCollectionResource):
+    """
+    CalDAV schedule Inbox resource.
+
+    Extends L{DAVResource} to provide CalDAV functionality.
+    """
+
+    def liveProperties(self):
+
+        return super(ScheduleInboxResource, self).liveProperties() + (
+            caldavxml.CalendarFreeBusySet.qname(),
+            caldavxml.ScheduleDefaultCalendarURL.qname(),
+            customxml.ScheduleDefaultTasksURL.qname(),
+        )
+
+
+    def resourceType(self):
+        return davxml.ResourceType.scheduleInbox
+
+
+    @inlineCallbacks
+    def readProperty(self, property, request):
+        if type(property) is tuple:
+            qname = property
+        else:
+            qname = property.qname()
+
+        if qname == caldavxml.CalendarFreeBusySet.qname():
+            # Always return at least an empty list
+            if not self.hasDeadProperty(property):
+                top = self.parent.url()
+                values = []
+                for cal in (yield self.parent._newStoreHome.calendars()):
+                    prop = cal.properties().get(PropertyName.fromString(ScheduleCalendarTransp.sname()))
+                    if prop == ScheduleCalendarTransp(Opaque()):
+                        values.append(HRef(joinURL(top, cal.name())))
+                returnValue(CalendarFreeBusySet(*values))
+        elif qname in (caldavxml.ScheduleDefaultCalendarURL.qname(), customxml.ScheduleDefaultTasksURL.qname()):
+            result = (yield self.readDefaultCalendarProperty(request, qname))
+            returnValue(result)
+
+        result = (yield super(ScheduleInboxResource, self).readProperty(property, request))
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def writeProperty(self, property, request):
+        assert isinstance(property, davxml.WebDAVElement)
+
+        # Strictly speaking CS:calendar-availability is a live property in the sense that the
+        # server enforces what can be stored, however it need not actually
+        # exist so we cannot list it in liveProperties on this resource, since its
+        # its presence there means that hasProperty will always return True for it.
+        if property.qname() == customxml.CalendarAvailability.qname():
+            if not property.valid():
+                raise HTTPError(ErrorResponse(
+                    responsecode.CONFLICT,
+                    (caldav_namespace, "valid-calendar-data"),
+                    description="Invalid property"
+                ))
+
+        elif property.qname() == caldavxml.CalendarFreeBusySet.qname():
+            # Verify that the calendars added in the PROPPATCH are valid. We do not check
+            # whether existing items in the property are still valid - only new ones.
+            property.children = [davxml.HRef(normalizeURL(str(href))) for href in property.children]
+            new_calendars = set([str(href) for href in property.children])
+            if not self.hasDeadProperty(property):
+                old_calendars = set()
+            else:
+                old_calendars = set([normalizeURL(str(href)) for href in self.readDeadProperty(property).children])
+            added_calendars = new_calendars.difference(old_calendars)
+            for href in added_calendars:
+                cal = (yield request.locateResource(str(href)))
+                if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
+                    # Validate that href's point to a valid calendar.
+                    raise HTTPError(ErrorResponse(
+                        responsecode.CONFLICT,
+                        (caldav_namespace, "valid-calendar-url"),
+                        "Invalid URI",
+                    ))
+            for href in tuple(new_calendars):
+                cal = (yield request.locateResource(str(href)))
+                if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
+                    new_calendars.remove(href)
+            property.children = [davxml.HRef(href) for href in new_calendars]
+
+        elif property.qname() in (caldavxml.ScheduleDefaultCalendarURL.qname(), customxml.ScheduleDefaultTasksURL.qname()):
+            property = (yield self.writeDefaultCalendarProperty(request, property))
+
+        yield super(ScheduleInboxResource, self).writeProperty(property, request)
+
+
+    def processFreeBusyCalendar(self, uri, addit):
+        uri = normalizeURL(uri)
+
+        if not self.hasDeadProperty(caldavxml.CalendarFreeBusySet.qname()):
+            fbset = set()
+        else:
+            fbset = set([normalizeURL(str(href)) for href in self.readDeadProperty(caldavxml.CalendarFreeBusySet.qname()).children])
+        if addit:
+            if uri not in fbset:
+                fbset.add(uri)
+                self.writeDeadProperty(caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in fbset]))
+        else:
+            if uri in fbset:
+                fbset.remove(uri)
+                self.writeDeadProperty(caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in fbset]))
+
+
+    @inlineCallbacks
+    def readDefaultCalendarProperty(self, request, qname):
+        """
+        Read either the default VEVENT or VTODO calendar property. Try to pick one if not present.
+        """
+
+        tasks = qname == customxml.ScheduleDefaultTasksURL.qname()
+
+        # Must have a valid default
+        try:
+            defaultCalendarProperty = self.readDeadProperty(qname)
+        except HTTPError:
+            defaultCalendarProperty = None
+        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():
+                returnValue(defaultCalendarProperty)
+
+        # Default is not valid - we have to try to pick one
+        defaultCalendarProperty = (yield self.pickNewDefaultCalendar(request, tasks=tasks))
+        returnValue(defaultCalendarProperty)
+
+
+    @inlineCallbacks
+    def writeDefaultCalendarProperty(self, request, property):
+        """
+        Write either the default VEVENT or VTODO calendar property, validating and canonicalizing the value
+        """
+        tasks = property.qname() == customxml.ScheduleDefaultTasksURL
+        componentType = "VTODO" if tasks else "VEVENT"
+        prop_to_set = customxml.ScheduleDefaultTasksURL if tasks else caldavxml.ScheduleDefaultCalendarURL
+        error_element = (calendarserver_namespace, "valid-schedule-default-tasks-URL") if tasks else (caldav_namespace, "valid-schedule-default-calendar-URL")
+
+        # Verify that the calendar added in the PROPPATCH is valid.
+        property.children = [davxml.HRef(normalizeURL(str(href))) for href in property.children]
+        new_calendar = [str(href) for href in property.children]
+        cal = None
+        if len(new_calendar) == 1:
+            calURI = str(new_calendar[0])
+            cal = (yield request.locateResource(str(new_calendar[0])))
+
+        # 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):
+            # Validate that href's point to a valid calendar.
+            raise HTTPError(ErrorResponse(
+                responsecode.CONFLICT,
+                error_element,
+                "Invalid URI",
+            ))
+        else:
+            # Canonicalize the URL to __uids__ form
+            calURI = (yield cal.canonicalURL(request))
+            property = prop_to_set(davxml.HRef(calURI))
+            returnValue(property)
+
+
+    @inlineCallbacks
+    def pickNewDefaultCalendar(self, request, tasks=False):
+        """
+        First see if default provisioned calendar exists in the calendar home and pick that. Otherwise
+        pick another from the calendar home.
+        """
+
+        componentType = "VTODO" if tasks else "VEVENT"
+        test_name = "tasks" if tasks else "calendar"
+        prop_to_set = customxml.ScheduleDefaultTasksURL if tasks else caldavxml.ScheduleDefaultCalendarURL
+
+        calendarHomeURL = self.parent.url()
+        defaultCalendarURL = joinURL(calendarHomeURL, test_name)
+        defaultCalendar = (yield request.locateResource(defaultCalendarURL))
+        if defaultCalendar is None or not defaultCalendar.exists():
+            # Really, the dead property shouldn't be necessary, and this should
+            # be entirely computed by a back-end method like 'defaultCalendar()'
+
+            @inlineCallbacks
+            def _findDefault():
+                for calendarName in (yield self.parent._newStoreHome.listCalendars()):  # These are only unshared children
+                    if calendarName == "inbox":
+                        continue
+                    calendar = (yield self.parent._newStoreHome.calendarWithName(calendarName))
+                    if not calendar.owned():
+                        continue
+                    if not calendar.isSupportedComponent(componentType):
+                        continue
+                    break
+                else:
+                    calendarName = None
+                returnValue(calendarName)
+
+            foundName = yield _findDefault()
+            if foundName is None:
+                # Create a default and try and get its name again
+                yield self.parent._newStoreHome.ensureDefaultCalendarsExist()
+                foundName = yield _findDefault()
+                if foundName is None:
+                    # Failed to even create a default - bad news...
+                    raise RuntimeError("No valid calendars to use as a default %s calendar." % (componentType,))
+
+            defaultCalendarURL = joinURL(calendarHomeURL, foundName)
+
+        prop = prop_to_set(davxml.HRef(defaultCalendarURL))
+        self.writeDeadProperty(prop)
+        returnValue(prop)
+
+
+    @inlineCallbacks
+    def defaultCalendar(self, request, componentType):
+        """
+        Find the default calendar for the supplied iCalendar component type. If one does
+        not exist, automatically provision it.
+        """
+
+        # Check any default calendar property first - this will create if none exists
+        default = (yield self.readProperty(caldavxml.ScheduleDefaultCalendarURL.qname(), request))
+        if len(default.children) == 1:
+            defaultURL = str(default.children[0])
+            default = (yield request.locateResource(defaultURL))
+        else:
+            default = None
+
+        # Check that default handles the component type
+        if default is not None:
+            if not default.isSupportedComponent(componentType):
+                default = None
+
+        # Must have a default - provision one if not
+        if default is None:
+
+            # Try to find a calendar supporting the required component type. If there are multiple, pick
+            # the one with the oldest created timestamp as that will likely be the initial provision.
+            for calendarName in (yield self.parent._newStoreHome.listCalendars()):  # These are only unshared children
+                if calendarName == "inbox":
+                    continue
+                calendar = (yield self.parent._newStoreHome.calendarWithName(calendarName))
+                if not calendar.isSupportedComponent(componentType):
+                    continue
+                if default is None or calendar.created() < default.created():
+                    default = calendar
+
+            # If none can be found, provision one
+            if default is None:
+                new_name = "%ss" % (componentType.lower()[1:],)
+                default = yield self.parent._newStoreHome.createCalendarWithName(new_name)
+                yield default.setSupportedComponents(componentType.upper())
+
+            # Need L{DAVResource} object to return not new store object
+            default = (yield request.locateResource(joinURL(self.parent.url(), default.name())))
+
+        returnValue(default)
+
+
+    @inlineCallbacks
+    def isDefaultCalendar(self, request, calendar):
+        """
+        Is the supplied calendar one of the possible default calendars.
+        """
+        assert calendar.isCalendarCollection()
+
+        # Not allowed to delete the default calendar
+        for default_prop in (caldavxml.ScheduleDefaultCalendarURL, customxml.ScheduleDefaultTasksURL,):
+            default = (yield self.readProperty(default_prop.qname(), request))
+            if default and len(default.children) == 1:
+                defaultURL = normalizeURL(str(default.children[0]))
+                myURL = (yield calendar.canonicalURL(request))
+                if defaultURL == myURL:
+                    returnValue(default_prop)
+
+        returnValue(None)
+
+
+    ##
+    # ACL
+    ##
+
+    def supportedPrivileges(self, request):
+        return succeed(deliverSchedulePrivilegeSet)
+
+
+    def defaultAccessControlList(self):
+
+        privs = (
+            davxml.Privilege(caldavxml.ScheduleDeliver()),
+        )
+        if config.Scheduling.CalDAV.OldDraftCompatibility:
+            privs += (davxml.Privilege(caldavxml.Schedule()),)
+
+        return davxml.ACL(
+            # CalDAV:schedule-deliver for any authenticated user
+            davxml.ACE(
+                davxml.Principal(davxml.Authenticated()),
+                davxml.Grant(*privs),
+            ),
+        )
+
+
+
+class ScheduleOutboxResource (CalendarSchedulingCollectionResource):
+    """
+    CalDAV schedule Outbox resource.
+
+    Extends L{DAVResource} to provide CalDAV functionality.
+    """
+
+    def resourceType(self):
+        return davxml.ResourceType.scheduleOutbox
+
+
+    def getSupportedComponentSet(self):
+        return caldavxml.SupportedCalendarComponentSet(
+            *[caldavxml.CalendarComponent(name=item) for item in allowedComponents]
+        )
+
+
+    @inlineCallbacks
+    def http_POST(self, request):
+        """
+        The CalDAV POST method.
+
+        This uses a generator function yielding either L{waitForDeferred} objects or L{Response} objects.
+        This allows for code that follows a 'linear' execution pattern rather than having to use nested
+        L{Deferred} callbacks. The logic is easier to follow this way plus we don't run into deep nesting
+        issues which the other approach would have with large numbers of recipients.
+        """
+        # Check authentication and access controls
+        yield self.authorize(request, (caldavxml.ScheduleSend(),))
+
+        # This is a local CALDAV scheduling operation.
+        scheduler = CalDAVScheduler(request, self)
+
+        # Do the POST processing treating
+        result = (yield scheduler.doSchedulingViaPOST(self._associatedTransaction))
+        returnValue(result.response())
+
+
+    ##
+    # ACL
+    ##
+
+    def supportedPrivileges(self, request):
+        return succeed(sendSchedulePrivilegeSet)
+
+
+    def defaultAccessControlList(self):
+        if config.EnableProxyPrincipals:
+            myPrincipal = self.parent.principalForRecord()
+
+            privs = (
+                davxml.Privilege(caldavxml.ScheduleSend()),
+            )
+            if config.Scheduling.CalDAV.OldDraftCompatibility:
+                privs += (davxml.Privilege(caldavxml.Schedule()),)
+
+            return davxml.ACL(
+                # CalDAV:schedule for associated write proxies
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-write"))),
+                    davxml.Grant(*privs),
+                    davxml.Protected(),
+                ),
+            )
+        else:
+            return super(ScheduleOutboxResource, self).defaultAccessControlList()
+
+
+    def report_urn_ietf_params_xml_ns_caldav_calendar_query(self, request, calendar_query):
+        return succeed(MultiStatusResponse(()))
+
+
+    def report_urn_ietf_params_xml_ns_caldav_calendar_multiget(self, request, multiget):
+        responses = [davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)) for href in multiget.resources]
+        return succeed(MultiStatusResponse((responses)))

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/scheduler.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/scheduler.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/scheduler.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,286 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All 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.python.log import Logger
-from twext.web2 import responsecode
-from twext.web2.dav.http import ErrorResponse
-from twext.web2.http import HTTPError, StatusResponse
-
-from twisted.internet.defer import inlineCallbacks
-
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.scheduling import addressmapping
-from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, \
-    InvalidCalendarUser, calendarUserFromPrincipal, RemoteCalendarUser
-from twistedcaldav.scheduling.scheduler import Scheduler, ScheduleResponseQueue
-
-from txdav.xml import element as davxml
-
-
-"""
-L{CalDAVScheduler} - handles deliveries for scheduling messages within the CalDAV server.
-"""
-
-__all__ = [
-    "CalDAVScheduler",
-]
-
-
-log = Logger()
-
-class CalDAVScheduler(Scheduler):
-
-    scheduleResponse = ScheduleResponseQueue
-
-    errorResponse = ErrorResponse
-
-    errorElements = {
-        "originator-missing": (caldav_namespace, "originator-specified"),
-        "originator-invalid": (caldav_namespace, "originator-allowed"),
-        "originator-denied": (caldav_namespace, "originator-allowed"),
-        "recipient-missing": (caldav_namespace, "recipient-specified"),
-        "recipient-invalid": (caldav_namespace, "recipient-exists"),
-        "organizer-denied": (caldav_namespace, "organizer-allowed"),
-        "attendee-denied": (caldav_namespace, "attendee-allowed"),
-        "invalid-calendar-data-type": (caldav_namespace, "supported-calendar-data"),
-        "invalid-calendar-data": (caldav_namespace, "valid-calendar-data"),
-        "invalid-scheduling-message": (caldav_namespace, "valid-calendar-data"),
-        "max-recipients": (caldav_namespace, "recipient-limit"),
-    }
-
-    def __init__(self, request, resource):
-        super(CalDAVScheduler, self).__init__(request, resource)
-        self.doingPOST = False
-
-
-    def doSchedulingViaPOST(self, transaction):
-        """
-        The Scheduling POST operation on an Outbox.
-        """
-        self.doingPOST = True
-        return super(CalDAVScheduler, self).doSchedulingViaPOST(transaction)
-
-
-    def checkAuthorization(self):
-        # Must have an authenticated user
-        if not self.internal_request and self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
-            log.err("Unauthenticated originators not allowed: %s" % (self.originator,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["originator-denied"],
-                "Invalid originator",
-            ))
-
-
-    def checkOriginator(self):
-        """
-        Check the validity of the Originator header. Extract the corresponding principal.
-        """
-
-        # Verify that Originator is a valid calendar user
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        if originatorPrincipal is None:
-            # Local requests MUST have a principal.
-            log.err("Could not find principal for originator: %s" % (self.originator,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["originator-denied"],
-                "No principal for originator",
-            ))
-        else:
-            # Must have a valid Inbox.
-            inboxURL = originatorPrincipal.scheduleInboxURL()
-            if inboxURL is None:
-                log.err("Could not find inbox for originator: %s" % (self.originator,))
-                raise HTTPError(self.errorResponse(
-                    responsecode.FORBIDDEN,
-                    self.errorElements["originator-denied"],
-                    "Originator cannot be scheduled",
-                ))
-
-            self.originator = LocalCalendarUser(self.originator, originatorPrincipal)
-
-
-    @inlineCallbacks
-    def checkRecipients(self):
-        """
-        Check the validity of the Recipient header values. Map these into local or
-        remote CalendarUsers.
-        """
-
-        results = []
-        for recipient in self.recipients:
-            # Get the principal resource for this recipient
-            principal = self.resource.principalForCalendarUserAddress(recipient)
-
-            # If no principal we may have a remote recipient but we should check whether
-            # the address is one that ought to be on our server and treat that as a missing
-            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
-            if principal is None:
-                address = (yield addressmapping.mapper.getCalendarUser(recipient, principal))
-                if isinstance(address, InvalidCalendarUser):
-                    log.err("Unknown calendar user address: %s" % (recipient,))
-                results.append(address)
-            else:
-                # Map recipient to their inbox
-                inboxURL = principal.scheduleInboxURL()
-                inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
-
-                if inbox:
-                    results.append(calendarUserFromPrincipal(recipient, principal, inbox, inboxURL))
-                else:
-                    log.err("No schedule inbox for principal: %s" % (principal,))
-                    results.append(InvalidCalendarUser(recipient))
-
-        self.recipients = results
-
-
-    @inlineCallbacks
-    def checkOrganizer(self):
-        """
-        Check the validity of the ORGANIZER value. ORGANIZER must be local.
-        """
-
-        # Verify that the ORGANIZER's cu address maps to a valid user
-        organizer = self.calendar.getOrganizer()
-        if organizer:
-            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
-            if organizerPrincipal:
-                outboxURL = organizerPrincipal.scheduleOutboxURL()
-                if outboxURL:
-
-                    # Only do this check for a freebusy request. A check for an invite needs
-                    # to be handled later when we know whether a new invite is being added
-                    # (which we reject) vs an update to an existing one (which we allow).
-                    if self.checkForFreeBusy() and not organizerPrincipal.enabledAsOrganizer():
-                        log.err("ORGANIZER not allowed to be an Organizer: %s" % (self.calendar,))
-                        raise HTTPError(self.errorResponse(
-                            responsecode.FORBIDDEN,
-                            self.errorElements["organizer-denied"],
-                            "Organizer cannot schedule",
-                        ))
-
-                    self.organizer = LocalCalendarUser(organizer, organizerPrincipal)
-                else:
-                    log.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
-                    raise HTTPError(self.errorResponse(
-                        responsecode.FORBIDDEN,
-                        self.errorElements["organizer-denied"],
-                        "Organizer cannot schedule",
-                    ))
-            else:
-                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
-                if localUser:
-                    log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendar,))
-                    raise HTTPError(self.errorResponse(
-                        responsecode.FORBIDDEN,
-                        self.errorElements["organizer-denied"],
-                        "No principal for organizer",
-                    ))
-                else:
-                    self.organizer = RemoteCalendarUser(organizer)
-        else:
-            log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["invalid-scheduling-message"],
-                "Missing organizer",
-            ))
-
-
-    def checkOrganizerAsOriginator(self):
-
-        # Make sure that the ORGANIZER is local
-        if not isinstance(self.organizer, LocalCalendarUser):
-            log.err("ORGANIZER is not local to server in calendar data: %s" % (self.calendar,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["organizer-denied"],
-                "Organizer is not local to server",
-            ))
-
-        # Make sure that the ORGANIZER's Outbox is the request URI
-        if self.doingPOST and self.organizer.principal.scheduleOutboxURL() != self.request.uri:
-            log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["organizer-denied"],
-                "Outbox does not belong to organizer",
-            ))
-
-
-    def checkAttendeeAsOriginator(self):
-        """
-        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
-        Only local attendees are allowed for message originating from this server.
-        """
-
-        # Attendee's Outbox MUST be the request URI
-        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
-        if attendeePrincipal:
-            if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != self.request.uri:
-                log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendar,))
-                raise HTTPError(self.errorResponse(
-                    responsecode.FORBIDDEN,
-                    self.errorElements["attendee-denied"],
-                    "Outbox does not belong to attendee",
-                ))
-        else:
-            log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["attendee-denied"],
-                "No principal for attendee",
-            ))
-
-
-    def securityChecks(self):
-        """
-        Check that the originator has the appropriate rights to send this type of iTIP message.
-        """
-
-        # Prevent spoofing of ORGANIZER with specific METHODs when local
-        if self.isiTIPRequest:
-            self.checkOrganizerAsOriginator()
-
-        # Prevent spoofing when doing reply-like METHODs
-        else:
-            self.checkAttendeeAsOriginator()
-
-
-    def finalChecks(self):
-        """
-        Final checks before doing the actual scheduling.
-        """
-
-        # With implicit scheduling only certain types of iTIP operations are allowed for POST.
-
-        if self.doingPOST:
-            # Freebusy requests always processed
-            if self.checkForFreeBusy():
-                return
-
-            # COUNTER and DECLINE-COUNTER allowed
-            if self.calendar.propertyValue("METHOD") in ("COUNTER", "DECLINECOUNTER"):
-                return
-
-            # Anything else is not allowed. However, for compatibility we will optionally
-            # return a success response for all attendees.
-            if config.Scheduling.CalDAV.OldDraftCompatibility:
-                self.fakeTheResult = True
-            else:
-                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid iTIP message for implicit scheduling"))

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/scheduler.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/caldav/scheduler.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/scheduler.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/scheduler.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,286 @@
+##
+# Copyright (c) 2012 Apple Inc. All 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.python.log import Logger
+from twext.web2 import responsecode
+from twext.web2.dav.http import ErrorResponse
+from twext.web2.http import HTTPError, StatusResponse
+
+from twisted.internet.defer import inlineCallbacks
+
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.scheduling import addressmapping
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, \
+    InvalidCalendarUser, calendarUserFromPrincipal, RemoteCalendarUser
+from twistedcaldav.scheduling.scheduler import Scheduler, ScheduleResponseQueue
+
+from txdav.xml import element as davxml
+
+
+"""
+L{CalDAVScheduler} - handles deliveries for scheduling messages within the CalDAV server.
+"""
+
+__all__ = [
+    "CalDAVScheduler",
+]
+
+
+log = Logger()
+
+class CalDAVScheduler(Scheduler):
+
+    scheduleResponse = ScheduleResponseQueue
+
+    errorResponse = ErrorResponse
+
+    errorElements = {
+        "originator-missing": (caldav_namespace, "originator-specified"),
+        "originator-invalid": (caldav_namespace, "originator-allowed"),
+        "originator-denied": (caldav_namespace, "originator-allowed"),
+        "recipient-missing": (caldav_namespace, "recipient-specified"),
+        "recipient-invalid": (caldav_namespace, "recipient-exists"),
+        "organizer-denied": (caldav_namespace, "organizer-allowed"),
+        "attendee-denied": (caldav_namespace, "attendee-allowed"),
+        "invalid-calendar-data-type": (caldav_namespace, "supported-calendar-data"),
+        "invalid-calendar-data": (caldav_namespace, "valid-calendar-data"),
+        "invalid-scheduling-message": (caldav_namespace, "valid-calendar-data"),
+        "max-recipients": (caldav_namespace, "recipient-limit"),
+    }
+
+    def __init__(self, request, resource):
+        super(CalDAVScheduler, self).__init__(request, resource)
+        self.doingPOST = False
+
+
+    def doSchedulingViaPOST(self, transaction):
+        """
+        The Scheduling POST operation on an Outbox.
+        """
+        self.doingPOST = True
+        return super(CalDAVScheduler, self).doSchedulingViaPOST(transaction)
+
+
+    def checkAuthorization(self):
+        # Must have an authenticated user
+        if not self.internal_request and self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
+            log.err("Unauthenticated originators not allowed: %s" % (self.originator,))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["originator-denied"],
+                "Invalid originator",
+            ))
+
+
+    def checkOriginator(self):
+        """
+        Check the validity of the Originator header. Extract the corresponding principal.
+        """
+
+        # Verify that Originator is a valid calendar user
+        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+        if originatorPrincipal is None:
+            # Local requests MUST have a principal.
+            log.err("Could not find principal for originator: %s" % (self.originator,))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["originator-denied"],
+                "No principal for originator",
+            ))
+        else:
+            # Must have a valid Inbox.
+            inboxURL = originatorPrincipal.scheduleInboxURL()
+            if inboxURL is None:
+                log.err("Could not find inbox for originator: %s" % (self.originator,))
+                raise HTTPError(self.errorResponse(
+                    responsecode.FORBIDDEN,
+                    self.errorElements["originator-denied"],
+                    "Originator cannot be scheduled",
+                ))
+
+            self.originator = LocalCalendarUser(self.originator, originatorPrincipal)
+
+
+    @inlineCallbacks
+    def checkRecipients(self):
+        """
+        Check the validity of the Recipient header values. Map these into local or
+        remote CalendarUsers.
+        """
+
+        results = []
+        for recipient in self.recipients:
+            # Get the principal resource for this recipient
+            principal = self.resource.principalForCalendarUserAddress(recipient)
+
+            # If no principal we may have a remote recipient but we should check whether
+            # the address is one that ought to be on our server and treat that as a missing
+            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
+            if principal is None:
+                address = (yield addressmapping.mapper.getCalendarUser(recipient, principal))
+                if isinstance(address, InvalidCalendarUser):
+                    log.err("Unknown calendar user address: %s" % (recipient,))
+                results.append(address)
+            else:
+                # Map recipient to their inbox
+                inboxURL = principal.scheduleInboxURL()
+                inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
+
+                if inbox:
+                    results.append(calendarUserFromPrincipal(recipient, principal, inbox, inboxURL))
+                else:
+                    log.err("No schedule inbox for principal: %s" % (principal,))
+                    results.append(InvalidCalendarUser(recipient))
+
+        self.recipients = results
+
+
+    @inlineCallbacks
+    def checkOrganizer(self):
+        """
+        Check the validity of the ORGANIZER value. ORGANIZER must be local.
+        """
+
+        # Verify that the ORGANIZER's cu address maps to a valid user
+        organizer = self.calendar.getOrganizer()
+        if organizer:
+            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+            if organizerPrincipal:
+                outboxURL = organizerPrincipal.scheduleOutboxURL()
+                if outboxURL:
+
+                    # Only do this check for a freebusy request. A check for an invite needs
+                    # to be handled later when we know whether a new invite is being added
+                    # (which we reject) vs an update to an existing one (which we allow).
+                    if self.checkForFreeBusy() and not organizerPrincipal.enabledAsOrganizer():
+                        log.err("ORGANIZER not allowed to be an Organizer: %s" % (self.calendar,))
+                        raise HTTPError(self.errorResponse(
+                            responsecode.FORBIDDEN,
+                            self.errorElements["organizer-denied"],
+                            "Organizer cannot schedule",
+                        ))
+
+                    self.organizer = LocalCalendarUser(organizer, organizerPrincipal)
+                else:
+                    log.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
+                    raise HTTPError(self.errorResponse(
+                        responsecode.FORBIDDEN,
+                        self.errorElements["organizer-denied"],
+                        "Organizer cannot schedule",
+                    ))
+            else:
+                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
+                if localUser:
+                    log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendar,))
+                    raise HTTPError(self.errorResponse(
+                        responsecode.FORBIDDEN,
+                        self.errorElements["organizer-denied"],
+                        "No principal for organizer",
+                    ))
+                else:
+                    self.organizer = RemoteCalendarUser(organizer)
+        else:
+            log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["invalid-scheduling-message"],
+                "Missing organizer",
+            ))
+
+
+    def checkOrganizerAsOriginator(self):
+
+        # Make sure that the ORGANIZER is local
+        if not isinstance(self.organizer, LocalCalendarUser):
+            log.err("ORGANIZER is not local to server in calendar data: %s" % (self.calendar,))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["organizer-denied"],
+                "Organizer is not local to server",
+            ))
+
+        # Make sure that the ORGANIZER's Outbox is the request URI
+        if self.doingPOST and self.organizer.principal.scheduleOutboxURL() != self.request.uri:
+            log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["organizer-denied"],
+                "Outbox does not belong to organizer",
+            ))
+
+
+    def checkAttendeeAsOriginator(self):
+        """
+        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+        Only local attendees are allowed for message originating from this server.
+        """
+
+        # Attendee's Outbox MUST be the request URI
+        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
+        if attendeePrincipal:
+            if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != self.request.uri:
+                log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendar,))
+                raise HTTPError(self.errorResponse(
+                    responsecode.FORBIDDEN,
+                    self.errorElements["attendee-denied"],
+                    "Outbox does not belong to attendee",
+                ))
+        else:
+            log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["attendee-denied"],
+                "No principal for attendee",
+            ))
+
+
+    def securityChecks(self):
+        """
+        Check that the originator has the appropriate rights to send this type of iTIP message.
+        """
+
+        # Prevent spoofing of ORGANIZER with specific METHODs when local
+        if self.isiTIPRequest:
+            self.checkOrganizerAsOriginator()
+
+        # Prevent spoofing when doing reply-like METHODs
+        else:
+            self.checkAttendeeAsOriginator()
+
+
+    def finalChecks(self):
+        """
+        Final checks before doing the actual scheduling.
+        """
+
+        # With implicit scheduling only certain types of iTIP operations are allowed for POST.
+
+        if self.doingPOST:
+            # Freebusy requests always processed
+            if self.checkForFreeBusy():
+                return
+
+            # COUNTER and DECLINE-COUNTER allowed
+            if self.calendar.propertyValue("METHOD") in ("COUNTER", "DECLINECOUNTER"):
+                return
+
+            # Anything else is not allowed. However, for compatibility we will optionally
+            # return a success response for all attendees.
+            if config.Scheduling.CalDAV.OldDraftCompatibility:
+                self.fakeTheResult = True
+            else:
+                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid iTIP message for implicit scheduling"))

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/__init__.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/__init__.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/__init__.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_delivery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_delivery.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,38 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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 twistedcaldav.test.util
-from twistedcaldav.scheduling.caldav.delivery import ScheduleViaCalDAV
-from twistedcaldav.config import config
-from twisted.internet.defer import inlineCallbacks
-
-class CalDAV (twistedcaldav.test.util.TestCase):
-    """
-    twistedcaldav.scheduling.caldav tests
-    """
-
-    @inlineCallbacks
-    def test_matchCalendarUserAddress(self):
-        """
-        Make sure we do an exact comparison on EmailDomain
-        """
-        self.patch(config.Scheduling[ScheduleViaCalDAV.serviceType()], "EmailDomain", "example.com")
-        result = yield ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at example.com")
-        self.assertTrue(result)
-        result = yield ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at foo.example.com")
-        self.assertFalse(result)
-        result = yield ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at xyzexample.com")
-        self.assertFalse(result)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_delivery.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_delivery.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_delivery.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,38 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All 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 twistedcaldav.test.util
+from twistedcaldav.scheduling.caldav.delivery import ScheduleViaCalDAV
+from twistedcaldav.config import config
+from twisted.internet.defer import inlineCallbacks
+
+class CalDAV (twistedcaldav.test.util.TestCase):
+    """
+    twistedcaldav.scheduling.caldav tests
+    """
+
+    @inlineCallbacks
+    def test_matchCalendarUserAddress(self):
+        """
+        Make sure we do an exact comparison on EmailDomain
+        """
+        self.patch(config.Scheduling[ScheduleViaCalDAV.serviceType()], "EmailDomain", "example.com")
+        result = yield ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at example.com")
+        self.assertTrue(result)
+        result = yield ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at foo.example.com")
+        self.assertFalse(result)
+        result = yield ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at xyzexample.com")
+        self.assertFalse(result)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_resource.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,457 +0,0 @@
-##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.web2 import responsecode, http_headers
-from twext.web2.dav.util import davXMLFromStream
-from twext.web2.http import HTTPError
-from twext.web2.iweb import IResponse
-from twext.web2.stream import MemoryStream
-from twext.web2.test.test_server import SimpleRequest
-from twisted.internet.defer import inlineCallbacks
-from twistedcaldav import caldavxml, customxml
-from twistedcaldav.test.util import HomeTestCase, TestCase
-from txdav.xml import element as davxml
-
-class Properties (HomeTestCase):
-    """
-    CalDAV properties
-    """
-    def test_free_busy_set_prop(self):
-        """
-        Test for PROPFIND on Inbox with missing calendar-free-busy-set property.
-        """
-
-        inbox_uri = "/inbox/"
-
-        def propfind_cb(response):
-            response = IResponse(response)
-
-            if response.code != responsecode.MULTI_STATUS:
-                self.fail("Incorrect response to PROPFIND: %s" % (response.code,))
-
-            def got_xml(doc):
-                if not isinstance(doc.root_element, davxml.MultiStatus):
-                    self.fail("PROPFIND response XML root element is not multistatus: %r" % (doc.root_element,))
-
-                response = doc.root_element.childOfType(davxml.Response)
-                href = response.childOfType(davxml.HRef)
-                self.failUnless(str(href) == inbox_uri)
-
-                for propstat in response.childrenOfType(davxml.PropertyStatus):
-                    status = propstat.childOfType(davxml.Status)
-                    if status.code != responsecode.OK:
-                        self.fail("Unable to read requested properties (%s): %r"
-                                  % (status, propstat.childOfType(davxml.PropertyContainer).toxml()))
-
-                container = propstat.childOfType(davxml.PropertyContainer)
-
-                #
-                # Check CalDAV:calendar-free-busy-set
-                #
-
-                free_busy_set = container.childOfType(caldavxml.CalendarFreeBusySet)
-                if not free_busy_set:
-                    self.fail("Expected CalDAV:calendar-free-busy-set element; but got none.")
-
-                if not free_busy_set.children:
-                    self.fail("Expected non-empty CalDAV:calendar-free-busy-set element.")
-
-            return davXMLFromStream(response.stream).addCallback(got_xml)
-
-        query = davxml.PropertyFind(
-                    davxml.PropertyContainer(
-                        caldavxml.CalendarFreeBusySet(),
-                    ),
-                )
-
-        request = SimpleRequest(
-            self.site,
-            "PROPFIND",
-            inbox_uri,
-            headers=http_headers.Headers({"Depth": "0"}),
-        )
-        request.stream = MemoryStream(query.toxml())
-        return self.send(request, propfind_cb)
-
-
-    @inlineCallbacks
-    def test_free_busy_set_remove_broken(self):
-        """
-        ???
-        """
-
-        request = SimpleRequest(self.site, "GET", "/inbox/")
-        inbox = yield request.locateResource("/inbox/")
-        self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
-        oldfbset = set(("/calendar",))
-        oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
-
-        newfbset = set()
-        newfbset.update(oldfbset)
-        newfbset.add("/calendar-broken")
-        newset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in newfbset])
-
-        inbox.writeDeadProperty(newset)
-        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
-        self.assertEqual(tuple(changedset.children), tuple(newset.children))
-
-        yield inbox.writeProperty(newset, request)
-
-        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
-        self.assertEqual(tuple(changedset.children), tuple(oldset.children))
-
-
-    @inlineCallbacks
-    def test_free_busy_set_strip_slash(self):
-        """
-        ???
-        """
-
-        request = SimpleRequest(self.site, "GET", "/inbox/")
-        inbox = yield request.locateResource("/inbox/")
-        self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
-
-        oldfbset = set(("/calendar/",))
-        oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
-        inbox.writeDeadProperty(oldset)
-
-        writefbset = set(("/calendar/",))
-        writeset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in writefbset])
-        yield inbox.writeProperty(writeset, request)
-
-        correctfbset = set(("/calendar",))
-        correctset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in correctfbset])
-        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
-        self.assertEqual(tuple(changedset.children), tuple(correctset.children))
-
-
-    @inlineCallbacks
-    def test_free_busy_set_strip_slash_remove(self):
-        """
-        ???
-        """
-
-        request = SimpleRequest(self.site, "GET", "/inbox/")
-        inbox = yield request.locateResource("/inbox/")
-        self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
-
-        oldfbset = set(("/calendar/", "/broken/"))
-        oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
-        inbox.writeDeadProperty(oldset)
-
-        writefbset = set(("/calendar/", "/broken/"))
-        writeset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in writefbset])
-        yield inbox.writeProperty(writeset, request)
-
-        correctfbset = set(("/calendar",))
-        correctset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in correctfbset])
-        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
-        self.assertEqual(tuple(changedset.children), tuple(correctset.children))
-
-
-
-class DefaultCalendar (TestCase):
-
-    def setUp(self):
-        super(DefaultCalendar, self).setUp()
-        self.createStockDirectoryService()
-        self.setupCalendars()
-
-
-    @inlineCallbacks
-    def test_pick_default_vevent_calendar(self):
-        """
-        Test that pickNewDefaultCalendar will choose the correct calendar.
-        """
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property initially not present
-        try:
-            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
-        yield inbox.pickNewDefaultCalendar(request)
-
-        try:
-            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
-
-        request._newStoreTransaction.abort()
-
-
-    @inlineCallbacks
-    def test_pick_default_vtodo_calendar(self):
-        """
-        Test that pickNewDefaultCalendar will choose the correct tasks calendar.
-        """
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property initially not present
-        try:
-            inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("customxml.ScheduleDefaultTasksURL is not empty")
-
-        yield inbox.pickNewDefaultCalendar(request, tasks=True)
-
-        try:
-            default = inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
-        except HTTPError:
-            self.fail("customxml.ScheduleDefaultTasksURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/tasks")
-
-        request._newStoreTransaction.abort()
-
-
-    @inlineCallbacks
-    def test_missing_default_vevent_calendar(self):
-        """
-        Test that pickNewDefaultCalendar will create a missing default calendar.
-        """
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        home = yield request.locateResource("/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property initially not present
-        try:
-            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
-        # Forcibly remove the one we need
-        yield home._newStoreHome.removeChildWithName("calendar")
-        names = [calendarName for calendarName in (yield home._newStoreHome.listCalendars())]
-        self.assertTrue("calendar" not in names)
-
-        yield inbox.pickNewDefaultCalendar(request)
-
-        try:
-            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
-
-        request._newStoreTransaction.abort()
-
-
-    @inlineCallbacks
-    def test_missing_default_vtodo_calendar(self):
-        """
-        Test that pickNewDefaultCalendar will create a missing default tasks calendar.
-        """
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        home = yield request.locateResource("/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property initially not present
-        try:
-            inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultTasksURL is not empty")
-
-        # Forcibly remove the one we need
-        yield home._newStoreHome.removeChildWithName("tasks")
-        names = [calendarName for calendarName in (yield home._newStoreHome.listCalendars())]
-        self.assertTrue("tasks" not in names)
-
-        yield inbox.pickNewDefaultCalendar(request, tasks=True)
-
-        try:
-            default = inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultTasksURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/tasks")
-
-        request._newStoreTransaction.abort()
-
-
-    @inlineCallbacks
-    def test_pick_default_other(self):
-        """
-        Make calendar
-        """
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property not present
-        try:
-            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
-        # Create a new default calendar
-        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
-        yield newcalendar.createCalendarCollection()
-        inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(
-            davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-        ))
-
-        # Delete the normal calendar
-        calendar = yield request.locateResource("/calendars/users/wsanchez/calendar")
-        yield calendar.storeRemove(request, False, "/calendars/users/wsanchez/calendar")
-
-        inbox.removeDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-
-        # default property not present
-        try:
-            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-        request._newStoreTransaction.commit()
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-        yield inbox.pickNewDefaultCalendar(request)
-
-        try:
-            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-
-        request._newStoreTransaction.abort()
-
-
-    @inlineCallbacks
-    def test_fix_shared_default(self):
-        """
-        Make calendar
-        """
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # Create a new default calendar
-        newcalendar = yield request.locateResource("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-        yield newcalendar.createCalendarCollection()
-        inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(
-            davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-        ))
-        try:
-            default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            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
-
-        try:
-            default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
-
-        request._newStoreTransaction.abort()
-
-
-    @inlineCallbacks
-    def test_set_default_vevent_other(self):
-        """
-        Test that the default URL can be set to another VEVENT calendar
-        """
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property not present
-        try:
-            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
-        # Create a new default calendar
-        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
-        yield newcalendar.createCalendarCollection()
-        yield newcalendar.setSupportedComponents(("VEVENT",))
-        request._newStoreTransaction.commit()
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-        yield inbox.writeProperty(caldavxml.ScheduleDefaultCalendarURL(davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")), request)
-
-        try:
-            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-
-        request._newStoreTransaction.commit()
-
-
-    @inlineCallbacks
-    def test_is_default_calendar(self):
-        """
-        Test .isDefaultCalendar() returns the proper class or None.
-        """
-
-        # Create a new non-default calendar
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
-        yield newcalendar.createCalendarCollection()
-        yield newcalendar.setSupportedComponents(("VEVENT",))
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-        yield inbox.pickNewDefaultCalendar(request)
-        request._newStoreTransaction.commit()
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-        calendar = yield request.locateResource("/calendars/users/wsanchez/calendar")
-        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
-        tasks = yield request.locateResource("/calendars/users/wsanchez/tasks")
-
-        result = yield inbox.isDefaultCalendar(request, calendar)
-        self.assertEqual(result, caldavxml.ScheduleDefaultCalendarURL)
-
-        result = yield inbox.isDefaultCalendar(request, newcalendar)
-        self.assertEqual(result, None)
-
-        result = yield inbox.isDefaultCalendar(request, tasks)
-        self.assertEqual(result, customxml.ScheduleDefaultTasksURL)
-
-        request._newStoreTransaction.commit()

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_resource.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_resource.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_resource.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav/test/test_resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,457 @@
+##
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.web2 import responsecode, http_headers
+from twext.web2.dav.util import davXMLFromStream
+from twext.web2.http import HTTPError
+from twext.web2.iweb import IResponse
+from twext.web2.stream import MemoryStream
+from twext.web2.test.test_server import SimpleRequest
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav import caldavxml, customxml
+from twistedcaldav.test.util import HomeTestCase, TestCase
+from txdav.xml import element as davxml
+
+class Properties (HomeTestCase):
+    """
+    CalDAV properties
+    """
+    def test_free_busy_set_prop(self):
+        """
+        Test for PROPFIND on Inbox with missing calendar-free-busy-set property.
+        """
+
+        inbox_uri = "/inbox/"
+
+        def propfind_cb(response):
+            response = IResponse(response)
+
+            if response.code != responsecode.MULTI_STATUS:
+                self.fail("Incorrect response to PROPFIND: %s" % (response.code,))
+
+            def got_xml(doc):
+                if not isinstance(doc.root_element, davxml.MultiStatus):
+                    self.fail("PROPFIND response XML root element is not multistatus: %r" % (doc.root_element,))
+
+                response = doc.root_element.childOfType(davxml.Response)
+                href = response.childOfType(davxml.HRef)
+                self.failUnless(str(href) == inbox_uri)
+
+                for propstat in response.childrenOfType(davxml.PropertyStatus):
+                    status = propstat.childOfType(davxml.Status)
+                    if status.code != responsecode.OK:
+                        self.fail("Unable to read requested properties (%s): %r"
+                                  % (status, propstat.childOfType(davxml.PropertyContainer).toxml()))
+
+                container = propstat.childOfType(davxml.PropertyContainer)
+
+                #
+                # Check CalDAV:calendar-free-busy-set
+                #
+
+                free_busy_set = container.childOfType(caldavxml.CalendarFreeBusySet)
+                if not free_busy_set:
+                    self.fail("Expected CalDAV:calendar-free-busy-set element; but got none.")
+
+                if not free_busy_set.children:
+                    self.fail("Expected non-empty CalDAV:calendar-free-busy-set element.")
+
+            return davXMLFromStream(response.stream).addCallback(got_xml)
+
+        query = davxml.PropertyFind(
+                    davxml.PropertyContainer(
+                        caldavxml.CalendarFreeBusySet(),
+                    ),
+                )
+
+        request = SimpleRequest(
+            self.site,
+            "PROPFIND",
+            inbox_uri,
+            headers=http_headers.Headers({"Depth": "0"}),
+        )
+        request.stream = MemoryStream(query.toxml())
+        return self.send(request, propfind_cb)
+
+
+    @inlineCallbacks
+    def test_free_busy_set_remove_broken(self):
+        """
+        ???
+        """
+
+        request = SimpleRequest(self.site, "GET", "/inbox/")
+        inbox = yield request.locateResource("/inbox/")
+        self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
+        oldfbset = set(("/calendar",))
+        oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
+
+        newfbset = set()
+        newfbset.update(oldfbset)
+        newfbset.add("/calendar-broken")
+        newset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in newfbset])
+
+        inbox.writeDeadProperty(newset)
+        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
+        self.assertEqual(tuple(changedset.children), tuple(newset.children))
+
+        yield inbox.writeProperty(newset, request)
+
+        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
+        self.assertEqual(tuple(changedset.children), tuple(oldset.children))
+
+
+    @inlineCallbacks
+    def test_free_busy_set_strip_slash(self):
+        """
+        ???
+        """
+
+        request = SimpleRequest(self.site, "GET", "/inbox/")
+        inbox = yield request.locateResource("/inbox/")
+        self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
+
+        oldfbset = set(("/calendar/",))
+        oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
+        inbox.writeDeadProperty(oldset)
+
+        writefbset = set(("/calendar/",))
+        writeset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in writefbset])
+        yield inbox.writeProperty(writeset, request)
+
+        correctfbset = set(("/calendar",))
+        correctset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in correctfbset])
+        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
+        self.assertEqual(tuple(changedset.children), tuple(correctset.children))
+
+
+    @inlineCallbacks
+    def test_free_busy_set_strip_slash_remove(self):
+        """
+        ???
+        """
+
+        request = SimpleRequest(self.site, "GET", "/inbox/")
+        inbox = yield request.locateResource("/inbox/")
+        self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
+
+        oldfbset = set(("/calendar/", "/broken/"))
+        oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
+        inbox.writeDeadProperty(oldset)
+
+        writefbset = set(("/calendar/", "/broken/"))
+        writeset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in writefbset])
+        yield inbox.writeProperty(writeset, request)
+
+        correctfbset = set(("/calendar",))
+        correctset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in correctfbset])
+        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
+        self.assertEqual(tuple(changedset.children), tuple(correctset.children))
+
+
+
+class DefaultCalendar (TestCase):
+
+    def setUp(self):
+        super(DefaultCalendar, self).setUp()
+        self.createStockDirectoryService()
+        self.setupCalendars()
+
+
+    @inlineCallbacks
+    def test_pick_default_vevent_calendar(self):
+        """
+        Test that pickNewDefaultCalendar will choose the correct calendar.
+        """
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+
+        # default property initially not present
+        try:
+            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
+
+        yield inbox.pickNewDefaultCalendar(request)
+
+        try:
+            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
+
+        request._newStoreTransaction.abort()
+
+
+    @inlineCallbacks
+    def test_pick_default_vtodo_calendar(self):
+        """
+        Test that pickNewDefaultCalendar will choose the correct tasks calendar.
+        """
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+
+        # default property initially not present
+        try:
+            inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("customxml.ScheduleDefaultTasksURL is not empty")
+
+        yield inbox.pickNewDefaultCalendar(request, tasks=True)
+
+        try:
+            default = inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
+        except HTTPError:
+            self.fail("customxml.ScheduleDefaultTasksURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/tasks")
+
+        request._newStoreTransaction.abort()
+
+
+    @inlineCallbacks
+    def test_missing_default_vevent_calendar(self):
+        """
+        Test that pickNewDefaultCalendar will create a missing default calendar.
+        """
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        home = yield request.locateResource("/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+
+        # default property initially not present
+        try:
+            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
+
+        # Forcibly remove the one we need
+        yield home._newStoreHome.removeChildWithName("calendar")
+        names = [calendarName for calendarName in (yield home._newStoreHome.listCalendars())]
+        self.assertTrue("calendar" not in names)
+
+        yield inbox.pickNewDefaultCalendar(request)
+
+        try:
+            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
+
+        request._newStoreTransaction.abort()
+
+
+    @inlineCallbacks
+    def test_missing_default_vtodo_calendar(self):
+        """
+        Test that pickNewDefaultCalendar will create a missing default tasks calendar.
+        """
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        home = yield request.locateResource("/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+
+        # default property initially not present
+        try:
+            inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("caldavxml.ScheduleDefaultTasksURL is not empty")
+
+        # Forcibly remove the one we need
+        yield home._newStoreHome.removeChildWithName("tasks")
+        names = [calendarName for calendarName in (yield home._newStoreHome.listCalendars())]
+        self.assertTrue("tasks" not in names)
+
+        yield inbox.pickNewDefaultCalendar(request, tasks=True)
+
+        try:
+            default = inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultTasksURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/tasks")
+
+        request._newStoreTransaction.abort()
+
+
+    @inlineCallbacks
+    def test_pick_default_other(self):
+        """
+        Make calendar
+        """
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+
+        # default property not present
+        try:
+            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
+
+        # Create a new default calendar
+        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
+        yield newcalendar.createCalendarCollection()
+        inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(
+            davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
+        ))
+
+        # Delete the normal calendar
+        calendar = yield request.locateResource("/calendars/users/wsanchez/calendar")
+        yield calendar.storeRemove(request, False, "/calendars/users/wsanchez/calendar")
+
+        inbox.removeDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+
+        # default property not present
+        try:
+            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
+        request._newStoreTransaction.commit()
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+        yield inbox.pickNewDefaultCalendar(request)
+
+        try:
+            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
+
+        request._newStoreTransaction.abort()
+
+
+    @inlineCallbacks
+    def test_fix_shared_default(self):
+        """
+        Make calendar
+        """
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+
+        # Create a new default calendar
+        newcalendar = yield request.locateResource("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
+        yield newcalendar.createCalendarCollection()
+        inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(
+            davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
+        ))
+        try:
+            default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
+        else:
+            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
+
+        try:
+            default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
+
+        request._newStoreTransaction.abort()
+
+
+    @inlineCallbacks
+    def test_set_default_vevent_other(self):
+        """
+        Test that the default URL can be set to another VEVENT calendar
+        """
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+
+        # default property not present
+        try:
+            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            pass
+        else:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
+
+        # Create a new default calendar
+        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
+        yield newcalendar.createCalendarCollection()
+        yield newcalendar.setSupportedComponents(("VEVENT",))
+        request._newStoreTransaction.commit()
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+        yield inbox.writeProperty(caldavxml.ScheduleDefaultCalendarURL(davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")), request)
+
+        try:
+            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
+        except HTTPError:
+            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
+        else:
+            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
+
+        request._newStoreTransaction.commit()
+
+
+    @inlineCallbacks
+    def test_is_default_calendar(self):
+        """
+        Test .isDefaultCalendar() returns the proper class or None.
+        """
+
+        # Create a new non-default calendar
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
+        yield newcalendar.createCalendarCollection()
+        yield newcalendar.setSupportedComponents(("VEVENT",))
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+        yield inbox.pickNewDefaultCalendar(request)
+        request._newStoreTransaction.commit()
+
+        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
+        calendar = yield request.locateResource("/calendars/users/wsanchez/calendar")
+        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
+        tasks = yield request.locateResource("/calendars/users/wsanchez/tasks")
+
+        result = yield inbox.isDefaultCalendar(request, calendar)
+        self.assertEqual(result, caldavxml.ScheduleDefaultCalendarURL)
+
+        result = yield inbox.isDefaultCalendar(request, newcalendar)
+        self.assertEqual(result, None)
+
+        result = yield inbox.isDefaultCalendar(request, tasks)
+        self.assertEqual(result, customxml.ScheduleDefaultTasksURL)
+
+        request._newStoreTransaction.commit()

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/caldav.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,313 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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.python.log import Logger
-from twext.web2 import responsecode
-from twext.web2.dav.http import ErrorResponse
-from twext.web2.dav.resource import AccessDeniedError
-from twext.web2.dav.util import joinURL
-from twext.web2.http import HTTPError
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.python.failure import Failure
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.method import report_common
-from twistedcaldav.resource import isCalendarCollectionResource
-from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser,\
-    PartitionedCalendarUser, OtherServerCalendarUser
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.scheduling.processing import ImplicitProcessor, ImplicitProcessorException
-
-from txdav.xml import element as davxml
-
-import hashlib
-import uuid
-
-"""
-Handles the sending of scheduling messages to the server itself. This will cause
-actual processing of the delivery of the message to the recipient's inbox, via the
-L{ImplicitProcessor} class.
-"""
-
-__all__ = [
-    "ScheduleViaCalDAV",
-]
-
-log = Logger()
-
-class ScheduleViaCalDAV(DeliveryService):
-    
-    def __init__(self, scheduler, recipients, responses, freebusy):
-
-        self.scheduler = scheduler
-        self.recipients = recipients
-        self.responses = responses
-        self.freebusy = freebusy
-
-    @classmethod
-    def serviceType(cls):
-        return DeliveryService.serviceType_caldav
-
-    @classmethod
-    def matchCalendarUserAddress(cls, cuaddr):
-
-        # Check for local address matches first
-        if cuaddr.startswith("mailto:") and config.Scheduling[cls.serviceType()]["EmailDomain"]:
-            addr = cuaddr[7:].split("?")[0]
-            domain = config.Scheduling[cls.serviceType()]["EmailDomain"]
-            _ignore_account, addrDomain = addr.split("@")
-            if addrDomain == domain:
-                return True
-
-        elif (cuaddr.startswith("http://") or cuaddr.startswith("https://")) and config.Scheduling[cls.serviceType()]["HTTPDomain"]:
-            splits = cuaddr.split(":")[0][2:].split("?")
-            domain = config.Scheduling[cls.serviceType()]["HTTPDomain"]
-            if splits[0].endswith(domain):
-                return True
-
-        elif cuaddr.startswith("/"):
-            # Assume relative HTTP URL - i.e. on this server
-            return True
-        
-        # Do default match
-        return super(ScheduleViaCalDAV, cls).matchCalendarUserAddress(cuaddr)
-
-    @inlineCallbacks
-    def generateSchedulingResponses(self):
-        
-        # Extract the ORGANIZER property and UID value from the calendar data for use later
-        organizerProp = self.scheduler.calendar.getOrganizerProperty()
-        uid = self.scheduler.calendar.resourceUID()
-
-        organizerPrincipal = None
-        if type(self.scheduler.organizer) in (LocalCalendarUser, PartitionedCalendarUser, OtherServerCalendarUser,):
-            organizerPrincipal = davxml.Principal(davxml.HRef(self.scheduler.organizer.principal.principalURL()))
-
-        for recipient in self.recipients:
-
-            #
-            # Check access controls
-            #
-            if organizerPrincipal:
-                try:
-                    yield recipient.inbox.checkPrivileges(self.scheduler.request, (caldavxml.ScheduleDeliver(),), principal=organizerPrincipal)
-                except AccessDeniedError:
-                    log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
-                    err = HTTPError(ErrorResponse(
-                        responsecode.NOT_FOUND,
-                        (caldav_namespace, "recipient-permissions"),
-                        "Access to inbox denied",
-                    ))
-                    self.responses.add(
-                        recipient.cuaddr,
-                        Failure(exc_value=err),
-                        reqstatus=iTIPRequestStatus.NO_AUTHORITY
-                    )
-                
-                    # Process next recipient
-                    continue
-            else:
-                # TODO: need to figure out how best to do server-to-server authorization.
-                # First thing would be to check for DAV:unauthenticated privilege.
-                # Next would be to allow the calendar user address of the organizer/originator to be used
-                # as a principal. 
-                pass
-
-            # Different behavior for free-busy vs regular invite
-            if self.freebusy:
-                # Look for special delegate extended free-busy request
-                event_details = [] if self.scheduler.calendar.getExtendedFreeBusy() else None
-
-                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid, event_details)
-            else:
-                yield self.generateResponse(recipient, self.responses)
-
-    @inlineCallbacks
-    def generateResponse(self, recipient, responses):
-        # Hash the iCalendar data for use as the last path element of the URI path
-        name =  "%s-%s.ics" % (hashlib.md5(self.scheduler.calendar.resourceUID()).hexdigest(), str(uuid.uuid4())[:8],)
-    
-        # Get a resource for the new item
-        childURL = joinURL(recipient.inboxURL, name)
-        child = (yield self.scheduler.request.locateResource(childURL))
-
-        # Do implicit scheduling message processing.
-        try:
-            processor = ImplicitProcessor()
-            _ignore_processed, autoprocessed, store_inbox, changes = (yield processor.doImplicitProcessing(
-                self.scheduler.request,
-                self.scheduler.calendar,
-                self.scheduler.originator,
-                recipient
-            ))
-        except ImplicitProcessorException, e:
-            log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
-            err = HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "recipient-permissions"),
-                "Could not store data in inbox",
-            ))
-            responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=e.msg)
-            returnValue(False)
-
-        if store_inbox:
-            # Copy calendar to inbox 
-            try:
-                from twistedcaldav.method.put_common import StoreCalendarObjectResource
-                yield StoreCalendarObjectResource(
-                             request=self.scheduler.request,
-                             destination = child,
-                             destination_uri = childURL,
-                             destinationparent = recipient.inbox,
-                             destinationcal = True,
-                             calendar = self.scheduler.calendar,
-                             isiTIP = True,
-                             internal_request = True,
-                         ).run()
-            except:
-                # FIXME: Bare except
-                log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
-                err = HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (caldav_namespace, "recipient-permissions"),
-                    "Could not store data in inbox",
-                ))
-                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
-                returnValue(False)
-            else:
-                # Store CALDAV:originator property
-                child.writeDeadProperty(caldavxml.Originator(davxml.HRef(self.scheduler.originator.cuaddr)))
-            
-                # Store CALDAV:recipient property
-                child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient.cuaddr)))
-            
-                # Store CS:schedule-changes property if present
-                if changes:
-                    child.writeDeadProperty(changes)
-
-        responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.MESSAGE_DELIVERED)
-        if autoprocessed:
-            if not hasattr(self.scheduler.request, "extendedLogItems"):
-                self.scheduler.request.extendedLogItems = {}
-            self.scheduler.request.extendedLogItems["itip.auto"] = self.scheduler.request.extendedLogItems.get("itip.auto", 0) + 1
-        returnValue(True)
-
-    @inlineCallbacks
-    def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid, event_details):
-
-        # Extract the ATTENDEE property matching current recipient from the calendar data
-        cuas = recipient.principal.calendarUserAddresses()
-        attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
-
-        remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
-
-        try:
-            fbresult = (yield self.generateAttendeeFreeBusyResponse(
-                recipient,
-                organizerProp,
-                organizerPrincipal,
-                uid,
-                attendeeProp,
-                remote,
-                event_details,
-            ))
-        except:
-            log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
-            err = HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "recipient-permissions"),
-                "Could not determine free busy information",
-            ))
-            responses.add(
-                recipient.cuaddr,
-                Failure(exc_value=err),
-                reqstatus=iTIPRequestStatus.NO_AUTHORITY
-            )
-            returnValue(False)
-        else:
-            responses.add(
-                recipient.cuaddr,
-                responsecode.OK,
-                reqstatus=iTIPRequestStatus.SUCCESS,
-                calendar=fbresult
-            )
-            returnValue(True)
-    
-    @inlineCallbacks
-    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None):
-
-        # Find the current recipients calendar-free-busy-set
-        fbset = (yield recipient.principal.calendarFreeBusyURIs(self.scheduler.request))
-
-        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-        fbinfo = ([], [], [])
-    
-        # Process the availability property from the Inbox.
-        has_prop = (yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
-        if has_prop:
-            availability = (yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
-            availability = availability.calendar()
-            report_common.processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
-
-        # Check to see if the recipient is the same calendar user as the organizer.
-        # Needed for masked UID stuff.
-        if isinstance(self.scheduler.organizer, LocalCalendarUser):
-            same_calendar_user = self.scheduler.organizer.principal.principalURL() == recipient.principal.principalURL()
-        else:
-            same_calendar_user = False
-
-        # Now process free-busy set calendars
-        matchtotal = 0
-        for calendarResourceURL in fbset:
-            if not calendarResourceURL.endswith('/'):
-                calendarResourceURL += '/'
-            calendarResource = (yield self.scheduler.request.locateResource(calendarResourceURL))
-            if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
-                # We will ignore missing calendars. If the recipient has failed to
-                # properly manage the free busy set that should not prevent us from working.
-                continue
-         
-            matchtotal = (yield report_common.generateFreeBusyInfo(
-                self.scheduler.request,
-                calendarResource,
-                fbinfo,
-                self.scheduler.timeRange,
-                matchtotal,
-                excludeuid = self.scheduler.excludeUID,
-                organizer = self.scheduler.organizer.cuaddr,
-                organizerPrincipal = organizerPrincipal,
-                same_calendar_user = same_calendar_user,
-                servertoserver=remote,
-                event_details=event_details,
-            ))
-    
-        # Build VFREEBUSY iTIP reply for this recipient
-        fbresult = report_common.buildFreeBusyResult(
-            fbinfo,
-            self.scheduler.timeRange,
-            organizer = organizerProp,
-            attendee = attendeeProp,
-            uid = uid,
-            method = "REPLY",
-            event_details=event_details,
-        )
-
-        returnValue(fbresult)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/cuaddress.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/cuaddress.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/cuaddress.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -31,48 +31,66 @@
 log = Logger()
 
 class CalendarUser(object):
+
     def __init__(self, cuaddr):
         self.cuaddr = cuaddr
         self.serviceType = None
 
+
+
 class LocalCalendarUser(CalendarUser):
+
     def __init__(self, cuaddr, principal, inbox=None, inboxURL=None):
         self.cuaddr = cuaddr
         self.principal = principal
         self.inbox = inbox
         self.inboxURL = inboxURL
         self.serviceType = DeliveryService.serviceType_caldav
-    
+
+
     def __str__(self):
         return "Local calendar user: %s" % (self.cuaddr,)
 
+
+
 class PartitionedCalendarUser(CalendarUser):
+
     def __init__(self, cuaddr, principal):
         self.cuaddr = cuaddr
         self.principal = principal
         self.serviceType = DeliveryService.serviceType_ischedule
 
+
     def __str__(self):
         return "Partitioned calendar user: %s" % (self.cuaddr,)
 
+
+
 class OtherServerCalendarUser(CalendarUser):
+
     def __init__(self, cuaddr, principal):
         self.cuaddr = cuaddr
         self.principal = principal
         self.serviceType = DeliveryService.serviceType_ischedule
 
+
     def __str__(self):
         return "Other server calendar user: %s" % (self.cuaddr,)
 
+
+
 class RemoteCalendarUser(CalendarUser):
+
     def __init__(self, cuaddr):
         self.cuaddr = cuaddr
         self.extractDomain()
         self.serviceType = DeliveryService.serviceType_ischedule
 
+
     def __str__(self):
         return "Remote calendar user: %s" % (self.cuaddr,)
-    
+
+
     def extractDomain(self):
         if self.cuaddr.startswith("mailto:"):
             splits = self.cuaddr[7:].split("?")
@@ -83,21 +101,27 @@
         else:
             self.domain = ""
 
+
+
 class EmailCalendarUser(CalendarUser):
-    
+
     def __init__(self, cuaddr):
         self.cuaddr = cuaddr
         self.serviceType = DeliveryService.serviceType_imip
-    
+
+
     def __str__(self):
         return "Email/iMIP calendar user: %s" % (self.cuaddr,)
 
+
+
 class InvalidCalendarUser(CalendarUser):
-    
+
     def __str__(self):
         return "Invalid calendar user: %s" % (self.cuaddr,)
 
 
+
 def normalizeCUAddr(addr):
     """
     Normalize a cuaddr string by lower()ing it if it's a mailto:, or
@@ -115,11 +139,13 @@
     else:
         return addr
 
+
+
 def calendarUserFromPrincipal(recipient, principal, inbox=None, inboxURL=None):
     """
     Get the appropriate calendar user address class for the provided principal.
     """
-    
+
     if principal.locallyHosted():
         return LocalCalendarUser(recipient, principal, inbox, inboxURL)
     elif principal.thisServer():

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/delivery.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/delivery.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -19,6 +19,7 @@
 from twext.python.log import Logger
 
 from twistedcaldav.config import config
+from twisted.internet.defer import succeed
 
 __all__ = [
     "DeliveryService",
@@ -30,12 +31,12 @@
     """
     Abstract base class that defines a delivery method for a scheduling message.
     """
-    
+
     # Known types
-    
-    serviceType_caldav    = 'CalDAV'
+
+    serviceType_caldav = 'CalDAV'
     serviceType_ischedule = 'iSchedule'
-    serviceType_imip      = 'iMIP'
+    serviceType_imip = 'iMIP'
 
     def __init__(self, scheduler, recipients, responses, freebusy):
 
@@ -44,23 +45,34 @@
         self.responses = responses
         self.freebusy = freebusy
 
+
     @classmethod
     def serviceType(cls):
         raise NotImplementedError
 
+
     @classmethod
     def matchCalendarUserAddress(cls, cuaddr):
+        """
+        Determine whether the delivery service is able to handle the specified calendar user address.
 
+        @param cuaddr: calendar user address to test
+        @type cuaddr: C{str}
+
+        @return: L{Deferred} with result C{True} or C{False}
+        """
+
         cuaddr = cuaddr.lower()
         # Do the pattern match
         for pattern in config.Scheduling[cls.serviceType()]["AddressPatterns"]:
             try:
                 if re.match(pattern, cuaddr) is not None:
-                    return True
+                    return succeed(True)
             except re.error:
                 log.error("Invalid regular expression for Scheduling configuration '%s/LocalAddresses': %s" % (cls.serviceType(), pattern,))
 
-        return False
+        return succeed(False)
 
+
     def generateSchedulingResponses(self):
         raise NotImplementedError

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/icaldiff.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/icaldiff.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -36,26 +36,27 @@
 log = Logger()
 
 class iCalDiff(object):
-    
+
     def __init__(self, oldcalendar, newcalendar, smart_merge):
         """
-        
+
         @param oldcalendar:
         @type oldcalendar:
         @param newcalendar:
         @type newcalendar:
         """
-        
+
         self.oldcalendar = oldcalendar
         self.newcalendar = newcalendar
         self.smart_merge = smart_merge
-    
+
+
     def organizerDiff(self):
         """
         Diff the two calendars looking for changes that should trigger implicit scheduling if
         changed by an organizer. Basically any change except for anything related to a VALARM.
         """
-        
+
         # If smart merge is needed we have to do this before trying the diff
         if self.smart_merge:
             log.debug("organizerDiff: doing smart Organizer diff/merge")
@@ -76,7 +77,7 @@
             calendar.removePropertyParameters("ATTENDEE", ("RSVP", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
             calendar.normalizeAll()
             return calendar
-        
+
         # Normalize components for comparison
         oldcalendar_norm = duplicateAndNormalize(self.oldcalendar)
         newcalendar_norm = duplicateAndNormalize(self.newcalendar)
@@ -84,6 +85,7 @@
         result = oldcalendar_norm == newcalendar_norm
         return result
 
+
     def _organizerMerge(self):
         """
         Merge changes to ATTENDEE properties in oldcalendar into newcalendar.
@@ -91,20 +93,21 @@
         organizer = normalizeCUAddr(self.newcalendar.masterComponent().propertyValue("ORGANIZER"))
         self._doSmartMerge(organizer, True)
 
+
     def _doSmartMerge(self, ignore_attendee, is_organizer):
         """
         Merge changes to ATTENDEE properties in oldcalendar into newcalendar.
         """
-        
+
         old_master = self.oldcalendar.masterComponent()
         new_master = self.newcalendar.masterComponent()
-        
+
         # Do master merge first
         self._tryComponentMerge(old_master, new_master, ignore_attendee, is_organizer)
 
         # New check the matching components
         for old_component in self.oldcalendar.subcomponents():
-            
+
             # Make sure we have an appropriate component
             if old_component.name() == "VTIMEZONE":
                 continue
@@ -118,7 +121,7 @@
                 # If the old component was cancelled ignore when an attendee
                 if not is_organizer and old_component.propertyValue("STATUS") == "CANCELLED":
                     continue
-                
+
                 # Determine whether the instance is still valid in the new calendar
                 new_component = self.newcalendar.deriveInstance(rid)
                 if new_component:
@@ -133,7 +136,7 @@
 
         # Check the new instances not in the old calendar
         for new_component in self.newcalendar.subcomponents():
-            
+
             # Make sure we have an appropriate component
             if new_component.name() == "VTIMEZONE":
                 continue
@@ -147,7 +150,7 @@
                 # If the new component is cancelled ignore when an attendee
                 if not is_organizer and new_component.propertyValue("STATUS") == "CANCELLED":
                     continue
-                
+
                 # Try to derive a new instance in the client and transfer attendee status
                 old_component = self.oldcalendar.deriveInstance(rid)
                 if old_component:
@@ -156,11 +159,13 @@
                 else:
                     # Ignore as we have no state for the new instance
                     pass
-    
+
+
     def _tryComponentMerge(self, old_comp, new_comp, ignore_attendee_value, is_organizer):
         if not is_organizer or not self._organizerChangePreventsMerge(old_comp, new_comp):
             self._transferAttendees(old_comp, new_comp, ignore_attendee_value)
 
+
     def _organizerChangePreventsMerge(self, old_comp, new_comp):
         """
         Check whether a change from an Organizer needs a re-schedule which means that any
@@ -175,7 +180,7 @@
         """
 
         props_to_test = ("DTSTART", "DTEND", "DURATION", "RRULE", "RDATE", "EXDATE", "RECURRENCE-ID",)
-        
+
         for prop in props_to_test:
             # Change => no merge
             old_props = set(old_comp.properties(prop))
@@ -184,7 +189,8 @@
                 return True
 
         return False
-    
+
+
     def _transferAttendees(self, old_comp, new_comp, ignore_attendee_value):
         """
         Transfer Attendee PARTSTAT from old component to new component.
@@ -206,7 +212,7 @@
             old_attendees[value] = attendee
 
         for new_attendee in new_comp.properties("ATTENDEE"):
-            
+
             # Whenever SCHEDULE-FORCE-SEND is explicitly set by the Organizer we assume the Organizer
             # is deliberately overwriting PARTSTAT
             if new_attendee.parameterValue("SCHEDULE-FORCE-SEND", "") == "REQUEST":
@@ -219,7 +225,8 @@
                 self._transferParameter(old_attendee, new_attendee, "PARTSTAT")
                 self._transferParameter(old_attendee, new_attendee, "RSVP")
                 self._transferParameter(old_attendee, new_attendee, "SCHEDULE-STATUS")
-    
+
+
     def _transferParameter(self, old_property, new_property, parameter):
         paramvalue = old_property.parameterValue(parameter)
         if paramvalue is None:
@@ -230,21 +237,22 @@
         else:
             new_property.setParameter(parameter, paramvalue)
 
+
     def attendeeMerge(self, attendee):
         """
         Merge the ATTENDEE specific changes with the organizer's view of the attendee's event.
         This will remove any attempt by the attendee to change things like the time or location.
-       
+
         @param attendee: the value of the ATTENDEE property corresponding to the attendee making the change
         @type attendee: C{str}
-        
+
         @return: C{tuple} of:
             C{bool} - change is allowed
             C{bool} - iTIP reply needs to be sent
             C{list} - list of RECURRENCE-IDs changed
             L{Component} - new calendar object to store
         """
-        
+
         self.attendee = normalizeCUAddr(attendee)
 
         returnCalendar = self.oldcalendar.duplicate()
@@ -252,7 +260,7 @@
 
         changeCausesReply = False
         changedRids = []
-        
+
         # First get uid/rid map of components
         def mapComponents(calendar):
             map = {}
@@ -269,7 +277,7 @@
                     cancelledRids.add(rid)
                 if rid is None:
                     master = component
-            
+
             # Normalize each master by adding any STATUS:CANCELLED components as EXDATEs
             exdates = None
             if master:
@@ -277,9 +285,9 @@
                 exdates = set()
                 for exdate in master.properties("EXDATE"):
                     exdates.update([value.getValue().duplicate().adjustToUTC() for value in exdate.value()])
-               
+
             return exdates, map, master
-        
+
         exdatesold, mapold, masterold = mapComponents(self.oldcalendar)
         setold = set(mapold.keys())
         exdatesnew, mapnew, masternew = mapComponents(self.newcalendar)
@@ -303,18 +311,18 @@
                     return False, False, (), None
             else:
                 componentold = self.oldcalendar.overriddenComponent(masternewStart)
-            
+
             # Take the recurrence ID from component1 and fix map2/set2
             keynew = (masternew.name(), masternew.propertyValue("UID"), None)
             componentnew = mapnew[keynew]
             del mapnew[keynew]
-            
+
             ridold = componentold.getRecurrenceIDUTC()
             newkeynew = (masternew.name(), masternew.propertyValue("UID"), ridold)
             mapnew[newkeynew] = componentnew
             setnew.remove(keynew)
             setnew.add(newkeynew)
-    
+
         # All the components in oldcalendar must be in newcalendar unless they are CANCELLED
         for key in setold - setnew:
             _ignore_name, _ignore_uid, rid = key
@@ -327,7 +335,7 @@
                     if self._attendeeDecline(overridden):
                         changeCausesReply = True
                         changedRids.append(rid.getText() if rid else "")
-                        
+
                     # When a master component is present we keep the missing override in place but mark it as hidden.
                     # When no master is present we remove the override,
                     if exdatesnew is not None:
@@ -338,7 +346,7 @@
                     # We used to generate a 403 here - but instead we now ignore this error and let the server data
                     # override the client
                     self._logDiffError("attendeeMerge: Missing uncancelled component from first calendar: %s" % (key,))
-            else: 
+            else:
                 if exdatesnew is not None and rid not in exdatesnew:
                     # We used to generate a 403 here - but instead we now ignore this error and let the server data
                     # override the client
@@ -350,11 +358,11 @@
                     if returnMaster:
                         # Use the original R-ID value so we preserve the timezone
                         original_rid = component.propertyValue("RECURRENCE-ID")
-                        returnMaster.addProperty(Property("EXDATE", [original_rid,]))
-        
+                        returnMaster.addProperty(Property("EXDATE", [original_rid, ]))
+
         # Derive a new component in the new calendar for each new one in setnew
         for key in setnew - setold:
-            
+
             # First check if the attendee's copy is cancelled and properly EXDATE'd
             # and skip it if so.
             _ignore_name, _ignore_uid, rid = key
@@ -387,7 +395,7 @@
                     returnCalendar.addComponent(newOverride)
 
         # So now returnCalendar has all the same components as set2. Check changes and do transfers.
-        
+
         # Make sure the same VCALENDAR properties match
         if not self._checkVCALENDARProperties(returnCalendar, self.newcalendar):
             # We used to generate a 403 here - but instead we now ignore this error and let the server data
@@ -402,7 +410,7 @@
             _ignore_name, _ignore_uid, rid = key
             serverData = returnCalendar.overriddenComponent(rid)
             clientData = mapnew[key]
-            
+
             allowed, reply = self._transferAttendeeData(serverData, clientData, declines)
             if not allowed:
                 # We used to generate a 403 here - but instead we now ignore this error and let the server data
@@ -422,7 +430,7 @@
                     if self._attendeeDecline(overridden):
                         changeCausesReply = True
                         changedRids.append(decline.getText() if decline else "")
-                        
+
                     # When a master component is present we keep the missing override in place but mark it as hidden.
                     # When no master is present we remove the override,
                     if exdatesnew is not None:
@@ -434,43 +442,45 @@
 
         return True, changeCausesReply, changedRids, returnCalendar
 
+
     def _checkVCALENDARProperties(self, serverData, clientData):
 
         self._transferProperty("X-CALENDARSERVER-ACCESS", serverData, clientData)
 
         # Get property differences in the VCALENDAR objects
         propdiff = set(serverData.properties()) ^ set(clientData.properties())
-        
+
         # Ignore certain properties
         ignored = ("PRODID", "CALSCALE",)
         propdiff = set([prop for prop in propdiff if prop.name() not in ignored])
-        
+
         result = len(propdiff) == 0
         if not result:
             log.debug("VCALENDAR properties differ: %s" % (propdiff,))
         return result
 
+
     def _transferAttendeeData(self, serverComponent, clientComponent, declines):
-        
+
         # We are skipping this check now - instead we let the server data override the broken client data
         # First check validity of date-time related properties and get removed components which are declines
         self._checkInvalidChanges(serverComponent, clientComponent, declines)
-        
+
         # Now look for items to transfer from one to the other.
         # We care about the ATTENDEE's PARTSTAT, TRANSP, VALARMS, X-APPLE-NEEDS-REPLY,
         # DTSTAMP, LAST-MODIFIED, COMPLETED, and ATTACH's referring to a dropbox
-        
+
         replyNeeded = False
 
         # ATTENDEE/PARTSTAT/RSVP
         serverAttendee = serverComponent.getAttendeeProperty((self.attendee,))
         clientAttendee = clientComponent.getAttendeeProperty((self.attendee,))
-        
+
         # Possible case where one ATTENDEE prop is missing - this happens with a "fake" master sometimes
         if serverAttendee is None or clientAttendee is None:
             log.err("ATTENDEE for user making an attendee change is missing: %s" % (self.attendee,))
             return False, False
-    
+
         if serverAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION") != clientAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION"):
             serverAttendee.setParameter("PARTSTAT", clientAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION"))
             replyNeeded = True
@@ -490,7 +500,7 @@
         self._transferProperty("LAST-MODIFIED", serverComponent, clientComponent)
         self._transferProperty("X-APPLE-NEEDS-REPLY", serverComponent, clientComponent)
         self._transferProperty("COMPLETED", serverComponent, clientComponent)
-        
+
         # Dropbox - this now never returns false
         self._transferDropBoxData(serverComponent, clientComponent)
 
@@ -499,14 +509,15 @@
         for comp in clientComponent.subcomponents():
             if comp.name() == "VALARM":
                 serverComponent.addComponent(comp)
-        
+
         return True, replyNeeded
-    
+
+
     def _transferDropBoxData(self, serverComponent, clientComponent):
-        
+
         serverDropbox = serverComponent.propertyValue("X-APPLE-DROPBOX")
         clientDropbox = clientComponent.propertyValue("X-APPLE-DROPBOX")
-        
+
         # Handle four cases
         if not clientDropbox:
             return True
@@ -527,7 +538,7 @@
                     dataValue = attachment.value()
                     if dataValue.find(serverDropbox) != -1:
                         serverComponent.removeProperty(attachment)
-        
+
             # Copy new ATTACH's to server
             for attachment in tuple(clientComponent.properties("ATTACH")):
                 valueType = attachment.parameterValue("VALUE")
@@ -535,16 +546,17 @@
                     dataValue = attachment.value()
                     if dataValue.find(serverDropbox) != -1:
                         serverComponent.addProperty(attachment)
-                        
+
             return True
-        
+
+
     def _checkInvalidChanges(self, serverComponent, clientComponent, declines):
-        
+
         # Properties we care about: DTSTART, DTEND, DURATION, RRULE, RDATE, EXDATE
-        
+
         serverProps = self._getNormalizedDateTimeProperties(serverComponent)
         clientProps = self._getNormalizedDateTimeProperties(clientComponent)
-        
+
         # Need to special case EXDATEs as an Attendee can effectively DECLINE by adding an EXDATE
         if serverProps[:-1] != clientProps[:-1]:
             invalidChanges = []
@@ -562,30 +574,31 @@
             return True
         else:
             return True
-        
+
+
     def _getNormalizedDateTimeProperties(self, component):
-        
+
         # Basic time properties
         if component.name() in ("VEVENT", "VJOURNAL",):
             dtstart = component.getProperty("DTSTART")
             dtend = component.getProperty("DTEND")
             duration = component.getProperty("DURATION")
-            
+
             timeRange = PyCalendarPeriod(
-                start    = dtstart.value()  if dtstart  is not None else None,
-                end      = dtend.value()    if dtend    is not None else None,
-                duration = duration.value() if duration is not None else None,
+                start=dtstart.value()  if dtstart  is not None else None,
+                end=dtend.value()    if dtend    is not None else None,
+                duration=duration.value() if duration is not None else None,
             )
             newdue = None
-            
+
         elif component.name() == "VTODO":
             dtstart = component.getProperty("DTSTART")
             duration = component.getProperty("DURATION")
-            
+
             if dtstart or duration:
                 timeRange = PyCalendarPeriod(
-                    start    = dtstart.value()  if dtstart  is not None else None,
-                    duration = duration.value() if duration is not None else None,
+                    start=dtstart.value()  if dtstart  is not None else None,
+                    duration=duration.value() if duration is not None else None,
                 )
             else:
                 timeRange = PyCalendarPeriod()
@@ -593,7 +606,7 @@
             newdue = component.getProperty("DUE")
             if newdue is not None:
                 newdue = newdue.value().duplicate().adjustToUTC()
-            
+
         # Recurrence rules - we need to normalize the order of the value parts
         newrrules = set()
         rrules = component.properties("RRULE")
@@ -602,7 +615,7 @@
             indexedTokens.update([valuePart.split("=") for valuePart in rrule.value().getText().split(";")])
             sortedValue = ";".join(["%s=%s" % (key, value,) for key, value in sorted(indexedTokens.iteritems(), key=lambda x:x[0])])
             newrrules.add(sortedValue)
-        
+
         # RDATEs
         newrdates = set()
         rdates = component.properties("RDATE")
@@ -611,7 +624,7 @@
                 if isinstance(value, PyCalendarDateTime):
                     value = value.duplicate().adjustToUTC()
                 newrdates.add(value)
-        
+
         # EXDATEs
         newexdates = set()
         exdates = component.properties("EXDATE")
@@ -620,6 +633,7 @@
 
         return timeRange.getStart(), timeRange.getEnd(), newdue, newrrules, newrdates, newexdates
 
+
     def _transferProperty(self, propName, serverComponent, clientComponent):
 
         changed = False
@@ -640,7 +654,7 @@
 
         @param component:
         @type component:
-        
+
         @return: C{bool} indicating whether the PARTSTAT value was in fact changed
         """
         attendee = component.getAttendeeProperty((self.attendee,))
@@ -649,14 +663,16 @@
         if attendee is None:
             log.err("ATTENDEE for user making an attendee change is missing: %s" % (self.attendee,))
             return False
-    
+
         partstatChanged = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") != "DECLINED"
         attendee.setParameter("PARTSTAT", "DECLINED")
         prop = component.getProperty("X-APPLE-NEEDS-REPLY")
         if prop:
             component.removeProperty(prop)
+        component.replaceProperty(Property("TRANSP", "TRANSPARENT"))
         return partstatChanged
 
+
     def whatIsDifferent(self):
         """
         Compare the two calendar objects in their entirety and return a list of properties
@@ -674,7 +690,7 @@
                 rid = component.getRecurrenceIDUTC()
                 map[(name, uid, rid,)] = component
             return map
-        
+
         rids = {}
 
         oldmap = mapComponents(self.oldcalendar)
@@ -687,7 +703,7 @@
             component1 = oldmap[key]
             component2 = newmap[key]
             self._diffComponents(component1, component2, rids)
-        
+
         # Now verify that each additional component in oldset matches a derived component in newset
         for key in oldset - newset:
             oldcomponent = oldmap[key]
@@ -695,7 +711,7 @@
             if newcomponent is None:
                 continue
             self._diffComponents(oldcomponent, newcomponent, rids)
-        
+
         # Now verify that each additional component in oldset matches a derived component in newset
         for key in newset - oldset:
             oldcomponent = self.oldcalendar.deriveInstance(key[2])
@@ -703,9 +719,10 @@
                 continue
             newcomponent = newmap[key]
             self._diffComponents(oldcomponent, newcomponent, rids)
-        
+
         return rids
 
+
     def _componentDuplicateAndNormalize(self, comp):
         comp = comp.duplicate()
         comp.normalizePropertyValueLists("EXDATE")
@@ -717,14 +734,15 @@
         iTipGenerator.prepareSchedulingMessage(comp, reply=True)
         return comp
 
+
     def _diffComponents(self, comp1, comp2, rids):
-        
+
         assert isinstance(comp1, Component) and isinstance(comp2, Component)
-        
+
         if comp1.name() != comp2.name():
             log.debug("Component names are different: '%s' and '%s'" % (comp1.name(), comp2.name()))
             return
-        
+
         # Duplicate then normalize for comparison
         comp1 = self._componentDuplicateAndNormalize(comp1)
         comp2 = self._componentDuplicateAndNormalize(comp2)
@@ -732,7 +750,7 @@
         # Diff all the properties
         propdiff = set(comp1.properties()) ^ set(comp2.properties())
         addedChanges = False
-        
+
         propsChanged = {}
         for prop in propdiff:
             if prop.name() in (
@@ -755,11 +773,12 @@
             if "_TZID" in propsChanged[prop.name()]:
                 propsChanged[prop.name()].remove("_TZID")
                 propsChanged[prop.name()].add("TZID")
-        
+
         if addedChanges:
             rid = comp1.getRecurrenceIDUTC()
             rids[rid.getText() if rid is not None else ""] = propsChanged
 
+
     def _logDiffError(self, title):
 
         strcal1 = str(self.oldcalendar)
@@ -770,7 +789,7 @@
             fromfile='Existing Calendar Object',
             tofile='New Calendar Object',
         ))
-        
+
         logstr = """%s
 
 ------ Existing Calendar Data ------

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/__init__.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/__init__.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/__init__.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/delivery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/delivery.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,150 +0,0 @@
-# -*- test-case-name: twistedcaldav.scheduling.test.test_imip -*-
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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.
-##
-
-"""
-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.http import HTTPError
-from twisted.web import client
-
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.util import AuthorizedHTTPGetter
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.internet.adaptendpoint import connect
-
-
-__all__ = [
-    "ScheduleViaIMip",
-]
-
-log = Logger()
-
-class ScheduleViaIMip(DeliveryService):
-
-    @classmethod
-    def serviceType(cls):
-        return DeliveryService.serviceType_imip
-
-
-    @inlineCallbacks
-    def generateSchedulingResponses(self):
-        def failForRecipient(recipient):
-            err = HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "recipient-failed"),
-                "iMIP request failed",
-            ))
-            self.responses.add(
-                recipient.cuaddr,
-                Failure(exc_value=err),
-                reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE,
-                suppressErrorLog=True
-            )
-
-        # Generate an HTTP client request
-        try:
-            # We do not do freebusy requests via iMIP
-            if self.freebusy:
-                raise ValueError("iMIP VFREEBUSY requests not supported.")
-
-            method = self.scheduler.calendar.propertyValue("METHOD")
-            if method not in (
-                "PUBLISH",
-                "REQUEST",
-                "REPLY",
-                "ADD",
-                "CANCEL",
-                "DECLINE_COUNTER",
-            ):
-                log.info("Could not do server-to-imip method: %s" % (method,))
-                for recipient in self.recipients:
-                    err = HTTPError(ErrorResponse(
-                        responsecode.FORBIDDEN,
-                        (caldav_namespace, "recipient-failed"),
-                        "iMIP method not allowed: %s" % (method,),
-                    ))
-                    self.responses.add(
-                        recipient.cuaddr,
-                        Failure(exc_value=err),
-                        reqstatus=iTIPRequestStatus.NO_USER_SUPPORT
-                    )
-                returnValue(None)
-
-            caldata = str(self.scheduler.calendar)
-
-            for recipient in self.recipients:
-                try:
-                    toAddr = str(recipient.cuaddr)
-                    if not toAddr.lower().startswith("mailto:"):
-                        raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP operation." % (toAddr,))
-
-                    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)
-
-                except Exception, e:
-                    # Generated failed response for this recipient
-                    log.debug("iMIP request %s failed for recipient %s: %s" % (self, recipient, e))
-                    failForRecipient(recipient)
-
-                else:
-                    self.responses.add(
-                        recipient.cuaddr,
-                        responsecode.OK,
-                        reqstatus=iTIPRequestStatus.MESSAGE_SENT
-                    )
-
-        except Exception, e:
-            # Generated failed responses for each recipient
-            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

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/delivery.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/delivery.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/delivery.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,150 @@
+# -*- test-case-name: twistedcaldav.scheduling.test.test_imip -*-
+##
+# Copyright (c) 2005-2012 Apple Inc. All 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.
+##
+
+"""
+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.http import HTTPError
+from twisted.web import client
+
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.util import AuthorizedHTTPGetter
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from twext.internet.gaiendpoint import GAIEndpoint
+from twext.internet.adaptendpoint import connect
+
+
+__all__ = [
+    "ScheduleViaIMip",
+]
+
+log = Logger()
+
+class ScheduleViaIMip(DeliveryService):
+
+    @classmethod
+    def serviceType(cls):
+        return DeliveryService.serviceType_imip
+
+
+    @inlineCallbacks
+    def generateSchedulingResponses(self):
+        def failForRecipient(recipient):
+            err = HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (caldav_namespace, "recipient-failed"),
+                "iMIP request failed",
+            ))
+            self.responses.add(
+                recipient.cuaddr,
+                Failure(exc_value=err),
+                reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE,
+                suppressErrorLog=True
+            )
+
+        # Generate an HTTP client request
+        try:
+            # We do not do freebusy requests via iMIP
+            if self.freebusy:
+                raise ValueError("iMIP VFREEBUSY requests not supported.")
+
+            method = self.scheduler.calendar.propertyValue("METHOD")
+            if method not in (
+                "PUBLISH",
+                "REQUEST",
+                "REPLY",
+                "ADD",
+                "CANCEL",
+                "DECLINE_COUNTER",
+            ):
+                log.info("Could not do server-to-imip method: %s" % (method,))
+                for recipient in self.recipients:
+                    err = HTTPError(ErrorResponse(
+                        responsecode.FORBIDDEN,
+                        (caldav_namespace, "recipient-failed"),
+                        "iMIP method not allowed: %s" % (method,),
+                    ))
+                    self.responses.add(
+                        recipient.cuaddr,
+                        Failure(exc_value=err),
+                        reqstatus=iTIPRequestStatus.NO_USER_SUPPORT
+                    )
+                returnValue(None)
+
+            caldata = str(self.scheduler.calendar)
+
+            for recipient in self.recipients:
+                try:
+                    toAddr = str(recipient.cuaddr)
+                    if not toAddr.lower().startswith("mailto:"):
+                        raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP operation." % (toAddr,))
+
+                    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)
+
+                except Exception, e:
+                    # Generated failed response for this recipient
+                    log.debug("iMIP request %s failed for recipient %s: %s" % (self, recipient, e))
+                    failForRecipient(recipient)
+
+                else:
+                    self.responses.add(
+                        recipient.cuaddr,
+                        responsecode.OK,
+                        reqstatus=iTIPRequestStatus.MESSAGE_SENT
+                    )
+
+        except Exception, e:
+            # Generated failed responses for each recipient
+            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

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/mailgateway.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/mailgateway.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/mailgateway.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,1884 +0,0 @@
-# -*- test-case-name: twistedcaldav.test.test_mail -*-
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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.
-##
-
-"""
-Mail Gateway for Calendar Server
-"""
-
-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
-
-
-__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.
-
-    SCHEMA:
-
-    Token Database:
-
-    ROW: TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP
-
-    """
-
-    dbType = "MAILGATEWAYTOKENS"
-    dbFilename = "mailgatewaytokens.sqlite"
-    dbFormatVersion = "1"
-
-
-    def __init__(self, path):
-        if path != ":memory:":
-            path = os.path.join(path, MailGatewayTokensDatabase.dbFilename)
-        super(MailGatewayTokensDatabase, self).__init__(path, True)
-
-
-    def createToken(self, organizer, attendee, icaluid, token=None):
-        if token is None:
-            token = str(uuid.uuid4())
-        self._db_execute(
-            """
-            insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
-            values (:1, :2, :3, :4, :5)
-            """, token, organizer, attendee, icaluid, datetime.date.today()
-        )
-        self._db_commit()
-        return token
-
-
-    def lookupByToken(self, token):
-        results = list(
-            self._db_execute(
-                """
-                select ORGANIZER, ATTENDEE, ICALUID from TOKENS
-                where TOKEN = :1
-                """, token
-            )
-        )
-
-        if len(results) != 1:
-            return None
-
-        return results[0]
-
-
-    def getToken(self, organizer, attendee, icaluid):
-        token = self._db_value_for_sql(
-            """
-            select TOKEN from TOKENS
-            where ORGANIZER = :1 and ATTENDEE = :2 and ICALUID = :3
-            """, organizer, attendee, icaluid
-        )
-        if token is not None:
-            # update the datestamp on the token to keep it from being purged
-            self._db_execute(
-                """
-                update TOKENS set DATESTAMP = :1 WHERE TOKEN = :2
-                """, datetime.date.today(), token
-            )
-            return str(token)
-        else:
-            return None
-
-
-    def deleteToken(self, token):
-        self._db_execute(
-            """
-            delete from TOKENS where TOKEN = :1
-            """, token
-        )
-        self._db_commit()
-
-
-    def purgeOldTokens(self, before):
-        self._db_execute(
-            """
-            delete from TOKENS where DATESTAMP < :1
-            """, before
-        )
-        self._db_commit()
-
-
-    def lowercase(self):
-        """
-        Lowercase mailto: addresses (and uppercase urn:uuid: addresses!) so
-        they can be located via normalized names.
-        """
-        rows = self._db_execute(
-            """
-            select ORGANIZER, ATTENDEE from TOKENS
-            """
-        )
-        for row in rows:
-            organizer = row[0]
-            attendee = row[1]
-            if organizer.lower().startswith("mailto:"):
-                self._db_execute(
-                    """
-                    update TOKENS set ORGANIZER = :1 WHERE ORGANIZER = :2
-                    """, organizer.lower(), organizer
-                )
-            else:
-                from txdav.base.datastore.util import normalizeUUIDOrNot
-                self._db_execute(
-                    """
-                    update TOKENS set ORGANIZER = :1 WHERE ORGANIZER = :2
-                    """, normalizeUUIDOrNot(organizer), organizer
-                )
-            # ATTENDEEs are always mailto: so unconditionally lower().
-            self._db_execute(
-                """
-                update TOKENS set ATTENDEE = :1 WHERE ATTENDEE = :2
-                """, attendee.lower(), attendee
-            )
-        self._db_commit()
-
-
-    def _db_version(self):
-        """
-        @return: the schema version assigned to this index.
-        """
-        return MailGatewayTokensDatabase.dbFormatVersion
-
-
-    def _db_type(self):
-        """
-        @return: the collection type assigned to this index.
-        """
-        return MailGatewayTokensDatabase.dbType
-
-
-    def _db_init_data_tables(self, q):
-        """
-        Initialise the underlying database tables.
-        @param q:           a database cursor to use.
-        """
-
-        #
-        # TOKENS table
-        #
-        q.execute(
-            """
-            create table TOKENS (
-                TOKEN       text,
-                ORGANIZER   text,
-                ATTENDEE    text,
-                ICALUID     text,
-                DATESTAMP   date
-            )
-            """
-        )
-        q.execute(
-            """
-            create index TOKENSINDEX on TOKENS (TOKEN)
-            """
-        )
-
-
-    def _db_upgrade_data_tables(self, q, old_version):
-        """
-        Upgrade the data from an older version of the DB.
-        @param q: a database cursor to use.
-        @param old_version: existing DB's version number
-        @type old_version: str
-        """
-        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):
-    """
-    ISchedule Inbox
-    """
-
-    def __init__(self, settings, mailer):
-        service.MultiService.__init__(self)
-        self.settings = settings
-        self.mailer = mailer
-
-        # 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)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/mailgateway.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/mailgateway.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/mailgateway.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/mailgateway.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,1884 @@
+# -*- test-case-name: twistedcaldav.test.test_mail -*-
+##
+# Copyright (c) 2005-2012 Apple Inc. All 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.
+##
+
+"""
+Mail Gateway for Calendar Server
+"""
+
+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
+
+
+__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.
+
+    SCHEMA:
+
+    Token Database:
+
+    ROW: TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP
+
+    """
+
+    dbType = "MAILGATEWAYTOKENS"
+    dbFilename = "mailgatewaytokens.sqlite"
+    dbFormatVersion = "1"
+
+
+    def __init__(self, path):
+        if path != ":memory:":
+            path = os.path.join(path, MailGatewayTokensDatabase.dbFilename)
+        super(MailGatewayTokensDatabase, self).__init__(path, True)
+
+
+    def createToken(self, organizer, attendee, icaluid, token=None):
+        if token is None:
+            token = str(uuid.uuid4())
+        self._db_execute(
+            """
+            insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
+            values (:1, :2, :3, :4, :5)
+            """, token, organizer, attendee, icaluid, datetime.date.today()
+        )
+        self._db_commit()
+        return token
+
+
+    def lookupByToken(self, token):
+        results = list(
+            self._db_execute(
+                """
+                select ORGANIZER, ATTENDEE, ICALUID from TOKENS
+                where TOKEN = :1
+                """, token
+            )
+        )
+
+        if len(results) != 1:
+            return None
+
+        return results[0]
+
+
+    def getToken(self, organizer, attendee, icaluid):
+        token = self._db_value_for_sql(
+            """
+            select TOKEN from TOKENS
+            where ORGANIZER = :1 and ATTENDEE = :2 and ICALUID = :3
+            """, organizer, attendee, icaluid
+        )
+        if token is not None:
+            # update the datestamp on the token to keep it from being purged
+            self._db_execute(
+                """
+                update TOKENS set DATESTAMP = :1 WHERE TOKEN = :2
+                """, datetime.date.today(), token
+            )
+            return str(token)
+        else:
+            return None
+
+
+    def deleteToken(self, token):
+        self._db_execute(
+            """
+            delete from TOKENS where TOKEN = :1
+            """, token
+        )
+        self._db_commit()
+
+
+    def purgeOldTokens(self, before):
+        self._db_execute(
+            """
+            delete from TOKENS where DATESTAMP < :1
+            """, before
+        )
+        self._db_commit()
+
+
+    def lowercase(self):
+        """
+        Lowercase mailto: addresses (and uppercase urn:uuid: addresses!) so
+        they can be located via normalized names.
+        """
+        rows = self._db_execute(
+            """
+            select ORGANIZER, ATTENDEE from TOKENS
+            """
+        )
+        for row in rows:
+            organizer = row[0]
+            attendee = row[1]
+            if organizer.lower().startswith("mailto:"):
+                self._db_execute(
+                    """
+                    update TOKENS set ORGANIZER = :1 WHERE ORGANIZER = :2
+                    """, organizer.lower(), organizer
+                )
+            else:
+                from txdav.base.datastore.util import normalizeUUIDOrNot
+                self._db_execute(
+                    """
+                    update TOKENS set ORGANIZER = :1 WHERE ORGANIZER = :2
+                    """, normalizeUUIDOrNot(organizer), organizer
+                )
+            # ATTENDEEs are always mailto: so unconditionally lower().
+            self._db_execute(
+                """
+                update TOKENS set ATTENDEE = :1 WHERE ATTENDEE = :2
+                """, attendee.lower(), attendee
+            )
+        self._db_commit()
+
+
+    def _db_version(self):
+        """
+        @return: the schema version assigned to this index.
+        """
+        return MailGatewayTokensDatabase.dbFormatVersion
+
+
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this index.
+        """
+        return MailGatewayTokensDatabase.dbType
+
+
+    def _db_init_data_tables(self, q):
+        """
+        Initialise the underlying database tables.
+        @param q:           a database cursor to use.
+        """
+
+        #
+        # TOKENS table
+        #
+        q.execute(
+            """
+            create table TOKENS (
+                TOKEN       text,
+                ORGANIZER   text,
+                ATTENDEE    text,
+                ICALUID     text,
+                DATESTAMP   date
+            )
+            """
+        )
+        q.execute(
+            """
+            create index TOKENSINDEX on TOKENS (TOKEN)
+            """
+        )
+
+
+    def _db_upgrade_data_tables(self, q, old_version):
+        """
+        Upgrade the data from an older version of the DB.
+        @param q: a database cursor to use.
+        @param old_version: existing DB's version number
+        @type old_version: str
+        """
+        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):
+    """
+    ISchedule Inbox
+    """
+
+    def __init__(self, settings, mailer):
+        service.MultiService.__init__(self)
+        self.settings = settings
+        self.mailer = mailer
+
+        # 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)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/resource.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,223 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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.python.log import Logger
-from twext.web2 import responsecode
-from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.http import Response, HTTPError
-from twext.web2.http_headers import MimeType
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue
-from twistedcaldav import caldavxml
-from twistedcaldav.config import config
-from twistedcaldav.directory.util import transactionFromRequest
-from twistedcaldav.ical import Component
-from twistedcaldav.localization import getLanguage
-from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.scheduling.caldav.resource import deliverSchedulePrivilegeSet
-from twistedcaldav.scheduling.imip.scheduler import IMIPScheduler
-from txdav.xml import element as davxml
-
-__all__ = [
-    "IMIPInboxResource",
-    "IMIPReplyInboxResource",
-    "IMIPInvitationInboxResource",
-]
-
-log = Logger()
-
-class IMIPInboxResource(CalDAVResource):
-    """
-    IMIP-delivery Inbox resource.
-
-    Extends L{DAVResource} to provide IMIP delivery functionality.
-    """
-
-    def __init__(self, parent, store):
-        """
-        @param parent: the parent resource of this one.
-        @param store: the store to use for transactions.
-        """
-        assert parent is not None
-
-        CalDAVResource.__init__(
-            self, principalCollections=parent.principalCollections()
-        )
-
-        self.parent = parent
-        self._newStore = store
-
-
-    def accessControlList(self, request, inheritance=True,
-        expanding=False, inherited_aces=None):
-
-        if not hasattr(self, "iMIPACL"):
-            guid = config.Scheduling.iMIP.GUID
-            self.iMIPACL = davxml.ACL(
-                davxml.ACE(
-                    davxml.Principal(
-                        davxml.HRef.fromString("/principals/__uids__/%s/"
-                                               % (guid,))
-                    ),
-                    davxml.Grant(
-                        davxml.Privilege(caldavxml.ScheduleDeliver()),
-                    ),
-                ),
-            )
-
-        return succeed(self.iMIPACL)
-
-
-    def resourceType(self):
-        return davxml.ResourceType.ischeduleinbox
-
-
-    def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8")
-
-
-    def isCollection(self):
-        return False
-
-
-    def isCalendarCollection(self):
-        return False
-
-
-    def isPseudoCalendarCollection(self):
-        return False
-
-
-    def deadProperties(self):
-        if not hasattr(self, "_dead_properties"):
-            self._dead_properties = NonePropertyStore(self)
-        return self._dead_properties
-
-
-    def etag(self):
-        return succeed(None)
-
-
-    def checkPreconditions(self, request):
-        return None
-
-
-    def render(self, request):
-        output = """<html>
-<head>
-<title>IMIP Delivery Resource</title>
-</head>
-<body>
-<h1>IMIP Delivery Resource.</h1>
-</body
-</html>"""
-
-        response = Response(200, {}, output)
-        response.headers.setHeader("content-type", MimeType("text", "html"))
-        return response
-
-    ##
-    # File
-    ##
-
-
-    def createSimilarFile(self, path):
-        log.err("Attempt to create clone %r of resource %r" % (path, self))
-        raise HTTPError(responsecode.NOT_FOUND)
-
-    ##
-    # ACL
-    ##
-
-
-    def defaultAccessControlList(self):
-        privs = (
-            davxml.Privilege(davxml.Read()),
-            davxml.Privilege(caldavxml.ScheduleDeliver()),
-        )
-        if config.Scheduling.CalDAV.OldDraftCompatibility:
-            privs += (davxml.Privilege(caldavxml.Schedule()),)
-        return davxml.ACL(
-            # DAV:Read, CalDAV:schedule-deliver for all principals (includes
-            # anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(*privs),
-                davxml.Protected(),
-            ),
-        )
-
-
-    def supportedPrivileges(self, request):
-        return succeed(deliverSchedulePrivilegeSet)
-
-
-
-class IMIPReplyInboxResource(IMIPInboxResource):
-
-    def renderHTTP(self, request):
-        """
-        Set up a transaction which will be used and committed by implicit
-        scheduling.
-        """
-        self.transaction = transactionFromRequest(request, self._newStore)
-        return super(IMIPReplyInboxResource, self).renderHTTP(request, self.transaction)
-
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The IMIP reply POST method (inbound)
-        """
-
-        # Check authentication and access controls
-        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
-
-        # Inject using the IMIPScheduler.
-        scheduler = IMIPScheduler(request, self)
-
-        # Do the POST processing treating this as a non-local schedule
-        result = (yield scheduler.doSchedulingViaPOST(self.transaction, use_request_headers=True))
-        returnValue(result.response())
-
-
-
-class IMIPInvitationInboxResource(IMIPInboxResource):
-
-    def __init__(self, parent, store, mailer):
-        super(IMIPInvitationInboxResource, self).__init__(parent, store)
-        self.mailer = mailer
-
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The IMIP invitation POST method (outbound)
-        """
-
-        # Check authentication and access controls
-        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
-
-        # Compute token, add to db, generate email and send it
-        calendar = (yield Component.fromIStream(request.stream))
-        originator = request.headers.getRawHeaders("originator")[0]
-        recipient = request.headers.getRawHeaders("recipient")[0]
-        language = getLanguage(config)
-
-        if not (yield self.mailer.outbound(originator,
-            recipient, calendar, language=language)):
-            returnValue(Response(code=responsecode.BAD_REQUEST))
-
-        returnValue(Response(code=responsecode.OK))

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/resource.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/resource.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/resource.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,223 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All 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.python.log import Logger
+from twext.web2 import responsecode
+from twext.web2.dav.noneprops import NonePropertyStore
+from twext.web2.http import Response, HTTPError
+from twext.web2.http_headers import MimeType
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
+from twistedcaldav import caldavxml
+from twistedcaldav.config import config
+from twistedcaldav.directory.util import transactionFromRequest
+from twistedcaldav.ical import Component
+from twistedcaldav.localization import getLanguage
+from twistedcaldav.resource import CalDAVResource
+from twistedcaldav.scheduling.caldav.resource import deliverSchedulePrivilegeSet
+from twistedcaldav.scheduling.imip.scheduler import IMIPScheduler
+from txdav.xml import element as davxml
+
+__all__ = [
+    "IMIPInboxResource",
+    "IMIPReplyInboxResource",
+    "IMIPInvitationInboxResource",
+]
+
+log = Logger()
+
+class IMIPInboxResource(CalDAVResource):
+    """
+    IMIP-delivery Inbox resource.
+
+    Extends L{DAVResource} to provide IMIP delivery functionality.
+    """
+
+    def __init__(self, parent, store):
+        """
+        @param parent: the parent resource of this one.
+        @param store: the store to use for transactions.
+        """
+        assert parent is not None
+
+        CalDAVResource.__init__(
+            self, principalCollections=parent.principalCollections()
+        )
+
+        self.parent = parent
+        self._newStore = store
+
+
+    def accessControlList(self, request, inheritance=True,
+        expanding=False, inherited_aces=None):
+
+        if not hasattr(self, "iMIPACL"):
+            guid = config.Scheduling.iMIP.GUID
+            self.iMIPACL = davxml.ACL(
+                davxml.ACE(
+                    davxml.Principal(
+                        davxml.HRef.fromString("/principals/__uids__/%s/"
+                                               % (guid,))
+                    ),
+                    davxml.Grant(
+                        davxml.Privilege(caldavxml.ScheduleDeliver()),
+                    ),
+                ),
+            )
+
+        return succeed(self.iMIPACL)
+
+
+    def resourceType(self):
+        return davxml.ResourceType.ischeduleinbox
+
+
+    def contentType(self):
+        return MimeType.fromString("text/html; charset=utf-8")
+
+
+    def isCollection(self):
+        return False
+
+
+    def isCalendarCollection(self):
+        return False
+
+
+    def isPseudoCalendarCollection(self):
+        return False
+
+
+    def deadProperties(self):
+        if not hasattr(self, "_dead_properties"):
+            self._dead_properties = NonePropertyStore(self)
+        return self._dead_properties
+
+
+    def etag(self):
+        return succeed(None)
+
+
+    def checkPreconditions(self, request):
+        return None
+
+
+    def render(self, request):
+        output = """<html>
+<head>
+<title>IMIP Delivery Resource</title>
+</head>
+<body>
+<h1>IMIP Delivery Resource.</h1>
+</body
+</html>"""
+
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
+
+    ##
+    # File
+    ##
+
+
+    def createSimilarFile(self, path):
+        log.err("Attempt to create clone %r of resource %r" % (path, self))
+        raise HTTPError(responsecode.NOT_FOUND)
+
+    ##
+    # ACL
+    ##
+
+
+    def defaultAccessControlList(self):
+        privs = (
+            davxml.Privilege(davxml.Read()),
+            davxml.Privilege(caldavxml.ScheduleDeliver()),
+        )
+        if config.Scheduling.CalDAV.OldDraftCompatibility:
+            privs += (davxml.Privilege(caldavxml.Schedule()),)
+        return davxml.ACL(
+            # DAV:Read, CalDAV:schedule-deliver for all principals (includes
+            # anonymous)
+            davxml.ACE(
+                davxml.Principal(davxml.All()),
+                davxml.Grant(*privs),
+                davxml.Protected(),
+            ),
+        )
+
+
+    def supportedPrivileges(self, request):
+        return succeed(deliverSchedulePrivilegeSet)
+
+
+
+class IMIPReplyInboxResource(IMIPInboxResource):
+
+    def renderHTTP(self, request):
+        """
+        Set up a transaction which will be used and committed by implicit
+        scheduling.
+        """
+        self.transaction = transactionFromRequest(request, self._newStore)
+        return super(IMIPReplyInboxResource, self).renderHTTP(request, self.transaction)
+
+
+    @inlineCallbacks
+    def http_POST(self, request):
+        """
+        The IMIP reply POST method (inbound)
+        """
+
+        # Check authentication and access controls
+        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
+
+        # Inject using the IMIPScheduler.
+        scheduler = IMIPScheduler(request, self)
+
+        # Do the POST processing treating this as a non-local schedule
+        result = (yield scheduler.doSchedulingViaPOST(self.transaction, use_request_headers=True))
+        returnValue(result.response())
+
+
+
+class IMIPInvitationInboxResource(IMIPInboxResource):
+
+    def __init__(self, parent, store, mailer):
+        super(IMIPInvitationInboxResource, self).__init__(parent, store)
+        self.mailer = mailer
+
+
+    @inlineCallbacks
+    def http_POST(self, request):
+        """
+        The IMIP invitation POST method (outbound)
+        """
+
+        # Check authentication and access controls
+        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
+
+        # Compute token, add to db, generate email and send it
+        calendar = (yield Component.fromIStream(request.stream))
+        originator = request.headers.getRawHeaders("originator")[0]
+        recipient = request.headers.getRawHeaders("recipient")[0]
+        language = getLanguage(config)
+
+        if not (yield self.mailer.outbound(originator,
+            recipient, calendar, language=language)):
+            returnValue(Response(code=responsecode.BAD_REQUEST))
+
+        returnValue(Response(code=responsecode.OK))

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/scheduler.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/scheduler.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/scheduler.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,112 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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.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 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
-
-
-"""
-L{IMIPScheduler} - handles deliveries for scheduling messages being POSTed to the iMIP inbox.
-"""
-
-__all__ = [
-    "IMIPScheduler",
-]
-
-log = Logger()
-
-class IMIPScheduler(RemoteScheduler):
-
-    scheduleResponse = ScheduleResponseQueue
-
-    errorResponse = ErrorResponse
-
-    errorElements = {
-        "originator-missing": (caldav_namespace, "originator-specified"),
-        "originator-invalid": (caldav_namespace, "originator-allowed"),
-        "originator-denied": (caldav_namespace, "originator-allowed"),
-        "recipient-missing": (caldav_namespace, "recipient-specified"),
-        "recipient-invalid": (caldav_namespace, "recipient-exists"),
-        "organizer-denied": (caldav_namespace, "organizer-allowed"),
-        "attendee-denied": (caldav_namespace, "attendee-allowed"),
-        "invalid-calendar-data-type": (caldav_namespace, "supported-calendar-data"),
-        "invalid-calendar-data": (caldav_namespace, "valid-calendar-data"),
-        "invalid-scheduling-message": (caldav_namespace, "valid-calendar-data"),
-        "max-recipients": (caldav_namespace, "recipient-limit"),
-    }
-
-    def checkAuthorization(self):
-        pass
-
-
-    @inlineCallbacks
-    def checkOriginator(self):
-        """
-        Check the validity of the Originator header.
-        """
-
-        # For remote requests we do not allow the originator to be a local user or one within our domain.
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
-        if originatorPrincipal or localUser:
-            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
-
-
-    def checkAttendeeAsOriginator(self):
-        pass
-
-
-    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",
-            ))

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/scheduler.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/scheduler.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/scheduler.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/scheduler.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,112 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All 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.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 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
+
+
+"""
+L{IMIPScheduler} - handles deliveries for scheduling messages being POSTed to the iMIP inbox.
+"""
+
+__all__ = [
+    "IMIPScheduler",
+]
+
+log = Logger()
+
+class IMIPScheduler(RemoteScheduler):
+
+    scheduleResponse = ScheduleResponseQueue
+
+    errorResponse = ErrorResponse
+
+    errorElements = {
+        "originator-missing": (caldav_namespace, "originator-specified"),
+        "originator-invalid": (caldav_namespace, "originator-allowed"),
+        "originator-denied": (caldav_namespace, "originator-allowed"),
+        "recipient-missing": (caldav_namespace, "recipient-specified"),
+        "recipient-invalid": (caldav_namespace, "recipient-exists"),
+        "organizer-denied": (caldav_namespace, "organizer-allowed"),
+        "attendee-denied": (caldav_namespace, "attendee-allowed"),
+        "invalid-calendar-data-type": (caldav_namespace, "supported-calendar-data"),
+        "invalid-calendar-data": (caldav_namespace, "valid-calendar-data"),
+        "invalid-scheduling-message": (caldav_namespace, "valid-calendar-data"),
+        "max-recipients": (caldav_namespace, "recipient-limit"),
+    }
+
+    def checkAuthorization(self):
+        pass
+
+
+    @inlineCallbacks
+    def checkOriginator(self):
+        """
+        Check the validity of the Originator header.
+        """
+
+        # For remote requests we do not allow the originator to be a local user or one within our domain.
+        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
+        if originatorPrincipal or localUser:
+            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
+
+
+    def checkAttendeeAsOriginator(self):
+        pass
+
+
+    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",
+            ))

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/__init__.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/__init__.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,108 +0,0 @@
-Return-path: <>
-Received: from elderberry.example.com ([17.128.115.181])
- by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
- 14 2008; 32bit)) with ESMTP id <0K5900HRN6UVQR40 at mail4.example.com> for
- xyzzy at example.com; Thu, 07 Aug 2008 16:06:31 -0700 (PDT)
-Original-recipient: rfc822;xyzzy at example.com
-Received: from relay12.example.com ([17.128.113.53])
- by elderberry.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb
- 28 2007)) with ESMTP id <0K5900H5D6UVQN60 at elderberry.example.com> for
- xyzzy at example.com (ORCPT xyzzy at example.com); Thu, 07 Aug 2008 16:06:31 -0700 (PDT)
-Received: from relay12.example.com (unknown [127.0.0.1])
-	by relay12.example.com (Symantec Mail Security) with ESMTP id E082C464003	for
- <xyzzy at example.com>; Thu, 07 Aug 2008 16:06:30 -0700 (PDT)
-Received: from mail-out4.example.com (mail-out4.example.com [17.254.13.23])
-	(using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
-	(No client certificate requested)	by relay12.example.com (example SCV relay)
- with ESMTP id B2942420006	for <xyzzy at example.com>; Thu,
- 07 Aug 2008 16:06:30 -0700 (PDT)
-Received: by mail-out4.example.com (Postfix)	id A18D2374C3E7; Thu,
- 07 Aug 2008 16:06:30 -0700 (PDT)
-Date: Thu, 07 Aug 2008 16:06:30 -0700 (PDT)
-From: MAILER-DAEMON at mail-out4.example.com (Mail Delivery System)
-Subject: Undelivered Mail Returned to Sender
-To: xyzzy at example.com
-Message-id: <20080807230630.A18D2374C3E7 at mail-out4.example.com>
-Auto-submitted: auto-replied
-MIME-version: 1.0
-Content-type: multipart/report; report-type=delivery-status;
- boundary="4B6C6374C3E5.1218150390/mail-out4.example.com"
-X-AuditID: 11807135-a7df5bb000001321-97-489b7ff6fce0
-X-Brightmail-Tracker: AAAAAA==
-
-This is a MIME-encapsulated message.
-
---4B6C6374C3E5.1218150390/mail-out4.example.com
-Content-Description: Notification
-Content-Type: text/plain; charset=us-ascii
-
-This is the mail system at host mail-out4.example.com.
-
-I'm sorry to have to inform you that your message could not
-be delivered to one or more recipients. It's attached below.
-
-For further assistance, please send mail to postmaster.
-
-If you do so, please include this problem report. You can
-delete your own text from the attached returned message.
-
-                   The mail system
-
-<thisdoesnotexist at example.com>: host
-    example-smtp-in.l.example.com[209.85.201.27] said: 550-5.1.1 The email account
-    that you tried to reach does not exist. Please 550-5.1.1 try
-    double-checking the recipient's email address for typos 550-5.1.1 or
-    unnecessary spaces. Learn more at                      550 5.1.1
-    http://mail.example.com/support/bin/answer.py?answer=6596 20si599639wfi.11
-    (in reply to RCPT TO command)
-
---4B6C6374C3E5.1218150390/mail-out4.example.com
-Content-Description: Delivery report
-Content-Type: message/delivery-status
-
-Reporting-MTA: dns; mail-out4.example.com
-X-Postfix-Queue-ID: 4B6C6374C3E5
-X-Postfix-Sender: rfc822; xyzzy at example.com
-Arrival-Date: Thu,  7 Aug 2008 16:06:30 -0700 (PDT)
-
-Final-Recipient: rfc822; thisdoesnotexist at example.com
-Original-Recipient: rfc822;thisdoesnotexist at example.com
-Action: failed
-Status: 5.1.1
-Remote-MTA: dns; example-smtp-in.l.example.com
-Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does
-    not exist. Please 550-5.1.1 try double-checking the recipient's email
-    address for typos 550-5.1.1 or unnecessary spaces. Learn more at
-    550 5.1.1 http://mail.example.com/support/bin/answer.py?answer=6596
-    20si599639wfi.11
-
---4B6C6374C3E5.1218150390/mail-out4.example.com
-Content-Description: Undelivered Message
-Content-Type: message/rfc822
-
-Received: from relay13.example.com (relay13.example.com [17.128.113.29])
-	by mail-out4.example.com (Postfix) with ESMTP id 4B6C6374C3E5
-	for <thisdoesnotexist at example.com>; Thu,  7 Aug 2008 16:06:30 -0700 (PDT)
-Received: from relay13.example.com (unknown [127.0.0.1])
-	by relay13.example.com (Symantec Mail Security) with ESMTP id 347CD280A3
-	for <thisdoesnotexist at example.com>; Thu,  7 Aug 2008 16:06:30 -0700 (PDT)
-X-AuditID: 1180711d-a2ff7bb000000ece-13-489b7ff6536b
-Received: from plugh.example.com (plugh.example.com [17.224.21.17])
-	(using TLSv1 with cipher AES128-SHA (128/128 bits))
-	(No client certificate requested)
-	by relay13.example.com (example SCV relay) with ESMTP id 1C5C928094
-	for <thisdoesnotexist at example.com>; Thu,  7 Aug 2008 16:06:30 -0700 (PDT)
-Message-Id: <40900559-DAB1-4956-BA87-F88E00CF5104 at example.com>
-From: xyzzy <xyzzy at example.com>
-To: thisdoesnotexist at example.com
-Content-Type: text/plain; charset=US-ASCII; format=flowed
-Content-Transfer-Encoding: 7bit
-Mime-Version: 1.0 (example Message framework v928.1)
-Subject: testing
-Date: Thu, 7 Aug 2008 16:06:29 -0700
-X-Mailer: example Mail (2.928.1)
-X-Brightmail-Tracker: AAAAAA==
-
-asdf
-
---4B6C6374C3E5.1218150390/mail-out4.example.com--

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,108 @@
+Return-path: <>
+Received: from elderberry.example.com ([17.128.115.181])
+ by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
+ 14 2008; 32bit)) with ESMTP id <0K5900HRN6UVQR40 at mail4.example.com> for
+ xyzzy at example.com; Thu, 07 Aug 2008 16:06:31 -0700 (PDT)
+Original-recipient: rfc822;xyzzy at example.com
+Received: from relay12.example.com ([17.128.113.53])
+ by elderberry.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb
+ 28 2007)) with ESMTP id <0K5900H5D6UVQN60 at elderberry.example.com> for
+ xyzzy at example.com (ORCPT xyzzy at example.com); Thu, 07 Aug 2008 16:06:31 -0700 (PDT)
+Received: from relay12.example.com (unknown [127.0.0.1])
+	by relay12.example.com (Symantec Mail Security) with ESMTP id E082C464003	for
+ <xyzzy at example.com>; Thu, 07 Aug 2008 16:06:30 -0700 (PDT)
+Received: from mail-out4.example.com (mail-out4.example.com [17.254.13.23])
+	(using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+	(No client certificate requested)	by relay12.example.com (example SCV relay)
+ with ESMTP id B2942420006	for <xyzzy at example.com>; Thu,
+ 07 Aug 2008 16:06:30 -0700 (PDT)
+Received: by mail-out4.example.com (Postfix)	id A18D2374C3E7; Thu,
+ 07 Aug 2008 16:06:30 -0700 (PDT)
+Date: Thu, 07 Aug 2008 16:06:30 -0700 (PDT)
+From: MAILER-DAEMON at mail-out4.example.com (Mail Delivery System)
+Subject: Undelivered Mail Returned to Sender
+To: xyzzy at example.com
+Message-id: <20080807230630.A18D2374C3E7 at mail-out4.example.com>
+Auto-submitted: auto-replied
+MIME-version: 1.0
+Content-type: multipart/report; report-type=delivery-status;
+ boundary="4B6C6374C3E5.1218150390/mail-out4.example.com"
+X-AuditID: 11807135-a7df5bb000001321-97-489b7ff6fce0
+X-Brightmail-Tracker: AAAAAA==
+
+This is a MIME-encapsulated message.
+
+--4B6C6374C3E5.1218150390/mail-out4.example.com
+Content-Description: Notification
+Content-Type: text/plain; charset=us-ascii
+
+This is the mail system at host mail-out4.example.com.
+
+I'm sorry to have to inform you that your message could not
+be delivered to one or more recipients. It's attached below.
+
+For further assistance, please send mail to postmaster.
+
+If you do so, please include this problem report. You can
+delete your own text from the attached returned message.
+
+                   The mail system
+
+<thisdoesnotexist at example.com>: host
+    example-smtp-in.l.example.com[209.85.201.27] said: 550-5.1.1 The email account
+    that you tried to reach does not exist. Please 550-5.1.1 try
+    double-checking the recipient's email address for typos 550-5.1.1 or
+    unnecessary spaces. Learn more at                      550 5.1.1
+    http://mail.example.com/support/bin/answer.py?answer=6596 20si599639wfi.11
+    (in reply to RCPT TO command)
+
+--4B6C6374C3E5.1218150390/mail-out4.example.com
+Content-Description: Delivery report
+Content-Type: message/delivery-status
+
+Reporting-MTA: dns; mail-out4.example.com
+X-Postfix-Queue-ID: 4B6C6374C3E5
+X-Postfix-Sender: rfc822; xyzzy at example.com
+Arrival-Date: Thu,  7 Aug 2008 16:06:30 -0700 (PDT)
+
+Final-Recipient: rfc822; thisdoesnotexist at example.com
+Original-Recipient: rfc822;thisdoesnotexist at example.com
+Action: failed
+Status: 5.1.1
+Remote-MTA: dns; example-smtp-in.l.example.com
+Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does
+    not exist. Please 550-5.1.1 try double-checking the recipient's email
+    address for typos 550-5.1.1 or unnecessary spaces. Learn more at
+    550 5.1.1 http://mail.example.com/support/bin/answer.py?answer=6596
+    20si599639wfi.11
+
+--4B6C6374C3E5.1218150390/mail-out4.example.com
+Content-Description: Undelivered Message
+Content-Type: message/rfc822
+
+Received: from relay13.example.com (relay13.example.com [17.128.113.29])
+	by mail-out4.example.com (Postfix) with ESMTP id 4B6C6374C3E5
+	for <thisdoesnotexist at example.com>; Thu,  7 Aug 2008 16:06:30 -0700 (PDT)
+Received: from relay13.example.com (unknown [127.0.0.1])
+	by relay13.example.com (Symantec Mail Security) with ESMTP id 347CD280A3
+	for <thisdoesnotexist at example.com>; Thu,  7 Aug 2008 16:06:30 -0700 (PDT)
+X-AuditID: 1180711d-a2ff7bb000000ece-13-489b7ff6536b
+Received: from plugh.example.com (plugh.example.com [17.224.21.17])
+	(using TLSv1 with cipher AES128-SHA (128/128 bits))
+	(No client certificate requested)
+	by relay13.example.com (example SCV relay) with ESMTP id 1C5C928094
+	for <thisdoesnotexist at example.com>; Thu,  7 Aug 2008 16:06:30 -0700 (PDT)
+Message-Id: <40900559-DAB1-4956-BA87-F88E00CF5104 at example.com>
+From: xyzzy <xyzzy at example.com>
+To: thisdoesnotexist at example.com
+Content-Type: text/plain; charset=US-ASCII; format=flowed
+Content-Transfer-Encoding: 7bit
+Mime-Version: 1.0 (example Message framework v928.1)
+Subject: testing
+Date: Thu, 7 Aug 2008 16:06:29 -0700
+X-Mailer: example Mail (2.928.1)
+X-Brightmail-Tracker: AAAAAA==
+
+asdf
+
+--4B6C6374C3E5.1218150390/mail-out4.example.com--

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,61 +0,0 @@
-Return-path: <>
-Received: from hemlock.example.com ([17.128.115.180])
- by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
- 14 2008; 32bit)) with ESMTP id <0K5I00K1J5OSXGB0 at mail4.example.com> for
- xyzzy at example.com; Tue, 12 Aug 2008 12:19:40 -0700 (PDT)
-Original-recipient: rfc822;xyzzy at example.com
-Received: from relay11.example.com ([17.128.113.48])
- by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
- 2007)) with ESMTP id <0K5I00KL15OSO0E0 at hemlock.example.com> for
- xyzzy at example.com (ORCPT xyzzy at example.com); Tue,
- 12 Aug 2008 12:19:40 -0700 (PDT)
-Received: by relay11.example.com (Symantec Mail Security)	id 15BA528084; Tue,
- 12 Aug 2008 12:19:40 -0700 (PDT)
-Date: Tue, 12 Aug 2008 12:19:40 -0700 (PDT)
-From: MAILER-DAEMON at relay11.example.com (Mail Delivery System)
-Subject: Undelivered Mail Returned to Sender
-To: xyzzy at example.com
-Message-id: <20080812191940.15BA528084 at relay11.example.com>
-MIME-version: 1.0
-Content-type: multipart/report; report-type=delivery-status;
- boundary="EC6672808D.1218568780/relay11.example.com"
-
-This is a MIME-encapsulated message.
-
---EC6672808D.1218568780/relay11.example.com
-Content-Description: Notification
-Content-Type: text/plain
-
-This is the Symantec Mail Security program at host relay11.example.com.
-
-I'm sorry to have to inform you that your message could not
-be delivered to one or more recipients. It's attached below.
-
-For further assistance, please send mail to <postmaster>
-
-If you do so, please include this problem report. You can
-delete your own text from the attached returned message.
-
-			The Symantec Mail Security program
-
-<nonexistant at example.com>: host elderberry.example.com[17.128.115.181] said: 550
-    5.1.1 unknown or illegal alias: nonexistant at example.com (in reply to RCPT TO
-    command)
-
---EC6672808D.1218568780/relay11.example.com
-Content-Description: Delivery report
-Content-Type: message/delivery-status
-
-Reporting-MTA: dns; relay11.example.com
-X-Symantec-Mail-Security-Queue-ID: EC6672808D
-X-Symantec-Mail-Security-Sender: rfc822; xyzzy at example.com
-Arrival-Date: Tue, 12 Aug 2008 12:19:39 -0700 (PDT)
-
-Final-Recipient: rfc822; nonexistant at example.com
-Action: failed
-Status: 5.0.0
-Diagnostic-Code: X-Symantec-Mail-Security; host
-    elderberry.example.com[17.128.115.181] said: 550 5.1.1 unknown or illegal
-    alias: nonexistant at example.com (in reply to RCPT TO command)
-
---EC6672808D.1218568780/relay11.example.com

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,61 @@
+Return-path: <>
+Received: from hemlock.example.com ([17.128.115.180])
+ by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
+ 14 2008; 32bit)) with ESMTP id <0K5I00K1J5OSXGB0 at mail4.example.com> for
+ xyzzy at example.com; Tue, 12 Aug 2008 12:19:40 -0700 (PDT)
+Original-recipient: rfc822;xyzzy at example.com
+Received: from relay11.example.com ([17.128.113.48])
+ by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
+ 2007)) with ESMTP id <0K5I00KL15OSO0E0 at hemlock.example.com> for
+ xyzzy at example.com (ORCPT xyzzy at example.com); Tue,
+ 12 Aug 2008 12:19:40 -0700 (PDT)
+Received: by relay11.example.com (Symantec Mail Security)	id 15BA528084; Tue,
+ 12 Aug 2008 12:19:40 -0700 (PDT)
+Date: Tue, 12 Aug 2008 12:19:40 -0700 (PDT)
+From: MAILER-DAEMON at relay11.example.com (Mail Delivery System)
+Subject: Undelivered Mail Returned to Sender
+To: xyzzy at example.com
+Message-id: <20080812191940.15BA528084 at relay11.example.com>
+MIME-version: 1.0
+Content-type: multipart/report; report-type=delivery-status;
+ boundary="EC6672808D.1218568780/relay11.example.com"
+
+This is a MIME-encapsulated message.
+
+--EC6672808D.1218568780/relay11.example.com
+Content-Description: Notification
+Content-Type: text/plain
+
+This is the Symantec Mail Security program at host relay11.example.com.
+
+I'm sorry to have to inform you that your message could not
+be delivered to one or more recipients. It's attached below.
+
+For further assistance, please send mail to <postmaster>
+
+If you do so, please include this problem report. You can
+delete your own text from the attached returned message.
+
+			The Symantec Mail Security program
+
+<nonexistant at example.com>: host elderberry.example.com[17.128.115.181] said: 550
+    5.1.1 unknown or illegal alias: nonexistant at example.com (in reply to RCPT TO
+    command)
+
+--EC6672808D.1218568780/relay11.example.com
+Content-Description: Delivery report
+Content-Type: message/delivery-status
+
+Reporting-MTA: dns; relay11.example.com
+X-Symantec-Mail-Security-Queue-ID: EC6672808D
+X-Symantec-Mail-Security-Sender: rfc822; xyzzy at example.com
+Arrival-Date: Tue, 12 Aug 2008 12:19:39 -0700 (PDT)
+
+Final-Recipient: rfc822; nonexistant at example.com
+Action: failed
+Status: 5.0.0
+Diagnostic-Code: X-Symantec-Mail-Security; host
+    elderberry.example.com[17.128.115.181] said: 550 5.1.1 unknown or illegal
+    alias: nonexistant at example.com (in reply to RCPT TO command)
+
+--EC6672808D.1218568780/relay11.example.com

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,144 +0,0 @@
-Return-path: <>
-Received: from hemlock.example.com ([17.128.115.180])
- by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
- 14 2008; 32bit)) with ESMTP id <0K5I00K1J5OSXGB0 at mail4.example.com> for
- xyzzy at example.com; Tue, 12 Aug 2008 12:19:40 -0700 (PDT)
-Original-recipient: rfc822;xyzzy at example.com
-Received: from relay11.example.com ([17.128.113.48])
- by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
- 2007)) with ESMTP id <0K5I00KL15OSO0E0 at hemlock.example.com> for
- xyzzy at example.com (ORCPT xyzzy at example.com); Tue,
- 12 Aug 2008 12:19:40 -0700 (PDT)
-Received: by relay11.example.com (Symantec Mail Security)	id 15BA528084; Tue,
- 12 Aug 2008 12:19:40 -0700 (PDT)
-Date: Tue, 12 Aug 2008 12:19:40 -0700 (PDT)
-From: MAILER-DAEMON at relay11.example.com (Mail Delivery System)
-Subject: Undelivered Mail Returned to Sender
-To: xyzzy at example.com
-Message-id: <20080812191940.15BA528084 at relay11.example.com>
-MIME-version: 1.0
-Content-type: multipart/report; report-type=delivery-status;
- boundary="EC6672808D.1218568780/relay11.example.com"
-
-This is a MIME-encapsulated message.
-
---EC6672808D.1218568780/relay11.example.com
-Content-Description: Notification
-Content-Type: text/plain
-
-This is the Symantec Mail Security program at host relay11.example.com.
-
-I'm sorry to have to inform you that your message could not
-be delivered to one or more recipients. It's attached below.
-
-For further assistance, please send mail to <postmaster>
-
-If you do so, please include this problem report. You can
-delete your own text from the attached returned message.
-
-			The Symantec Mail Security program
-
-<nonexistant at example.com>: host elderberry.example.com[17.128.115.181] said: 550
-    5.1.1 unknown or illegal alias: nonexistant at example.com (in reply to RCPT TO
-    command)
-
---EC6672808D.1218568780/relay11.example.com
-Content-Description: Delivery report
-Content-Type: message/delivery-status
-
-Reporting-MTA: dns; relay11.example.com
-X-Symantec-Mail-Security-Queue-ID: EC6672808D
-X-Symantec-Mail-Security-Sender: rfc822; xyzzy at example.com
-Arrival-Date: Tue, 12 Aug 2008 12:19:39 -0700 (PDT)
-
-Final-Recipient: rfc822; nonexistant at example.com
-Action: failed
-Status: 5.0.0
-Diagnostic-Code: X-Symantec-Mail-Security; host
-    elderberry.example.com[17.128.115.181] said: 550 5.1.1 unknown or illegal
-    alias: nonexistant at example.com (in reply to RCPT TO command)
-
---EC6672808D.1218568780/relay11.example.com
-Content-Description: Undelivered Message
-Content-Type: message/rfc822
-
-Received: from relay11.example.com (unknown [127.0.0.1])
-	by relay11.example.com (Symantec Mail Security) with ESMTP id EC6672808D
-	for <nonexistant at example.com>; Tue, 12 Aug 2008 12:19:39 -0700 (PDT)
-X-AuditID: 11807130-aa391bb000000ead-f9-48a1e24b510f
-Received: from plugh.example.com (plugh.example.com [17.224.21.17])
-	by relay11.example.com (example SCV relay) with SMTP id D58CA2804F
-	for <nonexistant at example.com>; Tue, 12 Aug 2008 12:19:39 -0700 (PDT)
-From: xyzzy at example.com
-Reply-To: user01 at example.com
-To: nonexistant at example.com
-Date: Tue, 12 Aug 2008 12:19:39 -0700
-Subject: Event invitation: New Event
-Message-ID: <20080812191939.51369.1538816694.0 at plugh.example.com>
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
-    boundary="17.224.21.17.269694933.51369.1218568779.859.1"
-X-Brightmail-Tracker: AAAAAA==
-
-
---17.224.21.17.269694933.51369.1218568779.859.1
-Content-Type: text/plain
-
-You've been invited to the following event:  
-
-Summary: New Event
-Organizer: User 01 <mailto:user01 at example.com>
-Starts:      Tuesday, August 12, 2008 09:45 AM (UTC)
-Ends:        Tuesday, August 12, 2008 10:45 AM (UTC)
-Duration:    1 hour
-Description: 
-
- To accept or decline this invitation, click the link below.
-
---17.224.21.17.269694933.51369.1218568779.859.1
-Content-Type: text/calendar; charset=utf-8
-Content-Transfer-Encoding: 7bit
-
-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
-
---17.224.21.17.269694933.51369.1218568779.859.1--
-
---EC6672808D.1218568780/relay11.example.com--

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,144 @@
+Return-path: <>
+Received: from hemlock.example.com ([17.128.115.180])
+ by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
+ 14 2008; 32bit)) with ESMTP id <0K5I00K1J5OSXGB0 at mail4.example.com> for
+ xyzzy at example.com; Tue, 12 Aug 2008 12:19:40 -0700 (PDT)
+Original-recipient: rfc822;xyzzy at example.com
+Received: from relay11.example.com ([17.128.113.48])
+ by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
+ 2007)) with ESMTP id <0K5I00KL15OSO0E0 at hemlock.example.com> for
+ xyzzy at example.com (ORCPT xyzzy at example.com); Tue,
+ 12 Aug 2008 12:19:40 -0700 (PDT)
+Received: by relay11.example.com (Symantec Mail Security)	id 15BA528084; Tue,
+ 12 Aug 2008 12:19:40 -0700 (PDT)
+Date: Tue, 12 Aug 2008 12:19:40 -0700 (PDT)
+From: MAILER-DAEMON at relay11.example.com (Mail Delivery System)
+Subject: Undelivered Mail Returned to Sender
+To: xyzzy at example.com
+Message-id: <20080812191940.15BA528084 at relay11.example.com>
+MIME-version: 1.0
+Content-type: multipart/report; report-type=delivery-status;
+ boundary="EC6672808D.1218568780/relay11.example.com"
+
+This is a MIME-encapsulated message.
+
+--EC6672808D.1218568780/relay11.example.com
+Content-Description: Notification
+Content-Type: text/plain
+
+This is the Symantec Mail Security program at host relay11.example.com.
+
+I'm sorry to have to inform you that your message could not
+be delivered to one or more recipients. It's attached below.
+
+For further assistance, please send mail to <postmaster>
+
+If you do so, please include this problem report. You can
+delete your own text from the attached returned message.
+
+			The Symantec Mail Security program
+
+<nonexistant at example.com>: host elderberry.example.com[17.128.115.181] said: 550
+    5.1.1 unknown or illegal alias: nonexistant at example.com (in reply to RCPT TO
+    command)
+
+--EC6672808D.1218568780/relay11.example.com
+Content-Description: Delivery report
+Content-Type: message/delivery-status
+
+Reporting-MTA: dns; relay11.example.com
+X-Symantec-Mail-Security-Queue-ID: EC6672808D
+X-Symantec-Mail-Security-Sender: rfc822; xyzzy at example.com
+Arrival-Date: Tue, 12 Aug 2008 12:19:39 -0700 (PDT)
+
+Final-Recipient: rfc822; nonexistant at example.com
+Action: failed
+Status: 5.0.0
+Diagnostic-Code: X-Symantec-Mail-Security; host
+    elderberry.example.com[17.128.115.181] said: 550 5.1.1 unknown or illegal
+    alias: nonexistant at example.com (in reply to RCPT TO command)
+
+--EC6672808D.1218568780/relay11.example.com
+Content-Description: Undelivered Message
+Content-Type: message/rfc822
+
+Received: from relay11.example.com (unknown [127.0.0.1])
+	by relay11.example.com (Symantec Mail Security) with ESMTP id EC6672808D
+	for <nonexistant at example.com>; Tue, 12 Aug 2008 12:19:39 -0700 (PDT)
+X-AuditID: 11807130-aa391bb000000ead-f9-48a1e24b510f
+Received: from plugh.example.com (plugh.example.com [17.224.21.17])
+	by relay11.example.com (example SCV relay) with SMTP id D58CA2804F
+	for <nonexistant at example.com>; Tue, 12 Aug 2008 12:19:39 -0700 (PDT)
+From: xyzzy at example.com
+Reply-To: user01 at example.com
+To: nonexistant at example.com
+Date: Tue, 12 Aug 2008 12:19:39 -0700
+Subject: Event invitation: New Event
+Message-ID: <20080812191939.51369.1538816694.0 at plugh.example.com>
+Mime-Version: 1.0
+Content-Type: multipart/mixed;
+    boundary="17.224.21.17.269694933.51369.1218568779.859.1"
+X-Brightmail-Tracker: AAAAAA==
+
+
+--17.224.21.17.269694933.51369.1218568779.859.1
+Content-Type: text/plain
+
+You've been invited to the following event:  
+
+Summary: New Event
+Organizer: User 01 <mailto:user01 at example.com>
+Starts:      Tuesday, August 12, 2008 09:45 AM (UTC)
+Ends:        Tuesday, August 12, 2008 10:45 AM (UTC)
+Duration:    1 hour
+Description: 
+
+ To accept or decline this invitation, click the link below.
+
+--17.224.21.17.269694933.51369.1218568779.859.1
+Content-Type: text/calendar; charset=utf-8
+Content-Transfer-Encoding: 7bit
+
+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
+
+--17.224.21.17.269694933.51369.1218568779.859.1--
+
+--EC6672808D.1218568780/relay11.example.com--

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/good_reply
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/good_reply	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/good_reply	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,92 +0,0 @@
-Return-path: <xyzzy at example.com>
-Received: from hemlock.example.com ([17.128.115.180])
- by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
- 14 2008; 32bit)) with ESMTP id <0K5I00I8I8G50QC0 at mail4.example.com> for
- ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com; Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Original-recipient:
- rfc822;ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
-Received: from relay14.example.com ([17.128.113.52])
- by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
- 2007)) with ESMTP id <0K5I004UB8G5UR90 at hemlock.example.com> for
- ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
- (ORCPT ical-living-on+2Bd7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com); Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Received: from relay14.example.com (unknown [127.0.0.1])
-	by relay14.example.com (Symantec Mail Security) with ESMTP id 6A5EF28087	for
- <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Received: from [17.83.208.154] (unknown [17.83.208.154])
-	(using TLSv1 with cipher AES128-SHA (128/128 bits))
-	(No client certificate requested)	by relay14.example.com (example SCV relay)
- with ESMTP id CAEE22808B	for
- <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
- 12 Aug 2008 13:19:16 -0700 (PDT)
-Date: Tue, 12 Aug 2008 13:19:14 -0700
-From: plugh xyzzy <xyzzy at example.com>
-Subject: Event accepted: New Event
-To: User 01 <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>
-Message-id: <1983F777-BE86-4B98-881E-06D938E60920 at example.com>
-MIME-version: 1.0 (example Message framework v928.1)
-X-Mailer: example Mail (2.928.1)
-Content-type: multipart/alternative; boundary=example-Mail-1--253014167
-X-Mail-Calendar-Part: Yes
-X-Brightmail-Tracker: AAAAAA==
-
-
---example-Mail-1--253014167
-Content-Type: text/plain;
-	charset=US-ASCII;
-	format=flowed;
-	delsp=yes
-Content-Transfer-Encoding: 7bit
-
-plugh xyzzy has accepted your iCal event invitation to the event: New  
-Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).
---example-Mail-1--253014167
-Content-Type: multipart/mixed;
-	boundary=example-Mail-2--253014167
-
-
---example-Mail-2--253014167
-Content-Type: text/html;
-	charset=US-ASCII
-Content-Transfer-Encoding: 7bit
-
-<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "><font face="Helvetica" size="3" style="font: 12.0px Helvetica">plugh xyzzy has accepted your iCal event invitation to the event: New Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).</font></div></div></body></html>
---example-Mail-2--253014167
-Content-Disposition: attachment;
-	filename=iCal-20080812-131911.ics
-Content-Type: text/calendar;
-	x-unix-mode=0644;
-	name="iCal-20080812-131911.ics"
-Content-Transfer-Encoding: quoted-printable
-
-BEGIN:VCALENDAR=0D=0APRODID:-//example=20Inc.//iCal=203.0//EN=0D=0A=
-CALSCALE:GREGORIAN=0D=0AVERSION:2.0=0D=0AMETHOD:REPLY=0D=0A=
-BEGIN:VTIMEZONE=0D=0ATZID:US/Pacific=0D=0ABEGIN:DAYLIGHT=0D=0A=
-TZOFFSETFROM:-0800=0D=0ATZOFFSETTO:-0700=0D=0ADTSTART:20070311T020000=0D=0A=
-RRULE:FREQ=3DYEARLY;BYMONTH=3D3;BYDAY=3D2SU=0D=0ATZNAME:PDT=0D=0A=
-END:DAYLIGHT=0D=0ABEGIN:STANDARD=0D=0ATZOFFSETFROM:-0700=0D=0A=
-TZOFFSETTO:-0800=0D=0ADTSTART:20071104T020000=0D=0A=
-RRULE:FREQ=3DYEARLY;BYMONTH=3D11;BYDAY=3D1SU=0D=0ATZNAME:PST=0D=0A=
-END:STANDARD=0D=0AEND:VTIMEZONE=0D=0ABEGIN:VEVENT=0D=0ASEQUENCE:7=0D=0A=
-DTSTART;TZID=3DUS/Pacific:20080812T100000=0D=0A=
-UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C=0D=0AORGANIZER;CN=3D"User=20=
-01":mailto:ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08=0D=0A=20=
-002fb285f at example.com=0D=0ADTSTAMP:20080812T201911Z=0D=0ASUMMARY:New=20=
-Event=0D=0A=
-ATTENDEE;CN=3D"xyzzy at example.com";CUTYPE=3DINDIVIDUAL;PARTSTAT=3DACCEPTED;RO=
-LE=3DR=0D=0A=20EQ-PARTICIPANT:mailto:xyzzy at example.com=0D=0A=
-CREATED:20080812T201906Z=0D=0ADTEND;TZID=3DUS/Pacific:20080812T110000=0D=0A=
-END:VEVENT=0D=0AEND:VCALENDAR=0D=0A=
-
---example-Mail-2--253014167
-Content-Type: text/html;
-	charset=US-ASCII
-Content-Transfer-Encoding: 7bit
-
-<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "></div></div></body></html>
---example-Mail-2--253014167--
-
---example-Mail-1--253014167--

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/good_reply (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/good_reply)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/good_reply	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/good_reply	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,92 @@
+Return-path: <xyzzy at example.com>
+Received: from hemlock.example.com ([17.128.115.180])
+ by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
+ 14 2008; 32bit)) with ESMTP id <0K5I00I8I8G50QC0 at mail4.example.com> for
+ ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com; Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Original-recipient:
+ rfc822;ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
+Received: from relay14.example.com ([17.128.113.52])
+ by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
+ 2007)) with ESMTP id <0K5I004UB8G5UR90 at hemlock.example.com> for
+ ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
+ (ORCPT ical-living-on+2Bd7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com); Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Received: from relay14.example.com (unknown [127.0.0.1])
+	by relay14.example.com (Symantec Mail Security) with ESMTP id 6A5EF28087	for
+ <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Received: from [17.83.208.154] (unknown [17.83.208.154])
+	(using TLSv1 with cipher AES128-SHA (128/128 bits))
+	(No client certificate requested)	by relay14.example.com (example SCV relay)
+ with ESMTP id CAEE22808B	for
+ <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
+ 12 Aug 2008 13:19:16 -0700 (PDT)
+Date: Tue, 12 Aug 2008 13:19:14 -0700
+From: plugh xyzzy <xyzzy at example.com>
+Subject: Event accepted: New Event
+To: User 01 <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>
+Message-id: <1983F777-BE86-4B98-881E-06D938E60920 at example.com>
+MIME-version: 1.0 (example Message framework v928.1)
+X-Mailer: example Mail (2.928.1)
+Content-type: multipart/alternative; boundary=example-Mail-1--253014167
+X-Mail-Calendar-Part: Yes
+X-Brightmail-Tracker: AAAAAA==
+
+
+--example-Mail-1--253014167
+Content-Type: text/plain;
+	charset=US-ASCII;
+	format=flowed;
+	delsp=yes
+Content-Transfer-Encoding: 7bit
+
+plugh xyzzy has accepted your iCal event invitation to the event: New  
+Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).
+--example-Mail-1--253014167
+Content-Type: multipart/mixed;
+	boundary=example-Mail-2--253014167
+
+
+--example-Mail-2--253014167
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "><font face="Helvetica" size="3" style="font: 12.0px Helvetica">plugh xyzzy has accepted your iCal event invitation to the event: New Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).</font></div></div></body></html>
+--example-Mail-2--253014167
+Content-Disposition: attachment;
+	filename=iCal-20080812-131911.ics
+Content-Type: text/calendar;
+	x-unix-mode=0644;
+	name="iCal-20080812-131911.ics"
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR=0D=0APRODID:-//example=20Inc.//iCal=203.0//EN=0D=0A=
+CALSCALE:GREGORIAN=0D=0AVERSION:2.0=0D=0AMETHOD:REPLY=0D=0A=
+BEGIN:VTIMEZONE=0D=0ATZID:US/Pacific=0D=0ABEGIN:DAYLIGHT=0D=0A=
+TZOFFSETFROM:-0800=0D=0ATZOFFSETTO:-0700=0D=0ADTSTART:20070311T020000=0D=0A=
+RRULE:FREQ=3DYEARLY;BYMONTH=3D3;BYDAY=3D2SU=0D=0ATZNAME:PDT=0D=0A=
+END:DAYLIGHT=0D=0ABEGIN:STANDARD=0D=0ATZOFFSETFROM:-0700=0D=0A=
+TZOFFSETTO:-0800=0D=0ADTSTART:20071104T020000=0D=0A=
+RRULE:FREQ=3DYEARLY;BYMONTH=3D11;BYDAY=3D1SU=0D=0ATZNAME:PST=0D=0A=
+END:STANDARD=0D=0AEND:VTIMEZONE=0D=0ABEGIN:VEVENT=0D=0ASEQUENCE:7=0D=0A=
+DTSTART;TZID=3DUS/Pacific:20080812T100000=0D=0A=
+UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C=0D=0AORGANIZER;CN=3D"User=20=
+01":mailto:ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08=0D=0A=20=
+002fb285f at example.com=0D=0ADTSTAMP:20080812T201911Z=0D=0ASUMMARY:New=20=
+Event=0D=0A=
+ATTENDEE;CN=3D"xyzzy at example.com";CUTYPE=3DINDIVIDUAL;PARTSTAT=3DACCEPTED;RO=
+LE=3DR=0D=0A=20EQ-PARTICIPANT:mailto:xyzzy at example.com=0D=0A=
+CREATED:20080812T201906Z=0D=0ADTEND;TZID=3DUS/Pacific:20080812T110000=0D=0A=
+END:VEVENT=0D=0AEND:VCALENDAR=0D=0A=
+
+--example-Mail-2--253014167
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "></div></div></body></html>
+--example-Mail-2--253014167--
+
+--example-Mail-1--253014167--

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.mo
===================================================================
(Binary files differ)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.mo (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.mo)
===================================================================
(Binary files differ)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,249 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR ORGANIZATION
-# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"POT-Creation-Date: 2008-10-24 15:03+PDT\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
-"Language-Team: LANGUAGE <LL at li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: pygettext.py 1.5\n"
-
-
-#: localization.py:171
-msgid "All day"
-msgstr ""
-
-#: localization.py:177
-msgid "%(startTime)s to %(endTime)s"
-msgstr ""
-
-#: localization.py:191
-msgid "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
-msgstr ""
-
-#: localization.py:207
-msgid "AM"
-msgstr ""
-
-#: localization.py:207
-msgid "PM"
-msgstr ""
-
-#: localization.py:213
-msgid "%(hour12Number)d:%(minuteNumber)02d %(ampm)s"
-msgstr ""
-
-#: localization.py:236
-msgid "Monday"
-msgstr ""
-
-#: localization.py:237
-msgid "Tuesday"
-msgstr ""
-
-#: localization.py:238
-msgid "Wednesday"
-msgstr ""
-
-#: localization.py:239
-msgid "Thursday"
-msgstr ""
-
-#: localization.py:240
-msgid "Friday"
-msgstr ""
-
-#: localization.py:241
-msgid "Saturday"
-msgstr ""
-
-#: localization.py:242
-msgid "Sunday"
-msgstr ""
-
-#: localization.py:246
-msgid "Mon"
-msgstr ""
-
-#: localization.py:247
-msgid "Tue"
-msgstr ""
-
-#: localization.py:248
-msgid "Wed"
-msgstr ""
-
-#: localization.py:249
-msgid "Thu"
-msgstr ""
-
-#: localization.py:250
-msgid "Fri"
-msgstr ""
-
-#: localization.py:251
-msgid "Sun"
-msgstr ""
-
-#: localization.py:252
-msgid "Sat"
-msgstr ""
-
-#: localization.py:257
-msgid "January"
-msgstr ""
-
-#: localization.py:258
-msgid "February"
-msgstr ""
-
-#: localization.py:259
-msgid "March"
-msgstr ""
-
-#: localization.py:260
-msgid "April"
-msgstr ""
-
-#: localization.py:261 localization.py:277
-msgid "May"
-msgstr ""
-
-#: localization.py:262
-msgid "June"
-msgstr ""
-
-#: localization.py:263
-msgid "July"
-msgstr ""
-
-#: localization.py:264
-msgid "August"
-msgstr ""
-
-#: localization.py:265
-msgid "September"
-msgstr ""
-
-#: localization.py:266
-msgid "October"
-msgstr ""
-
-#: localization.py:267
-msgid "November"
-msgstr ""
-
-#: localization.py:268
-msgid "December"
-msgstr ""
-
-#: localization.py:273
-msgid "Jan"
-msgstr ""
-
-#: localization.py:274
-msgid "Feb"
-msgstr ""
-
-#: localization.py:275
-msgid "Mar"
-msgstr ""
-
-#: localization.py:276
-msgid "Apr"
-msgstr ""
-
-#: localization.py:278
-msgid "Jun"
-msgstr ""
-
-#: localization.py:279
-msgid "Jul"
-msgstr ""
-
-#: localization.py:280
-msgid "Aug"
-msgstr ""
-
-#: localization.py:281
-msgid "Sep"
-msgstr ""
-
-#: localization.py:282
-msgid "Oct"
-msgstr ""
-
-#: localization.py:283
-msgid "Nov"
-msgstr ""
-
-#: localization.py:284
-msgid "Dec"
-msgstr ""
-
-#: mail.py:726 mail.py:755 mail.py:792
-msgid "Event cancelled"
-msgstr ""
-
-#: mail.py:727
-msgid "Event invitation: %(summary)s"
-msgstr ""
-
-#: mail.py:736
-msgid "Event Invitation"
-msgstr ""
-
-#: mail.py:737
-msgid "Date"
-msgstr ""
-
-#: mail.py:738
-msgid "Time"
-msgstr ""
-
-#: mail.py:739
-msgid "Description"
-msgstr ""
-
-#: mail.py:740
-msgid "Organizer"
-msgstr ""
-
-#: mail.py:741
-msgid "Attendees"
-msgstr ""
-
-#: mail.py:742
-msgid "Location"
-msgstr ""
-
-
-msgid "1 day"
-msgstr
-
-msgid "%(dayCount)d days"
-msgstr
-
-msgid "1 hour"
-
-msgid "%(hourCount)d hours"
-msgstr
-
-msgid "1 minute"
-msgstr
-
-msgid "%(minuteCount)d minutes"
-msgstr
-
-msgid "1 second"
-msgstr
-
-msgid "%(secondCount)d seconds"
-msgstr
-

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,249 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2008-10-24 15:03+PDT\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
+"Language-Team: LANGUAGE <LL at li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: localization.py:171
+msgid "All day"
+msgstr ""
+
+#: localization.py:177
+msgid "%(startTime)s to %(endTime)s"
+msgstr ""
+
+#: localization.py:191
+msgid "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
+msgstr ""
+
+#: localization.py:207
+msgid "AM"
+msgstr ""
+
+#: localization.py:207
+msgid "PM"
+msgstr ""
+
+#: localization.py:213
+msgid "%(hour12Number)d:%(minuteNumber)02d %(ampm)s"
+msgstr ""
+
+#: localization.py:236
+msgid "Monday"
+msgstr ""
+
+#: localization.py:237
+msgid "Tuesday"
+msgstr ""
+
+#: localization.py:238
+msgid "Wednesday"
+msgstr ""
+
+#: localization.py:239
+msgid "Thursday"
+msgstr ""
+
+#: localization.py:240
+msgid "Friday"
+msgstr ""
+
+#: localization.py:241
+msgid "Saturday"
+msgstr ""
+
+#: localization.py:242
+msgid "Sunday"
+msgstr ""
+
+#: localization.py:246
+msgid "Mon"
+msgstr ""
+
+#: localization.py:247
+msgid "Tue"
+msgstr ""
+
+#: localization.py:248
+msgid "Wed"
+msgstr ""
+
+#: localization.py:249
+msgid "Thu"
+msgstr ""
+
+#: localization.py:250
+msgid "Fri"
+msgstr ""
+
+#: localization.py:251
+msgid "Sun"
+msgstr ""
+
+#: localization.py:252
+msgid "Sat"
+msgstr ""
+
+#: localization.py:257
+msgid "January"
+msgstr ""
+
+#: localization.py:258
+msgid "February"
+msgstr ""
+
+#: localization.py:259
+msgid "March"
+msgstr ""
+
+#: localization.py:260
+msgid "April"
+msgstr ""
+
+#: localization.py:261 localization.py:277
+msgid "May"
+msgstr ""
+
+#: localization.py:262
+msgid "June"
+msgstr ""
+
+#: localization.py:263
+msgid "July"
+msgstr ""
+
+#: localization.py:264
+msgid "August"
+msgstr ""
+
+#: localization.py:265
+msgid "September"
+msgstr ""
+
+#: localization.py:266
+msgid "October"
+msgstr ""
+
+#: localization.py:267
+msgid "November"
+msgstr ""
+
+#: localization.py:268
+msgid "December"
+msgstr ""
+
+#: localization.py:273
+msgid "Jan"
+msgstr ""
+
+#: localization.py:274
+msgid "Feb"
+msgstr ""
+
+#: localization.py:275
+msgid "Mar"
+msgstr ""
+
+#: localization.py:276
+msgid "Apr"
+msgstr ""
+
+#: localization.py:278
+msgid "Jun"
+msgstr ""
+
+#: localization.py:279
+msgid "Jul"
+msgstr ""
+
+#: localization.py:280
+msgid "Aug"
+msgstr ""
+
+#: localization.py:281
+msgid "Sep"
+msgstr ""
+
+#: localization.py:282
+msgid "Oct"
+msgstr ""
+
+#: localization.py:283
+msgid "Nov"
+msgstr ""
+
+#: localization.py:284
+msgid "Dec"
+msgstr ""
+
+#: mail.py:726 mail.py:755 mail.py:792
+msgid "Event cancelled"
+msgstr ""
+
+#: mail.py:727
+msgid "Event invitation: %(summary)s"
+msgstr ""
+
+#: mail.py:736
+msgid "Event Invitation"
+msgstr ""
+
+#: mail.py:737
+msgid "Date"
+msgstr ""
+
+#: mail.py:738
+msgid "Time"
+msgstr ""
+
+#: mail.py:739
+msgid "Description"
+msgstr ""
+
+#: mail.py:740
+msgid "Organizer"
+msgstr ""
+
+#: mail.py:741
+msgid "Attendees"
+msgstr ""
+
+#: mail.py:742
+msgid "Location"
+msgstr ""
+
+
+msgid "1 day"
+msgstr
+
+msgid "%(dayCount)d days"
+msgstr
+
+msgid "1 hour"
+
+msgid "%(hourCount)d hours"
+msgstr
+
+msgid "1 minute"
+msgstr
+
+msgid "%(minuteCount)d minutes"
+msgstr
+
+msgid "1 second"
+msgstr
+
+msgid "%(secondCount)d seconds"
+msgstr
+

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.mo
===================================================================
(Binary files differ)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.mo (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.mo)
===================================================================
(Binary files differ)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,250 +0,0 @@
-# Pig Latin Translation
-# Copyright (C) YEAR ORGANIZATION
-# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"POT-Creation-Date: 2008-10-24 15:03+PDT\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
-"Language-Team: LANGUAGE <LL at li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: pygettext.py 1.5\n"
-
-
-#: localization.py:171
-msgid "All day"
-msgstr "Allway ayday"
-
-#: localization.py:177
-msgid "%(startTime)s to %(endTime)s"
-msgstr "%(startTime)s otay %(endTime)s"
-
-#: localization.py:191
-msgid "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
-msgstr "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
-
-#: localization.py:207
-msgid "AM"
-msgstr "AMWAY"
-
-#: localization.py:207
-msgid "PM"
-msgstr "PMAY"
-
-#: localization.py:213
-msgid "%(hour12Number)d:%(minuteNumber)02d %(ampm)s"
-msgstr "%(hour24Number)02d:%(minuteNumber)02d"
-
-#: localization.py:236
-msgid "Monday"
-msgstr "Ondaymay"
-
-#: localization.py:237
-msgid "Tuesday"
-msgstr "Uesdaytay"
-
-#: localization.py:238
-msgid "Wednesday"
-msgstr "Ednesdayway"
-
-#: localization.py:239
-msgid "Thursday"
-msgstr "Ursdaythay"
-
-#: localization.py:240
-msgid "Friday"
-msgstr "Idayfray"
-
-#: localization.py:241
-msgid "Saturday"
-msgstr "Aturdaysay"
-
-#: localization.py:242
-msgid "Sunday"
-msgstr "Undaysay"
-
-#: localization.py:246
-msgid "Mon"
-msgstr ""
-
-#: localization.py:247
-msgid "Tue"
-msgstr ""
-
-#: localization.py:248
-msgid "Wed"
-msgstr ""
-
-#: localization.py:249
-msgid "Thu"
-msgstr ""
-
-#: localization.py:250
-msgid "Fri"
-msgstr ""
-
-#: localization.py:251
-msgid "Sun"
-msgstr ""
-
-#: localization.py:252
-msgid "Sat"
-msgstr ""
-
-#: localization.py:257
-msgid "January"
-msgstr "Anuaryjay"
-
-#: localization.py:258
-msgid "February"
-msgstr "Ebruaryfay"
-
-#: localization.py:259
-msgid "March"
-msgstr "Archmay"
-
-#: localization.py:260
-msgid "April"
-msgstr "Aprilway"
-
-#: localization.py:261 localization.py:277
-msgid "May"
-msgstr "Aymay"
-
-#: localization.py:262
-msgid "June"
-msgstr "Unejay"
-
-#: localization.py:263
-msgid "July"
-msgstr "Ulyjay"
-
-#: localization.py:264
-msgid "August"
-msgstr "Augustway"
-
-#: localization.py:265
-msgid "September"
-msgstr "Eptembersay"
-
-#: localization.py:266
-msgid "October"
-msgstr "Octoberway"
-
-#: localization.py:267
-msgid "November"
-msgstr "Ovembernay"
-
-#: localization.py:268
-msgid "December"
-msgstr "Ecemberday"
-
-#: localization.py:273
-msgid "JAN"
-msgstr "ANJAY"
-
-#: localization.py:274
-msgid "Feb"
-msgstr ""
-
-#: localization.py:275
-msgid "Mar"
-msgstr ""
-
-#: localization.py:276
-msgid "Apr"
-msgstr ""
-
-#: localization.py:278
-msgid "Jun"
-msgstr ""
-
-#: localization.py:279
-msgid "Jul"
-msgstr ""
-
-#: localization.py:280
-msgid "Aug"
-msgstr ""
-
-#: localization.py:281
-msgid "Sep"
-msgstr ""
-
-#: localization.py:282
-msgid "Oct"
-msgstr ""
-
-#: localization.py:283
-msgid "Nov"
-msgstr ""
-
-#: localization.py:284
-msgid "Dec"
-msgstr ""
-
-#: mail.py:726 mail.py:755 mail.py:792
-msgid "Event cancelled"
-msgstr "Eventway ancelledcay"
-
-#: mail.py:727
-msgid "Event invitation: %(summary)s"
-msgstr "Eventway invitationway: %(summary)s"
-
-#: mail.py:736
-msgid "Event Invitation"
-msgstr "Eventway invitationway"
-
-#: mail.py:737
-msgid "Date"
-msgstr "Ateday"
-
-#: mail.py:738
-msgid "Time"
-msgstr "Imetay"
-
-#: mail.py:739
-msgid "Description"
-msgstr "Escriptionday"
-
-#: mail.py:740
-msgid "Organizer"
-msgstr "Organizerway"
-
-#: mail.py:741
-msgid "Attendees"
-msgstr "Attendeesway"
-
-#: mail.py:742
-msgid "Location"
-msgstr "Ocationlay"
-
-
-msgid "1 day"
-msgstr "1 ayday"
-
-msgid "%(dayCount)d days"
-msgstr "%(dayCount)d aysday"
-
-msgid "1 hour"
-msgstr "1 ourhay"
-
-msgid "%(hourCount)d hours"
-msgstr "%(hourCount)d ourshay"
-
-msgid "1 minute"
-msgstr "1 inutemay"
-
-msgid "%(minuteCount)d minutes"
-msgstr "%(minuteCount)d inutesmay"
-
-msgid "1 second"
-msgstr "1 econdsay"
-
-msgid "%(secondCount)d seconds"
-msgstr "%(secondCount)d econdsay"
-

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,250 @@
+# Pig Latin Translation
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2008-10-24 15:03+PDT\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
+"Language-Team: LANGUAGE <LL at li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: localization.py:171
+msgid "All day"
+msgstr "Allway ayday"
+
+#: localization.py:177
+msgid "%(startTime)s to %(endTime)s"
+msgstr "%(startTime)s otay %(endTime)s"
+
+#: localization.py:191
+msgid "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
+msgstr "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
+
+#: localization.py:207
+msgid "AM"
+msgstr "AMWAY"
+
+#: localization.py:207
+msgid "PM"
+msgstr "PMAY"
+
+#: localization.py:213
+msgid "%(hour12Number)d:%(minuteNumber)02d %(ampm)s"
+msgstr "%(hour24Number)02d:%(minuteNumber)02d"
+
+#: localization.py:236
+msgid "Monday"
+msgstr "Ondaymay"
+
+#: localization.py:237
+msgid "Tuesday"
+msgstr "Uesdaytay"
+
+#: localization.py:238
+msgid "Wednesday"
+msgstr "Ednesdayway"
+
+#: localization.py:239
+msgid "Thursday"
+msgstr "Ursdaythay"
+
+#: localization.py:240
+msgid "Friday"
+msgstr "Idayfray"
+
+#: localization.py:241
+msgid "Saturday"
+msgstr "Aturdaysay"
+
+#: localization.py:242
+msgid "Sunday"
+msgstr "Undaysay"
+
+#: localization.py:246
+msgid "Mon"
+msgstr ""
+
+#: localization.py:247
+msgid "Tue"
+msgstr ""
+
+#: localization.py:248
+msgid "Wed"
+msgstr ""
+
+#: localization.py:249
+msgid "Thu"
+msgstr ""
+
+#: localization.py:250
+msgid "Fri"
+msgstr ""
+
+#: localization.py:251
+msgid "Sun"
+msgstr ""
+
+#: localization.py:252
+msgid "Sat"
+msgstr ""
+
+#: localization.py:257
+msgid "January"
+msgstr "Anuaryjay"
+
+#: localization.py:258
+msgid "February"
+msgstr "Ebruaryfay"
+
+#: localization.py:259
+msgid "March"
+msgstr "Archmay"
+
+#: localization.py:260
+msgid "April"
+msgstr "Aprilway"
+
+#: localization.py:261 localization.py:277
+msgid "May"
+msgstr "Aymay"
+
+#: localization.py:262
+msgid "June"
+msgstr "Unejay"
+
+#: localization.py:263
+msgid "July"
+msgstr "Ulyjay"
+
+#: localization.py:264
+msgid "August"
+msgstr "Augustway"
+
+#: localization.py:265
+msgid "September"
+msgstr "Eptembersay"
+
+#: localization.py:266
+msgid "October"
+msgstr "Octoberway"
+
+#: localization.py:267
+msgid "November"
+msgstr "Ovembernay"
+
+#: localization.py:268
+msgid "December"
+msgstr "Ecemberday"
+
+#: localization.py:273
+msgid "JAN"
+msgstr "ANJAY"
+
+#: localization.py:274
+msgid "Feb"
+msgstr ""
+
+#: localization.py:275
+msgid "Mar"
+msgstr ""
+
+#: localization.py:276
+msgid "Apr"
+msgstr ""
+
+#: localization.py:278
+msgid "Jun"
+msgstr ""
+
+#: localization.py:279
+msgid "Jul"
+msgstr ""
+
+#: localization.py:280
+msgid "Aug"
+msgstr ""
+
+#: localization.py:281
+msgid "Sep"
+msgstr ""
+
+#: localization.py:282
+msgid "Oct"
+msgstr ""
+
+#: localization.py:283
+msgid "Nov"
+msgstr ""
+
+#: localization.py:284
+msgid "Dec"
+msgstr ""
+
+#: mail.py:726 mail.py:755 mail.py:792
+msgid "Event cancelled"
+msgstr "Eventway ancelledcay"
+
+#: mail.py:727
+msgid "Event invitation: %(summary)s"
+msgstr "Eventway invitationway: %(summary)s"
+
+#: mail.py:736
+msgid "Event Invitation"
+msgstr "Eventway invitationway"
+
+#: mail.py:737
+msgid "Date"
+msgstr "Ateday"
+
+#: mail.py:738
+msgid "Time"
+msgstr "Imetay"
+
+#: mail.py:739
+msgid "Description"
+msgstr "Escriptionday"
+
+#: mail.py:740
+msgid "Organizer"
+msgstr "Organizerway"
+
+#: mail.py:741
+msgid "Attendees"
+msgstr "Attendeesway"
+
+#: mail.py:742
+msgid "Location"
+msgstr "Ocationlay"
+
+
+msgid "1 day"
+msgstr "1 ayday"
+
+msgid "%(dayCount)d days"
+msgstr "%(dayCount)d aysday"
+
+msgid "1 hour"
+msgstr "1 ourhay"
+
+msgid "%(hourCount)d hours"
+msgstr "%(hourCount)d ourshay"
+
+msgid "1 minute"
+msgstr "1 inutemay"
+
+msgid "%(minuteCount)d minutes"
+msgstr "%(minuteCount)d inutesmay"
+
+msgid "1 second"
+msgstr "1 econdsay"
+
+msgid "%(secondCount)d seconds"
+msgstr "%(secondCount)d econdsay"
+

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,65 +0,0 @@
-Return-path: <xyzzy at example.com>
-Received: from hemlock.example.com ([17.128.115.180])
- by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
- 14 2008; 32bit)) with ESMTP id <0K5I00I8I8G50QC0 at mail4.example.com> for
- ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com; Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Original-recipient:
- rfc822;ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
-Received: from relay14.example.com ([17.128.113.52])
- by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
- 2007)) with ESMTP id <0K5I004UB8G5UR90 at hemlock.example.com> for
- ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
- (ORCPT ical-living-on+2Bd7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com); Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Received: from relay14.example.com (unknown [127.0.0.1])
-	by relay14.example.com (Symantec Mail Security) with ESMTP id 6A5EF28087	for
- <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Received: from [17.83.208.154] (unknown [17.83.208.154])
-	(using TLSv1 with cipher AES128-SHA (128/128 bits))
-	(No client certificate requested)	by relay14.example.com (example SCV relay)
- with ESMTP id CAEE22808B	for
- <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
- 12 Aug 2008 13:19:16 -0700 (PDT)
-Date: Tue, 12 Aug 2008 13:19:14 -0700
-From: plugh xyzzy <xyzzy at example.com>
-Subject: Event accepted: New Event
-To: User 01 <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>
-Message-id: <1983F777-BE86-4B98-881E-06D938E60920 at example.com>
-MIME-version: 1.0 (example Message framework v928.1)
-X-Mailer: example Mail (2.928.1)
-Content-type: multipart/alternative; boundary=example-Mail-1--253014167
-X-Mail-Calendar-Part: Yes
-X-Brightmail-Tracker: AAAAAA==
-
-
---example-Mail-1--253014167
-Content-Type: text/plain;
-	charset=US-ASCII;
-	format=flowed;
-	delsp=yes
-Content-Transfer-Encoding: 7bit
-
-plugh xyzzy has accepted your iCal event invitation to the event: New  
-Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).
---example-Mail-1--253014167
-Content-Type: multipart/mixed;
-	boundary=example-Mail-2--253014167
-
-
---example-Mail-2--253014167
-Content-Type: text/html;
-	charset=US-ASCII
-Content-Transfer-Encoding: 7bit
-
-<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "><font face="Helvetica" size="3" style="font: 12.0px Helvetica">plugh xyzzy has accepted your iCal event invitation to the event: New Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).</font></div></div></body></html>
---example-Mail-2--253014167
-Content-Type: text/html;
-	charset=US-ASCII
-Content-Transfer-Encoding: 7bit
-
-<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "></div></div></body></html>
---example-Mail-2--253014167--
-
---example-Mail-1--253014167--

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,65 @@
+Return-path: <xyzzy at example.com>
+Received: from hemlock.example.com ([17.128.115.180])
+ by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
+ 14 2008; 32bit)) with ESMTP id <0K5I00I8I8G50QC0 at mail4.example.com> for
+ ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com; Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Original-recipient:
+ rfc822;ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
+Received: from relay14.example.com ([17.128.113.52])
+ by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
+ 2007)) with ESMTP id <0K5I004UB8G5UR90 at hemlock.example.com> for
+ ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
+ (ORCPT ical-living-on+2Bd7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com); Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Received: from relay14.example.com (unknown [127.0.0.1])
+	by relay14.example.com (Symantec Mail Security) with ESMTP id 6A5EF28087	for
+ <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Received: from [17.83.208.154] (unknown [17.83.208.154])
+	(using TLSv1 with cipher AES128-SHA (128/128 bits))
+	(No client certificate requested)	by relay14.example.com (example SCV relay)
+ with ESMTP id CAEE22808B	for
+ <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
+ 12 Aug 2008 13:19:16 -0700 (PDT)
+Date: Tue, 12 Aug 2008 13:19:14 -0700
+From: plugh xyzzy <xyzzy at example.com>
+Subject: Event accepted: New Event
+To: User 01 <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>
+Message-id: <1983F777-BE86-4B98-881E-06D938E60920 at example.com>
+MIME-version: 1.0 (example Message framework v928.1)
+X-Mailer: example Mail (2.928.1)
+Content-type: multipart/alternative; boundary=example-Mail-1--253014167
+X-Mail-Calendar-Part: Yes
+X-Brightmail-Tracker: AAAAAA==
+
+
+--example-Mail-1--253014167
+Content-Type: text/plain;
+	charset=US-ASCII;
+	format=flowed;
+	delsp=yes
+Content-Transfer-Encoding: 7bit
+
+plugh xyzzy has accepted your iCal event invitation to the event: New  
+Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).
+--example-Mail-1--253014167
+Content-Type: multipart/mixed;
+	boundary=example-Mail-2--253014167
+
+
+--example-Mail-2--253014167
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "><font face="Helvetica" size="3" style="font: 12.0px Helvetica">plugh xyzzy has accepted your iCal event invitation to the event: New Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).</font></div></div></body></html>
+--example-Mail-2--253014167
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "></div></div></body></html>
+--example-Mail-2--253014167--
+
+--example-Mail-1--253014167--

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,90 +0,0 @@
-Return-path: <xyzzy at example.com>
-Received: from hemlock.example.com ([17.128.115.180])
- by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
- 14 2008; 32bit)) with ESMTP id <0K5I00I8I8G50QC0 at mail4.example.com> for
- ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com; Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Original-recipient:
- rfc822;ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
-Received: from relay14.example.com ([17.128.113.52])
- by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
- 2007)) with ESMTP id <0K5I004UB8G5UR90 at hemlock.example.com> for
- ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
- (ORCPT ical-living-on+2Bd7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com); Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Received: from relay14.example.com (unknown [127.0.0.1])
-	by relay14.example.com (Symantec Mail Security) with ESMTP id 6A5EF28087	for
- <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Received: from [17.83.208.154] (unknown [17.83.208.154])
-	(using TLSv1 with cipher AES128-SHA (128/128 bits))
-	(No client certificate requested)	by relay14.example.com (example SCV relay)
- with ESMTP id CAEE22808B	for
- <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
- 12 Aug 2008 13:19:16 -0700 (PDT)
-Date: Tue, 12 Aug 2008 13:19:14 -0700
-From: plugh xyzzy <xyzzy at example.com>
-Subject: Event accepted: New Event
-To: User 01 <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>
-Message-id: <1983F777-BE86-4B98-881E-06D938E60920 at example.com>
-MIME-version: 1.0 (example Message framework v928.1)
-X-Mailer: example Mail (2.928.1)
-Content-type: multipart/alternative; boundary=example-Mail-1--253014167
-X-Mail-Calendar-Part: Yes
-X-Brightmail-Tracker: AAAAAA==
-
-
---example-Mail-1--253014167
-Content-Type: text/plain;
-	charset=US-ASCII;
-	format=flowed;
-	delsp=yes
-Content-Transfer-Encoding: 7bit
-
-plugh xyzzy has accepted your iCal event invitation to the event: New  
-Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).
---example-Mail-1--253014167
-Content-Type: multipart/mixed;
-	boundary=example-Mail-2--253014167
-
-
---example-Mail-2--253014167
-Content-Type: text/html;
-	charset=US-ASCII
-Content-Transfer-Encoding: 7bit
-
-<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "><font face="Helvetica" size="3" style="font: 12.0px Helvetica">plugh xyzzy has accepted your iCal event invitation to the event: New Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).</font></div></div></body></html>
---example-Mail-2--253014167
-Content-Disposition: attachment;
-	filename=iCal-20080812-131911.ics
-Content-Type: text/calendar;
-	x-unix-mode=0644;
-	name="iCal-20080812-131911.ics"
-Content-Transfer-Encoding: quoted-printable
-
-BEGIN:VCALENDAR=0D=0APRODID:-//example=20Inc.//iCal=203.0//EN=0D=0A=
-CALSCALE:GREGORIAN=0D=0AVERSION:2.0=0D=0AMETHOD:REPLY=0D=0A=
-BEGIN:VTIMEZONE=0D=0ATZID:US/Pacific=0D=0ABEGIN:DAYLIGHT=0D=0A=
-TZOFFSETFROM:-0800=0D=0ATZOFFSETTO:-0700=0D=0ADTSTART:20070311T020000=0D=0A=
-RRULE:FREQ=3DYEARLY;BYMONTH=3D3;BYDAY=3D2SU=0D=0ATZNAME:PDT=0D=0A=
-END:DAYLIGHT=0D=0ABEGIN:STANDARD=0D=0ATZOFFSETFROM:-0700=0D=0A=
-TZOFFSETTO:-0800=0D=0ADTSTART:20071104T020000=0D=0A=
-RRULE:FREQ=3DYEARLY;BYMONTH=3D11;BYDAY=3D1SU=0D=0ATZNAME:PST=0D=0A=
-END:STANDARD=0D=0AEND:VTIMEZONE=0D=0ABEGIN:VEVENT=0D=0ASEQUENCE:7=0D=0A=
-DTSTART;TZID=3DUS/Pacific:20080812T100000=0D=0A=
-UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C=0D=0AORGANIZER;CN=3D"User=20=
-01":mailto:ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08=0D=0A=20=
-002fb285f at example.com=0D=0ADTSTAMP:20080812T201911Z=0D=0ASUMMARY:New=20=
-Event=0D=0A=
-CREATED:20080812T201906Z=0D=0ADTEND;TZID=3DUS/Pacific:20080812T110000=0D=0A=
-END:VEVENT=0D=0AEND:VCALENDAR=0D=0A=
-
---example-Mail-2--253014167
-Content-Type: text/html;
-	charset=US-ASCII
-Content-Transfer-Encoding: 7bit
-
-<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "></div></div></body></html>
---example-Mail-2--253014167--
-
---example-Mail-1--253014167--

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,90 @@
+Return-path: <xyzzy at example.com>
+Received: from hemlock.example.com ([17.128.115.180])
+ by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
+ 14 2008; 32bit)) with ESMTP id <0K5I00I8I8G50QC0 at mail4.example.com> for
+ ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com; Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Original-recipient:
+ rfc822;ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
+Received: from relay14.example.com ([17.128.113.52])
+ by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
+ 2007)) with ESMTP id <0K5I004UB8G5UR90 at hemlock.example.com> for
+ ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
+ (ORCPT ical-living-on+2Bd7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com); Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Received: from relay14.example.com (unknown [127.0.0.1])
+	by relay14.example.com (Symantec Mail Security) with ESMTP id 6A5EF28087	for
+ <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Received: from [17.83.208.154] (unknown [17.83.208.154])
+	(using TLSv1 with cipher AES128-SHA (128/128 bits))
+	(No client certificate requested)	by relay14.example.com (example SCV relay)
+ with ESMTP id CAEE22808B	for
+ <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
+ 12 Aug 2008 13:19:16 -0700 (PDT)
+Date: Tue, 12 Aug 2008 13:19:14 -0700
+From: plugh xyzzy <xyzzy at example.com>
+Subject: Event accepted: New Event
+To: User 01 <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>
+Message-id: <1983F777-BE86-4B98-881E-06D938E60920 at example.com>
+MIME-version: 1.0 (example Message framework v928.1)
+X-Mailer: example Mail (2.928.1)
+Content-type: multipart/alternative; boundary=example-Mail-1--253014167
+X-Mail-Calendar-Part: Yes
+X-Brightmail-Tracker: AAAAAA==
+
+
+--example-Mail-1--253014167
+Content-Type: text/plain;
+	charset=US-ASCII;
+	format=flowed;
+	delsp=yes
+Content-Transfer-Encoding: 7bit
+
+plugh xyzzy has accepted your iCal event invitation to the event: New  
+Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).
+--example-Mail-1--253014167
+Content-Type: multipart/mixed;
+	boundary=example-Mail-2--253014167
+
+
+--example-Mail-2--253014167
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "><font face="Helvetica" size="3" style="font: 12.0px Helvetica">plugh xyzzy has accepted your iCal event invitation to the event: New Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).</font></div></div></body></html>
+--example-Mail-2--253014167
+Content-Disposition: attachment;
+	filename=iCal-20080812-131911.ics
+Content-Type: text/calendar;
+	x-unix-mode=0644;
+	name="iCal-20080812-131911.ics"
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR=0D=0APRODID:-//example=20Inc.//iCal=203.0//EN=0D=0A=
+CALSCALE:GREGORIAN=0D=0AVERSION:2.0=0D=0AMETHOD:REPLY=0D=0A=
+BEGIN:VTIMEZONE=0D=0ATZID:US/Pacific=0D=0ABEGIN:DAYLIGHT=0D=0A=
+TZOFFSETFROM:-0800=0D=0ATZOFFSETTO:-0700=0D=0ADTSTART:20070311T020000=0D=0A=
+RRULE:FREQ=3DYEARLY;BYMONTH=3D3;BYDAY=3D2SU=0D=0ATZNAME:PDT=0D=0A=
+END:DAYLIGHT=0D=0ABEGIN:STANDARD=0D=0ATZOFFSETFROM:-0700=0D=0A=
+TZOFFSETTO:-0800=0D=0ADTSTART:20071104T020000=0D=0A=
+RRULE:FREQ=3DYEARLY;BYMONTH=3D11;BYDAY=3D1SU=0D=0ATZNAME:PST=0D=0A=
+END:STANDARD=0D=0AEND:VTIMEZONE=0D=0ABEGIN:VEVENT=0D=0ASEQUENCE:7=0D=0A=
+DTSTART;TZID=3DUS/Pacific:20080812T100000=0D=0A=
+UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C=0D=0AORGANIZER;CN=3D"User=20=
+01":mailto:ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08=0D=0A=20=
+002fb285f at example.com=0D=0ADTSTAMP:20080812T201911Z=0D=0ASUMMARY:New=20=
+Event=0D=0A=
+CREATED:20080812T201906Z=0D=0ADTEND;TZID=3DUS/Pacific:20080812T110000=0D=0A=
+END:VEVENT=0D=0AEND:VCALENDAR=0D=0A=
+
+--example-Mail-2--253014167
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "></div></div></body></html>
+--example-Mail-2--253014167--
+
+--example-Mail-1--253014167--

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,89 +0,0 @@
-Return-path: <xyzzy at example.com>
-Received: from hemlock.example.com ([17.128.115.180])
- by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
- 14 2008; 32bit)) with ESMTP id <0K5I00I8I8G50QC0 at mail4.example.com> for
- ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com; Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Original-recipient:
- rfc822;ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
-Received: from relay14.example.com ([17.128.113.52])
- by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
- 2007)) with ESMTP id <0K5I004UB8G5UR90 at hemlock.example.com> for
- ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
- (ORCPT ical-living-on+2Bd7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com); Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Received: from relay14.example.com (unknown [127.0.0.1])
-	by relay14.example.com (Symantec Mail Security) with ESMTP id 6A5EF28087	for
- <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
- 12 Aug 2008 13:19:17 -0700 (PDT)
-Received: from [17.83.208.154] (unknown [17.83.208.154])
-	(using TLSv1 with cipher AES128-SHA (128/128 bits))
-	(No client certificate requested)	by relay14.example.com (example SCV relay)
- with ESMTP id CAEE22808B	for
- <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
- 12 Aug 2008 13:19:16 -0700 (PDT)
-Date: Tue, 12 Aug 2008 13:19:14 -0700
-From: plugh xyzzy <xyzzy at example.com>
-Subject: Event accepted: New Event
-To: User 01 <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>
-Message-id: <1983F777-BE86-4B98-881E-06D938E60920 at example.com>
-MIME-version: 1.0 (example Message framework v928.1)
-X-Mailer: example Mail (2.928.1)
-Content-type: multipart/alternative; boundary=example-Mail-1--253014167
-X-Mail-Calendar-Part: Yes
-X-Brightmail-Tracker: AAAAAA==
-
-
---example-Mail-1--253014167
-Content-Type: text/plain;
-	charset=US-ASCII;
-	format=flowed;
-	delsp=yes
-Content-Transfer-Encoding: 7bit
-
-plugh xyzzy has accepted your iCal event invitation to the event: New  
-Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).
---example-Mail-1--253014167
-Content-Type: multipart/mixed;
-	boundary=example-Mail-2--253014167
-
-
---example-Mail-2--253014167
-Content-Type: text/html;
-	charset=US-ASCII
-Content-Transfer-Encoding: 7bit
-
-<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "><font face="Helvetica" size="3" style="font: 12.0px Helvetica">plugh xyzzy has accepted your iCal event invitation to the event: New Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).</font></div></div></body></html>
---example-Mail-2--253014167
-Content-Disposition: attachment;
-	filename=iCal-20080812-131911.ics
-Content-Type: text/calendar;
-	x-unix-mode=0644;
-	name="iCal-20080812-131911.ics"
-Content-Transfer-Encoding: quoted-printable
-
-BEGIN:VCALENDAR=0D=0APRODID:-//example=20Inc.//iCal=203.0//EN=0D=0A=
-CALSCALE:GREGORIAN=0D=0AVERSION:2.0=0D=0AMETHOD:REPLY=0D=0A=
-BEGIN:VTIMEZONE=0D=0ATZID:US/Pacific=0D=0ABEGIN:DAYLIGHT=0D=0A=
-TZOFFSETFROM:-0800=0D=0ATZOFFSETTO:-0700=0D=0ADTSTART:20070311T020000=0D=0A=
-RRULE:FREQ=3DYEARLY;BYMONTH=3D3;BYDAY=3D2SU=0D=0ATZNAME:PDT=0D=0A=
-END:DAYLIGHT=0D=0ABEGIN:STANDARD=0D=0ATZOFFSETFROM:-0700=0D=0A=
-TZOFFSETTO:-0800=0D=0ADTSTART:20071104T020000=0D=0A=
-RRULE:FREQ=3DYEARLY;BYMONTH=3D11;BYDAY=3D1SU=0D=0ATZNAME:PST=0D=0A=
-END:STANDARD=0D=0AEND:VTIMEZONE=0D=0ABEGIN:VEVENT=0D=0ASEQUENCE:7=0D=0A=
-DTSTART;TZID=3DUS/Pacific:20080812T100000=0D=0A=
-UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C=0D=0A
-DTSTAMP:20080812T201911Z=0D=0ASUMMARY:New=20=
-Event=0D=0A=
-CREATED:20080812T201906Z=0D=0ADTEND;TZID=3DUS/Pacific:20080812T110000=0D=0A=
-END:VEVENT=0D=0AEND:VCALENDAR=0D=0A=
-
---example-Mail-2--253014167
-Content-Type: text/html;
-	charset=US-ASCII
-Content-Transfer-Encoding: 7bit
-
-<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "></div></div></body></html>
---example-Mail-2--253014167--
-
---example-Mail-1--253014167--

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,89 @@
+Return-path: <xyzzy at example.com>
+Received: from hemlock.example.com ([17.128.115.180])
+ by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
+ 14 2008; 32bit)) with ESMTP id <0K5I00I8I8G50QC0 at mail4.example.com> for
+ ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com; Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Original-recipient:
+ rfc822;ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
+Received: from relay14.example.com ([17.128.113.52])
+ by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
+ 2007)) with ESMTP id <0K5I004UB8G5UR90 at hemlock.example.com> for
+ ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
+ (ORCPT ical-living-on+2Bd7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com); Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Received: from relay14.example.com (unknown [127.0.0.1])
+	by relay14.example.com (Symantec Mail Security) with ESMTP id 6A5EF28087	for
+ <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Received: from [17.83.208.154] (unknown [17.83.208.154])
+	(using TLSv1 with cipher AES128-SHA (128/128 bits))
+	(No client certificate requested)	by relay14.example.com (example SCV relay)
+ with ESMTP id CAEE22808B	for
+ <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
+ 12 Aug 2008 13:19:16 -0700 (PDT)
+Date: Tue, 12 Aug 2008 13:19:14 -0700
+From: plugh xyzzy <xyzzy at example.com>
+Subject: Event accepted: New Event
+To: User 01 <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>
+Message-id: <1983F777-BE86-4B98-881E-06D938E60920 at example.com>
+MIME-version: 1.0 (example Message framework v928.1)
+X-Mailer: example Mail (2.928.1)
+Content-type: multipart/alternative; boundary=example-Mail-1--253014167
+X-Mail-Calendar-Part: Yes
+X-Brightmail-Tracker: AAAAAA==
+
+
+--example-Mail-1--253014167
+Content-Type: text/plain;
+	charset=US-ASCII;
+	format=flowed;
+	delsp=yes
+Content-Transfer-Encoding: 7bit
+
+plugh xyzzy has accepted your iCal event invitation to the event: New  
+Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).
+--example-Mail-1--253014167
+Content-Type: multipart/mixed;
+	boundary=example-Mail-2--253014167
+
+
+--example-Mail-2--253014167
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "><font face="Helvetica" size="3" style="font: 12.0px Helvetica">plugh xyzzy has accepted your iCal event invitation to the event: New Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).</font></div></div></body></html>
+--example-Mail-2--253014167
+Content-Disposition: attachment;
+	filename=iCal-20080812-131911.ics
+Content-Type: text/calendar;
+	x-unix-mode=0644;
+	name="iCal-20080812-131911.ics"
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR=0D=0APRODID:-//example=20Inc.//iCal=203.0//EN=0D=0A=
+CALSCALE:GREGORIAN=0D=0AVERSION:2.0=0D=0AMETHOD:REPLY=0D=0A=
+BEGIN:VTIMEZONE=0D=0ATZID:US/Pacific=0D=0ABEGIN:DAYLIGHT=0D=0A=
+TZOFFSETFROM:-0800=0D=0ATZOFFSETTO:-0700=0D=0ADTSTART:20070311T020000=0D=0A=
+RRULE:FREQ=3DYEARLY;BYMONTH=3D3;BYDAY=3D2SU=0D=0ATZNAME:PDT=0D=0A=
+END:DAYLIGHT=0D=0ABEGIN:STANDARD=0D=0ATZOFFSETFROM:-0700=0D=0A=
+TZOFFSETTO:-0800=0D=0ADTSTART:20071104T020000=0D=0A=
+RRULE:FREQ=3DYEARLY;BYMONTH=3D11;BYDAY=3D1SU=0D=0ATZNAME:PST=0D=0A=
+END:STANDARD=0D=0AEND:VTIMEZONE=0D=0ABEGIN:VEVENT=0D=0ASEQUENCE:7=0D=0A=
+DTSTART;TZID=3DUS/Pacific:20080812T100000=0D=0A=
+UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C=0D=0A
+DTSTAMP:20080812T201911Z=0D=0ASUMMARY:New=20=
+Event=0D=0A=
+CREATED:20080812T201906Z=0D=0ADTEND;TZID=3DUS/Pacific:20080812T110000=0D=0A=
+END:VEVENT=0D=0AEND:VCALENDAR=0D=0A=
+
+--example-Mail-2--253014167
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "></div></div></body></html>
+--example-Mail-2--253014167--
+
+--example-Mail-1--253014167--

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_delivery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_delivery.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,74 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks
-from twext.web2 import responsecode
-from twistedcaldav.ical import Component
-from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
-from twistedcaldav.scheduling.imip.delivery import ScheduleViaIMip
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.scheduling.scheduler import ScheduleResponseQueue
-import twistedcaldav.test.util
-from twistedcaldav.config import config
-
-class iMIPProcessing (twistedcaldav.test.util.TestCase):
-    """
-    iCalendar support tests
-    """
-
-    class FakeSchedule(object):
-
-        def __init__(self, calendar):
-            self.calendar = calendar
-
-
-    @inlineCallbacks
-    def test_no_freebusy(self):
-
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REQUEST
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VFREEBUSY
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VFREEBUSY
-END:VCALENDAR
-"""
-
-        scheduler = iMIPProcessing.FakeSchedule(Component.fromString(data))
-        recipients = (RemoteCalendarUser("mailto:user1 at example.com"),)
-        responses = ScheduleResponseQueue("REQUEST", responsecode.OK)
-
-        delivery = ScheduleViaIMip(scheduler, recipients, responses, True)
-        yield delivery.generateSchedulingResponses()
-
-        self.assertEqual(len(responses.responses), 1)
-        self.assertEqual(str(responses.responses[0].children[1]), iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
-
-    @inlineCallbacks
-    def test_matchCalendarUserAddress(self):
-        # iMIP not sensitive to case:
-        self.patch(config.Scheduling[ScheduleViaIMip.serviceType()], "AddressPatterns", ["mailto:.*"])
-        result = yield ScheduleViaIMip.matchCalendarUserAddress("mailto:user at xyzexample.com")
-        self.assertTrue(result)
-        result = ScheduleViaIMip.matchCalendarUserAddress("MAILTO:user at xyzexample.com")
-        self.assertTrue(result)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_delivery.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_delivery.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_delivery.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,74 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks
+from twext.web2 import responsecode
+from twistedcaldav.ical import Component
+from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
+from twistedcaldav.scheduling.imip.delivery import ScheduleViaIMip
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from twistedcaldav.scheduling.scheduler import ScheduleResponseQueue
+import twistedcaldav.test.util
+from twistedcaldav.config import config
+
+class iMIPProcessing (twistedcaldav.test.util.TestCase):
+    """
+    iCalendar support tests
+    """
+
+    class FakeSchedule(object):
+
+        def __init__(self, calendar):
+            self.calendar = calendar
+
+
+    @inlineCallbacks
+    def test_no_freebusy(self):
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+"""
+
+        scheduler = iMIPProcessing.FakeSchedule(Component.fromString(data))
+        recipients = (RemoteCalendarUser("mailto:user1 at example.com"),)
+        responses = ScheduleResponseQueue("REQUEST", responsecode.OK)
+
+        delivery = ScheduleViaIMip(scheduler, recipients, responses, True)
+        yield delivery.generateSchedulingResponses()
+
+        self.assertEqual(len(responses.responses), 1)
+        self.assertEqual(str(responses.responses[0].children[1]), iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+
+    @inlineCallbacks
+    def test_matchCalendarUserAddress(self):
+        # iMIP not sensitive to case:
+        self.patch(config.Scheduling[ScheduleViaIMip.serviceType()], "AddressPatterns", ["mailto:.*"])
+        result = yield ScheduleViaIMip.matchCalendarUserAddress("mailto:user at xyzexample.com")
+        self.assertTrue(result)
+        result = ScheduleViaIMip.matchCalendarUserAddress("MAILTO:user at xyzexample.com")
+        self.assertTrue(result)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_mailgateway.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_mailgateway.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_mailgateway.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,871 +0,0 @@
-##
-# Copyright (c) 2008-2012 Apple Inc. All 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 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
-
-import datetime
-import email
-import os
-
-
-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):
-
-    def setUp(self):
-        super(MailHandlerTests, 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")
-
-
-    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
-        """
-
-        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>
-"""

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_mailgateway.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_mailgateway.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_mailgateway.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip/test/test_mailgateway.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,871 @@
+##
+# Copyright (c) 2008-2012 Apple Inc. All 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 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
+
+import datetime
+import email
+import os
+
+
+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):
+
+    def setUp(self):
+        super(MailHandlerTests, 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")
+
+
+    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
+        """
+
+        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>
+"""

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/imip.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,150 +0,0 @@
-# -*- test-case-name: twistedcaldav.scheduling.test.test_imip -*-
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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.
-##
-
-"""
-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.http import HTTPError
-from twisted.web import client
-
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.util import AuthorizedHTTPGetter
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.internet.adaptendpoint import connect
-
-
-__all__ = [
-    "ScheduleViaIMip",
-]
-
-log = Logger()
-
-
-class ScheduleViaIMip(DeliveryService):
-    
-    @classmethod
-    def serviceType(cls):
-        return DeliveryService.serviceType_imip
-
-    @inlineCallbacks
-    def generateSchedulingResponses(self):
-        def failForRecipient(recipient):
-            err = HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "recipient-failed"),
-                "iMIP request failed",
-            ))
-            self.responses.add(
-                recipient.cuaddr,
-                Failure(exc_value=err),
-                reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE,
-                suppressErrorLog=True
-            )
-        
-        # Generate an HTTP client request
-        try:
-            # We do not do freebusy requests via iMIP
-            if self.freebusy:
-                raise ValueError("iMIP VFREEBUSY requests not supported.")
-
-            method = self.scheduler.calendar.propertyValue("METHOD") 
-            if method not in (
-                "PUBLISH",
-                "REQUEST",
-                "REPLY",
-                "ADD",
-                "CANCEL",
-                "DECLINE_COUNTER",
-            ):
-                log.info("Could not do server-to-imip method: %s" % (method,))
-                for recipient in self.recipients:
-                    err = HTTPError(ErrorResponse(
-                        responsecode.FORBIDDEN,
-                        (caldav_namespace, "recipient-failed"),
-                        "iMIP method not allowed: %s" % (method,),
-                    ))
-                    self.responses.add(
-                        recipient.cuaddr,
-                        Failure(exc_value=err),
-                        reqstatus=iTIPRequestStatus.NO_USER_SUPPORT
-                    )
-                returnValue(None)
-
-            caldata = str(self.scheduler.calendar)
-
-            for recipient in self.recipients:
-                try:
-                    toAddr = str(recipient.cuaddr)
-                    if not toAddr.lower().startswith("mailto:"):
-                        raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP operation." % (toAddr,))
-
-                    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)
-        
-                except Exception, e:
-                    # Generated failed response for this recipient
-                    log.debug("iMIP request %s failed for recipient %s: %s" % (self, recipient, e))
-                    failForRecipient(recipient)
-                
-                else:
-                    self.responses.add(
-                        recipient.cuaddr,
-                        responsecode.OK,
-                        reqstatus=iTIPRequestStatus.MESSAGE_SENT
-                    )
-
-        except Exception, e:
-            # Generated failed responses for each recipient
-            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/implicit.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/implicit.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/implicit.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -26,12 +26,12 @@
 from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
 from twistedcaldav.ical import Property
 from twistedcaldav.scheduling import addressmapping
-from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
-    LocalCalendarUser, PartitionedCalendarUser, OtherServerCalendarUser,\
+from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser, \
+    LocalCalendarUser, PartitionedCalendarUser, OtherServerCalendarUser, \
     normalizeCUAddr
 from twistedcaldav.scheduling.icaldiff import iCalDiff
 from twistedcaldav.scheduling.itip import iTipGenerator, iTIPRequestStatus
-from twistedcaldav.scheduling.scheduler import CalDAVScheduler
+from twistedcaldav.scheduling.caldav.scheduler import CalDAVScheduler
 from twistedcaldav.scheduling.utils import getCalendarObjectForPrincipals
 from twistedcaldav.config import config
 
@@ -48,19 +48,20 @@
 #
 
 class ImplicitScheduler(object):
-    
+
     # Return Status codes
-    STATUS_OK                       = 0
+    STATUS_OK = 0
     STATUS_ORPHANED_CANCELLED_EVENT = 1
-    STATUS_ORPHANED_EVENT           = 2
+    STATUS_ORPHANED_EVENT = 2
 
     def __init__(self):
-        
+
         self.return_status = ImplicitScheduler.STATUS_OK
 
+
     @inlineCallbacks
     def testImplicitSchedulingPUT(self, request, resource, resource_uri, calendar, internal_request=False):
-        
+
         self.request = request
         self.resource = resource
         self.calendar = calendar
@@ -78,12 +79,12 @@
             resource.isScheduleObject = None
             is_scheduling_object = (yield self.checkSchedulingObjectResource(resource))
             existing_type = "schedule" if is_scheduling_object else "calendar"
-            
+
         if existing_type == "calendar":
             self.action = "create" if new_type == "schedule" else "none"
         else:
             self.action = "modify" if new_type == "schedule" else "remove"
-                
+
         # Cannot create new resource with existing UID
         if not existing_resource or self.action == "create":
             yield self.hasCalendarResourceUIDSomewhereElse(resource, resource_uri, new_type)
@@ -95,7 +96,7 @@
             self.return_calendar = calendar
             self.calendar = (yield resource.iCalendarForUser(request))
             yield self.checkImplicitState()
-        
+
         # Once we have collected sufficient information from the calendar data, check validity of organizer and attendees
         self.checkValidOrganizer()
 
@@ -119,13 +120,13 @@
                 (caldav_namespace, "organizer-allowed"),
                 "Organizer cannot schedule without a master component.",
             ))
-            
 
         returnValue((self.action != "none", new_type == "schedule",))
 
+
     @inlineCallbacks
     def testImplicitSchedulingMOVE(self, request, srcresource, srccal, src_uri, destresource, destcal, dest_uri, calendar, internal_request=False):
-        
+
         self.request = request
         self.resource = destresource
         self.calendar = calendar
@@ -158,9 +159,10 @@
 
         returnValue((self.action != "none", new_type == "schedule",))
 
+
     @inlineCallbacks
     def testImplicitSchedulingCOPY(self, request, srcresource, srccal, src_uri, destresource, destcal, dest_uri, calendar, internal_request=False):
-        
+
         self.request = request
         self.resource = destresource
         self.calendar = calendar
@@ -191,9 +193,10 @@
 
         returnValue((self.action != "none", src_is_implicit,))
 
+
     @inlineCallbacks
     def testImplicitSchedulingDELETE(self, request, resource, calendar, internal_request=False):
-        
+
         self.request = request
         self.resource = resource
         self.calendar = calendar
@@ -207,11 +210,12 @@
 
         returnValue((self.action != "none", False,))
 
+
     def checkValidOrganizer(self):
         """
         Make sure the ORGANIZER is allowed to do certain scheduling operations.
         """
-        
+
         # Check to see whether the organizer principal is enabled for scheduling. If not, do not allow them
         # to create new scheduling resources.
         if self.action == "create":
@@ -223,9 +227,10 @@
                     "Organizer cannot schedule",
                 ))
 
+
     @inlineCallbacks
     def checkSchedulingObjectResource(self, resource):
-        
+
         if resource and resource.exists():
             implicit = resource.isScheduleObject
             if implicit is not None:
@@ -238,12 +243,13 @@
                 except ValueError:
                     # We have different ORGANIZERs in the same iCalendar object - this is an error
                     returnValue(False)
-                    
+
                 # Any ORGANIZER => a scheduling object resource
                 returnValue(organizer is not None)
 
         returnValue(False)
-        
+
+
     @inlineCallbacks
     def checkImplicitState(self):
         # Get some useful information from the calendar
@@ -265,6 +271,7 @@
 
         returnValue(self.state is not None)
 
+
     @inlineCallbacks
     def doImplicitScheduling(self, do_smart_merge=False):
         """
@@ -275,7 +282,7 @@
         @return: a new calendar object modified with scheduling information,
             or C{None} if nothing happened or C{int} if some other state occurs
         """
-        
+
         # Setup some parameters
         self.do_smart_merge = do_smart_merge
         self.except_attendees = ()
@@ -296,6 +303,7 @@
         else:
             returnValue(self.return_calendar if hasattr(self, "return_calendar") else self.calendar)
 
+
     @inlineCallbacks
     def refreshAllAttendeesExceptSome(self, request, resource, except_attendees=(), only_attendees=None):
         """
@@ -315,7 +323,6 @@
         self.changed_rids = None
         self.reinvites = None
 
-        
         # Get some useful information from the calendar
         yield self.extractCalendarData()
         self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
@@ -324,14 +331,14 @@
         # Originator is the organizer in this case
         self.originatorPrincipal = self.organizerPrincipal
         self.originator = self.organizer
-        
+
         # We want to suppress chatty iMIP messages when other attendees reply
         self.request.suppressRefresh = False
 
         for attendee in self.calendar.getAllAttendeeProperties():
             if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION").upper() == "NEEDS-ACTION":
                 self.request.suppressRefresh = True
-        
+
         if hasattr(self.request, "doing_attendee_refresh"):
             self.request.doing_attendee_refresh += 1
         else:
@@ -348,9 +355,10 @@
                 self.request.extendedLogItems = {}
             self.request.extendedLogItems["itip.refreshes"] = refreshCount
 
+
     @inlineCallbacks
     def sendAttendeeReply(self, request, resource, calendar, attendee):
-        
+
         self.request = request
         self.resource = resource
         self.calendar = calendar
@@ -360,20 +368,21 @@
         self.calendar_owner = None
         self.internal_request = True
         self.changed_rids = None
-        
+
         # Get some useful information from the calendar
-        yield self.extractCalendarData()        
+        yield self.extractCalendarData()
 
         self.originator = self.attendee = attendee.principal.canonicalCalendarUserAddress()
         self.attendeePrincipal = attendee.principal
-        
+
         result = (yield self.scheduleWithOrganizer())
 
         returnValue(result)
 
+
     @inlineCallbacks
     def extractCalendarData(self):
-        
+
         # Get the originator who is the owner of the calendar resource being modified
         self.originatorPrincipal = None
         self.originator = ""
@@ -401,17 +410,18 @@
                 (caldav_namespace, "single-organizer"),
                 "Only one organizer allowed in scheduling object resource",
             ))
-        
+
         # Get the ATTENDEEs
         self.attendeesByInstance = self.calendar.getAttendeesByInstance(True, onlyScheduleAgentServer=True)
         self.instances = set(self.calendar.getComponentInstances())
         self.attendees = set()
         for attendee, _ignore in self.attendeesByInstance:
             self.attendees.add(attendee)
-            
+
         # Some other useful things
         self.uid = self.calendar.resourceUID()
-    
+
+
     @inlineCallbacks
     def hasCalendarResourceUIDSomewhereElse(self, check_resource, check_uri, type):
         """
@@ -437,34 +447,36 @@
                 "Cannot duplicate scheduling object resource",
             ))
 
+
     @inlineCallbacks
     def isOrganizerScheduling(self):
         """
         Test whether this is a scheduling operation by an organizer
         """
-        
+
         # First must have organizer property
         if not self.organizer:
             returnValue(False)
-        
+
         # Organizer must map to a valid principal
         self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
         self.organizerAddress = (yield addressmapping.mapper.getCalendarUser(self.organizer, self.organizerPrincipal))
         if not self.organizerPrincipal:
             returnValue(False)
-        
+
         # Organizer must be the owner of the calendar resource
         if str(self.calendar_owner) != self.organizerPrincipal.principalURL():
             returnValue(False)
 
         returnValue(True)
 
+
     def isAttendeeScheduling(self):
-        
+
         # First must have organizer property
         if not self.organizer:
             return False
-        
+
         # Check to see whether any attendee is the owner
         for attendee in self.attendees:
             attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
@@ -472,9 +484,10 @@
                 self.attendee = attendee
                 self.attendeePrincipal = attendeePrincipal
                 return True
-        
+
         return False
 
+
     @inlineCallbacks
     def doAccessControl(self, principal, is_organizer):
         """
@@ -486,21 +499,23 @@
         @param is_organizer:
         @type is_organizer:
         """
-        
+
         # Find outbox
         outboxURL = principal.scheduleOutboxURL()
         outbox = (yield self.request.locateResource(outboxURL))
         yield outbox.authorize(self.request, (caldavxml.ScheduleSend(),))
 
+
     def makeScheduler(self):
         """
         Convenience method which we can override in unit tests to make testing easier.
         """
         return CalDAVScheduler(self.request, self.resource)
 
+
     @inlineCallbacks
     def doImplicitOrganizer(self):
-        
+
         # Do access control
         if not self.internal_request:
             yield self.doAccessControl(self.organizerPrincipal, True)
@@ -510,7 +525,7 @@
         self.cancelledAttendees = ()
         self.reinvites = None
         self.needs_action_rids = None
-        
+
         self.needs_sequence_change = False
 
         # Check for a delete
@@ -521,7 +536,7 @@
 
             # Cancel all attendees
             self.cancelledAttendees = [(attendee, None) for attendee in self.attendees]
-            
+
             # CANCEL always bumps sequence
             self.needs_sequence_change = True
 
@@ -533,7 +548,7 @@
             self.oldAttendeesByInstance = self.oldcalendar.getAttendeesByInstance(True, onlyScheduleAgentServer=True)
             self.oldInstances = set(self.oldcalendar.getComponentInstances())
             self.coerceAttendeesPartstatOnModify()
-            
+
             # Don't allow any SEQUENCE to decrease
             if self.oldcalendar:
                 self.calendar.sequenceInSync(self.oldcalendar)
@@ -550,14 +565,14 @@
                     returnValue(None)
             else:
                 log.debug("Implicit - organizer '%s' is modifying UID: '%s'" % (self.organizer, self.uid))
-    
+
                 for rid in self.needs_action_rids:
                     comp = self.calendar.overriddenComponent(rid)
-            
+
                     for attendee in comp.getAllAttendeeProperties():
                         if attendee.hasParameter("PARTSTAT"):
                             cuaddr = attendee.value()
-                            
+
                             if cuaddr in self.organizerPrincipal.calendarUserAddresses():
                                 # If the attendee is the organizer then do not update
                                 # the PARTSTAT to NEEDS-ACTION.
@@ -569,7 +584,7 @@
                 # Check for removed attendees
                 if not recurrence_reschedule:
                     self.findRemovedAttendees()
-                    
+
                 # 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.
                 self.needs_sequence_change = self.calendar.needsiTIPSequenceChange(self.oldcalendar)
@@ -577,7 +592,7 @@
         elif self.action == "create":
             log.debug("Implicit - organizer '%s' is creating UID: '%s'" % (self.organizer, self.uid))
             self.coerceAttendeesPartstatOnCreate()
-            
+
         # Always set RSVP=TRUE for any NEEDS-ACTION
         for attendee in self.calendar.getAllAttendeeProperties():
             if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION").upper() == "NEEDS-ACTION":
@@ -587,7 +602,7 @@
             self.calendar.bumpiTIPInfo(oldcalendar=self.oldcalendar, doSequence=True)
 
         yield self.scheduleWithAttendees()
-        
+
         # Always clear SCHEDULE-FORCE-SEND from all attendees after scheduling
         for attendee in self.calendar.getAllAttendeeProperties():
             try:
@@ -595,8 +610,9 @@
             except KeyError:
                 pass
 
+
     def isOrganizerChangeInsignificant(self):
-        
+
         rids = None
         date_changed_rids = None
         reinvites = None
@@ -613,7 +629,7 @@
                 if "ORGANIZER" in props:
                     checkOrganizerValue = True
                 rids.add(rid)
-                
+
                 if any([testprop in props for testprop in (
                     "DTSTART",
                     "DTEND",
@@ -631,7 +647,7 @@
                         recurrence_reschedule = True
 
                     elif "RRULE" in props:
-                        
+
                         # Need to see if the RRULE change is a simple truncation or expansion - i.e. a change to
                         # COUNT or UNTIL only. If so we don't need to treat this as a complete re-schedule.
 
@@ -643,23 +659,23 @@
                         oldrrule = oldrrule[0].value() if len(oldrrule) else None
                         newrrule = tuple(self.calendar.masterComponent().properties("RRULE"))
                         newrrule = newrrule[0].value() if len(newrrule) else None
-                        
+
                         if newrrule is not None and oldrrule is not None:
-                            
+
                             # Normalize the rrules by removing COUNT/UNTIL and then compare
                             oldrrule = oldrrule.duplicate()
                             newrrule = newrrule.duplicate()
-                            
+
                             oldrrule.setUseUntil(False)
                             oldrrule.setUntil(None)
                             oldrrule.setUseCount(False)
                             oldrrule.setCount(0)
- 
+
                             newrrule.setUseUntil(False)
                             newrrule.setUntil(None)
                             newrrule.setUseCount(False)
                             newrrule.setCount(0)
-                            
+
                             # If they are equal we have a simple change - no overall reschedule
                             if newrrule == oldrrule:
                                 recurrence_reschedule = False
@@ -685,13 +701,14 @@
                     pass
 
         return no_change, rids, date_changed_rids, reinvites, recurrence_reschedule
-    
+
+
     def findRemovedAttendees(self):
         """
         Look for attendees that have been removed from any instances. Save those off
         as users that need to be sent a cancel.
         """
-        
+
         # Several possibilities for when CANCELs need to be sent:
         #
         # Remove ATTENDEE property
@@ -700,12 +717,12 @@
         # Remove RDATE
         # Truncate RRULE
         # Change RRULE
-        
+
         # TODO: the later three will be ignored for now.
 
         mappedOld = set(self.oldAttendeesByInstance)
         mappedNew = set(self.attendeesByInstance)
-        
+
         # Get missing instances
         removedInstances = self.oldInstances - self.instances
         addedInstances = self.instances - self.oldInstances
@@ -722,18 +739,18 @@
 
         # Now figure out the attendees that need to be sent CANCELs
         self.cancelledAttendees = set()
-        
+
         for item in mappedOld:
             if item not in mappedNew:
-                
+
                 # Several possibilities:
                 #
                 # 1. removed from master component - always a CANCEL
                 # 2. removed from overridden component - always a CANCEL
                 # 3. removed overridden component - only CANCEL if not in master or exdate added
-                 
+
                 new_attendee, rid = item
-                
+
                 # 1. & 2.
                 if rid is None or rid not in removedInstances:
                     self.cancelledAttendees.add(item)
@@ -757,6 +774,7 @@
                 if (attendee, rid) not in mappedNew and rid not in oldexdates:
                     self.cancelledAttendees.add((attendee, rid))
 
+
     def coerceAttendeesPartstatOnCreate(self):
         """
         Make sure any attendees handled by the server start off with PARTSTAT=NEEDS-ACTION as
@@ -768,21 +786,22 @@
                 continue
             if attendee.parameterValue("SCHEDULE-AGENT", "SERVER").upper() == "SERVER" and attendee.hasParameter("PARTSTAT"):
                 attendee.setParameter("PARTSTAT", "NEEDS-ACTION")
-    
+
+
     def coerceAttendeesPartstatOnModify(self):
         """
         Make sure that the organizer does not change attendees' PARTSTAT to anything
         other than NEEDS-ACTION for those attendees handled by the server.
         """
-        
+
         # Get the set of Rids in each calendar
         newRids = set(self.calendar.getComponentInstances())
         oldRids = set(self.oldcalendar.getComponentInstances())
-        
+
         # Test/fix ones that are the same
         for rid in (newRids & oldRids):
             self.compareAttendeePartstats(self.oldcalendar.overriddenComponent(rid), self.calendar.overriddenComponent(rid))
-        
+
         # Test/fix ones added
         for rid in (newRids - oldRids):
             # Compare the new one to the old master
@@ -790,26 +809,30 @@
 
         # For removals, we ignore ones that are no longer valid
         valid_old_rids = self.calendar.validInstances(oldRids - newRids)
-    
-        # Test/fix ones removed         
+
+        # Test/fix ones removed
         for rid in valid_old_rids:
-            # Compare the old one to the new master
-            # Note it is hard to recover from this state so raise instead
-            self.compareAttendeePartstats(
+            # Compare the old one to a derived instance, and if there is a change
+            # add the derived instance to the new data
+            newcomp = self.calendar.deriveInstance(rid)
+            changed = self.compareAttendeePartstats(
                 self.oldcalendar.overriddenComponent(rid),
-                self.calendar.overriddenComponent(None),
-                raiseOnMisMatch=True
+                newcomp,
             )
-        
-    def compareAttendeePartstats(self, old_component, new_component, raiseOnMisMatch=False):
+            if changed:
+                self.calendar.addComponent(newcomp)
+
+
+    def compareAttendeePartstats(self, old_component, new_component):
         """
         Compare two components, old and new, and make sure the Organizer has not changed the PARTSTATs
         in the new one to anything other than NEEDS-ACTION. If there is a change, undo it.
         """
-        
+
         old_attendees = dict([(normalizeCUAddr(attendee.value()), attendee) for attendee in old_component.getAllAttendeeProperties()])
         new_attendees = dict([(normalizeCUAddr(attendee.value()), attendee) for attendee in new_component.getAllAttendeeProperties()])
-        
+
+        changed = False
         for cuaddr, newattendee in new_attendees.items():
             # Don't adjust ORGANIZER's ATTENDEE
             if newattendee.value() in self.organizerPrincipal.calendarUserAddresses():
@@ -819,21 +842,18 @@
                 old_attendee = old_attendees.get(cuaddr)
                 old_partstat = old_attendee.parameterValue("PARTSTAT", "NEEDS-ACTION").upper() if old_attendee else "NEEDS-ACTION"
                 if old_attendee is None or old_partstat != new_partstat:
-                    if raiseOnMisMatch:
-                        raise HTTPError(ErrorResponse(
-                            responsecode.FORBIDDEN,
-                            (caldav_namespace, "valid-organizer-change"),
-                            "Organizer cannot change Attendee PARTSTAT",
-                        ))
-                    else:
-                        newattendee.setParameter("PARTSTAT", old_partstat)
+                    newattendee.setParameter("PARTSTAT", old_partstat)
+                    changed = True
 
+        return changed
+
+
     @inlineCallbacks
     def scheduleWithAttendees(self):
-        
+
         # First process cancelled attendees
         total = (yield self.processCancels())
-        
+
         # Process regular requests next
         if self.action in ("create", "modify",):
             total += (yield self.processRequests())
@@ -842,9 +862,10 @@
             self.request.extendedLogItems = {}
         self.request.extendedLogItems["itip.requests"] = total
 
+
     @inlineCallbacks
     def processCancels(self):
-        
+
         # TODO: a better policy here is to aggregate by attendees with the same set of instances
         # being cancelled, but for now we will do one scheduling message per attendee.
 
@@ -852,17 +873,17 @@
         aggregated = {}
         for attendee, rid in self.cancelledAttendees:
             aggregated.setdefault(attendee, []).append(rid)
-        
+
         count = 0
         for attendee, rids in aggregated.iteritems():
-            
+
             # Don't send message back to the ORGANIZER
             if attendee in self.organizerPrincipal.calendarUserAddresses():
                 continue
 
             # Generate an iTIP CANCEL message for this attendee, cancelling
             # each instance or the whole
-            
+
             if None in rids:
                 # One big CANCEL will do
                 itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), None, self.action == "remove")
@@ -874,19 +895,20 @@
             if itipmsg:
                 # This is a local CALDAV scheduling operation.
                 scheduler = self.makeScheduler()
-        
+
                 # Do the PUT processing
                 log.info("Implicit CANCEL - organizer: '%s' to attendee: '%s', UID: '%s', RIDs: '%s'" % (self.organizer, attendee, self.uid, rids))
                 response = (yield scheduler.doSchedulingViaPUT(self.originator, (attendee,), itipmsg, internal_request=True))
                 self.handleSchedulingResponse(response, True)
-                
+
                 count += 1
-            
+
         returnValue(count)
-            
+
+
     @inlineCallbacks
     def processRequests(self):
-        
+
         # TODO: a better policy here is to aggregate by attendees with the same set of instances
         # being requested, but for now we will do one scheduling message per attendee.
 
@@ -916,18 +938,19 @@
             if itipmsg is not None:
                 # This is a local CALDAV scheduling operation.
                 scheduler = self.makeScheduler()
-        
+
                 # Do the PUT processing
                 log.info("Implicit REQUEST - organizer: '%s' to attendee: '%s', UID: '%s'" % (self.organizer, attendee, self.uid,))
                 response = (yield scheduler.doSchedulingViaPUT(self.originator, (attendee,), itipmsg, internal_request=True))
                 self.handleSchedulingResponse(response, True)
-                
+
                 count += 1
-                
+
         returnValue(count)
 
+
     def handleSchedulingResponse(self, response, is_organizer):
-        
+
         # Map each recipient in the response to a status code
         responses = {}
         for item in response.responses:
@@ -935,7 +958,7 @@
             recipient = str(item.children[0].children[0])
             status = str(item.children[1])
             responses[recipient] = status
-            
+
             # Now apply to each ATTENDEE/ORGANIZER in the original data
             self.calendar.setParameterToValueForPropertyWithValue(
                 "SCHEDULE-STATUS",
@@ -943,6 +966,7 @@
                 "ATTENDEE" if is_organizer else "ORGANIZER",
                 recipient)
 
+
     @inlineCallbacks
     def doImplicitAttendee(self):
 
@@ -986,7 +1010,7 @@
                     ))
             else:
                 self.oldcalendar = None
-            
+
             # Get the ORGANIZER's current copy of the calendar object
             yield self.getOrganizersCopy()
             if self.organizer_calendar:
@@ -1008,7 +1032,7 @@
                 if not changeAllowed:
                     if self.calendar.hasPropertyValueInAllComponents(Property("STATUS", "CANCELLED")):
                         log.debug("Attendee '%s' is creating CANCELLED event for mismatched UID: '%s' - removing entire event" % (self.attendee, self.uid,))
-                        self.return_status = ImplicitScheduler.STATUS_ORPHANED_CANCELLED_EVENT
+                        self.return_status = ImplicitScheduler.STATUS_ORPHANED_EVENT
                         returnValue(None)
                     else:
                         log.error("Attendee '%s' is not allowed to make an unauthorized change to an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
@@ -1018,6 +1042,13 @@
                             "Attendee changes are not allowed",
                         ))
 
+                # Check that the return calendar actually has any components left - this can happen if a cancelled
+                # component is removed and replaced by another cancelled or invalid one
+                if self.calendar.mainType() is None:
+                    log.debug("Attendee '%s' is replacing CANCELLED event: '%s' - removing entire event" % (self.attendee, self.uid,))
+                    self.return_status = ImplicitScheduler.STATUS_ORPHANED_EVENT
+                    returnValue(None)
+
                 if not doITipReply:
                     log.debug("Implicit - attendee '%s' is updating UID: '%s' but change is not significant" % (self.attendee, self.uid))
                     returnValue(None)
@@ -1078,6 +1109,7 @@
                 log.debug("Implicit - attendee '%s' is updating UID without server scheduling: '%s'" % (self.attendee, self.uid))
                 # Nothing else to do
 
+
     @inlineCallbacks
     def doImplicitMissingAttendee(self):
 
@@ -1089,7 +1121,7 @@
             # We will allow the attendee to do anything in this case, but we will mark the organizer
             # with an schedule-status error and schedule-agent none
             log.debug("Missing attendee is allowed to update UID: '%s' with invalid organizer '%s'" % (self.uid, self.organizer))
-            
+
             # Make sure ORGANIZER is not changed if originally SCHEDULE-AGENT=SERVER
             if self.resource.exists():
                 self.oldcalendar = (yield self.resource.iCalendarForUser(self.request))
@@ -1108,6 +1140,7 @@
                 self.calendar.setParameterToValueForPropertyWithValue("SCHEDULE-AGENT", "NONE", "ORGANIZER", None)
                 self.calendar.setParameterToValueForPropertyWithValue("SCHEDULE-STATUS", iTIPRequestStatus.NO_USER_SUPPORT_CODE, "ORGANIZER", None)
 
+
     def checkOrganizerScheduleAgent(self):
 
         is_server = self.calendar.getOrganizerScheduleAgent()
@@ -1116,25 +1149,26 @@
         if config.Scheduling.iMIP.Enabled and self.organizerAddress.cuaddr.lower().startswith("mailto:"):
             return is_server
 
-        if not local_organizer and is_server:
+        if not config.Scheduling.iSchedule.Enabled and not local_organizer and is_server:
             # Coerce ORGANIZER to SCHEDULE-AGENT=NONE
             log.debug("Attendee '%s' is not allowed to use SCHEDULE-AGENT=SERVER on organizer: UID:%s" % (self.attendeePrincipal, self.uid,))
             self.calendar.setParameterToValueForPropertyWithValue("SCHEDULE-AGENT", "NONE", "ORGANIZER", None)
             self.calendar.setParameterToValueForPropertyWithValue("SCHEDULE-STATUS", iTIPRequestStatus.NO_USER_SUPPORT_CODE, "ORGANIZER", None)
             is_server = False
-            
+
         return is_server
 
+
     @inlineCallbacks
     def getOrganizersCopy(self):
         """
         Get the Organizer's copy of the event being processed.
-        
+
         NB it is possible that the Organizer is not hosted on this server
         so the result here will be None. In that case we have to trust that
         the attendee does the right thing about changing the details in the event.
         """
-        
+
         self.organizer_calendar = None
         calendar_resource, _ignore_name, _ignore_collection, _ignore_uri = (yield getCalendarObjectForPrincipals(self.request, self.organizerPrincipal, self.uid))
         if calendar_resource:
@@ -1143,14 +1177,15 @@
             # For partitioning where the organizer is on a different node, we will assume that the attendee's copy
             # of the event is up to date and "authoritative". So we pretend that is the organizer copy
             self.organizer_calendar = self.oldcalendar
-        
+
+
     def isAttendeeChangeInsignificant(self):
         """
         Check whether the change is significant (PARTSTAT) or allowed
         (attendee can only change their property, alarms, TRANSP, and
         instances. Raise an exception if it is not allowed.
         """
-        
+
         oldcalendar = self.oldcalendar
         if oldcalendar is None:
             oldcalendar = self.organizer_calendar
@@ -1158,28 +1193,31 @@
         differ = iCalDiff(oldcalendar, self.calendar, self.do_smart_merge)
         return differ.attendeeMerge(self.attendee)
 
+
     def scheduleWithOrganizer(self, changedRids=None):
 
         if not hasattr(self.request, "extendedLogItems"):
             self.request.extendedLogItems = {}
         self.request.extendedLogItems["itip.reply"] = "reply"
-    
+
         itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, changedRids=changedRids)
 
         # Send scheduling message
         return self.sendToOrganizer("REPLY", itipmsg)
 
+
     def scheduleCancelWithOrganizer(self):
 
         if not hasattr(self.request, "extendedLogItems"):
             self.request.extendedLogItems = {}
         self.request.extendedLogItems["itip.reply"] = "cancel"
-    
+
         itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, force_decline=True)
 
         # Send scheduling message
         return self.sendToOrganizer("CANCEL", itipmsg)
 
+
     def sendToOrganizer(self, action, itipmsg):
 
         # Send scheduling message
@@ -1190,7 +1228,7 @@
         # Do the PUT processing
         def _gotResponse(response):
             self.handleSchedulingResponse(response, False)
-            
+
         log.info("Implicit %s - attendee: '%s' to organizer: '%s', UID: '%s'" % (action, self.attendee, self.organizer, self.uid,))
         d = scheduler.doSchedulingViaPUT(self.originator, (self.organizer,), itipmsg, internal_request=True)
         d.addCallback(_gotResponse)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/__init__.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,26 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All 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.
-##
-
-
-"""
-iSchedule scheduling.
-"""
-
-from twext.web2 import http_headers
-
-# These new HTTP headers should appear with case-preserved
-hdrs = ("iSchedule-Version", "iSchedule-Message-ID", "DKIM-Signature",)
-http_headers.casemappingify(dict([(i, i) for i in hdrs]))

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/__init__.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/__init__.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,26 @@
+##
+# Copyright (c) 2012 Apple Inc. All 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.
+##
+
+
+"""
+iSchedule scheduling.
+"""
+
+from twext.web2 import http_headers
+
+# These new HTTP headers should appear with case-preserved
+hdrs = ("iSchedule-Version", "iSchedule-Message-ID", "DKIM-Signature",)
+http_headers.casemappingify(dict([(i, i) for i in hdrs]))

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/delivery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/delivery.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,498 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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 StringIO import StringIO
-
-from twisted.internet.defer import inlineCallbacks, DeferredList, returnValue
-from twisted.internet.protocol import Factory
-
-from twisted.python.failure import Failure
-
-from twext.web2 import responsecode
-from twext.web2.client.http import ClientRequest
-from twext.web2.client.http import HTTPClientProtocol
-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 twistedcaldav.accounting import accountingEnabledForCategory, emitAccounting
-from twistedcaldav.client.pool import _configuredClientContextFactory
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser, RemoteCalendarUser, \
-    OtherServerCalendarUser
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.ischedule.dkim import DKIMRequest, DKIMUtils
-from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServerRecord
-from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServers
-from twistedcaldav.scheduling.ischedule.utils import lookupServerViaSRV
-from twistedcaldav.scheduling.ischedule.xml import ScheduleResponse, Response, \
-    RequestStatus, Recipient, ischedule_namespace, CalendarData, \
-    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,
-as well as internal partitioning or podding.
-"""
-
-__all__ = [
-    "ScheduleViaISchedule",
-]
-
-log = Logger()
-
-
-
-class ScheduleViaISchedule(DeliveryService):
-
-    domainServerMap = {}
-
-    @classmethod
-    def serviceType(cls):
-        return DeliveryService.serviceType_ischedule
-
-
-    @classmethod
-    @inlineCallbacks
-    def matchCalendarUserAddress(cls, cuaddr):
-
-        # TODO: here is where we would attempt service discovery based on the cuaddr.
-
-        # Only handle mailtos:
-        if cuaddr.lower().startswith("mailto:"):
-            _ignore_local, domain = cuaddr[7:].split("@", 1)
-            server = (yield cls.serverForDomain(domain))
-            returnValue(server is not None)
-
-        # Do default match
-        result = (yield super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr))
-        returnValue(result)
-
-
-    @classmethod
-    @inlineCallbacks
-    def serverForDomain(cls, domain):
-        if domain not in cls.domainServerMap:
-
-            # First check built-in list of remote servers
-            servermgr = IScheduleServers()
-            server = servermgr.mapDomain(domain)
-            if server is not None:
-                cls.domainServerMap[domain] = server
-            else:
-                # Lookup domain
-                result = (yield lookupServerViaSRV(domain))
-                if result is None:
-                    # Lookup domain
-                    result = (yield lookupServerViaSRV(domain, service="_ischedule"))
-                    if result is None:
-                        cls.domainServerMap[domain] = None
-                    else:
-                        # Create the iSchedule server record for this server
-                        cls.domainServerMap[domain] = IScheduleServerRecord(uri="http://%s:%s/.well-known/ischedule" % result)
-                else:
-                    # Create the iSchedule server record for this server
-                    cls.domainServerMap[domain] = IScheduleServerRecord(uri="https://%s:%s/.well-known/ischedule" % result)
-
-        returnValue(cls.domainServerMap[domain])
-
-
-    @inlineCallbacks
-    def generateSchedulingResponses(self, refreshOnly=False):
-        """
-        Generate scheduling responses for remote recipients.
-        """
-
-        # Group recipients by server so that we can do a single request with multiple recipients
-        # to each different server.
-        groups = {}
-        for recipient in self.recipients:
-            if isinstance(recipient, RemoteCalendarUser):
-                # Map the recipient's domain to a server
-                server = (yield self.serverForDomain(recipient.domain))
-            elif isinstance(recipient, PartitionedCalendarUser):
-                server = self._getServerForPartitionedUser(recipient)
-            elif isinstance(recipient, OtherServerCalendarUser):
-                server = self._getServerForOtherServerUser(recipient)
-            else:
-                assert False, "Incorrect calendar user address class"
-            if not server:
-                # Cannot do server-to-server for this recipient.
-                err = HTTPError(ErrorResponse(
-                    responsecode.NOT_FOUND,
-                    (ischedule_namespace, "recipient-allowed"),
-                    "No server for recipient",
-                ))
-                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
-
-                # Process next recipient
-                continue
-
-            if not server.allow_to:
-                # Cannot do server-to-server outgoing requests for this server.
-                err = HTTPError(ErrorResponse(
-                    responsecode.NOT_FOUND,
-                    (ischedule_namespace, "recipient-allowed"),
-                    "Cannot send to recipient's server",
-                ))
-                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
-                # Process next recipient
-                continue
-
-            groups.setdefault(server, []).append(recipient)
-
-        if len(groups) == 0:
-            returnValue(None)
-
-        # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
-        # we will generate for each request. That way we can have parallel requests in progress
-        # rather than serialize them.
-        deferreds = []
-        for server, recipients in groups.iteritems():
-            requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses, refreshOnly)
-            deferreds.append(requestor.doRequest())
-
-        yield DeferredList(deferreds)
-
-
-    def _getServerForPartitionedUser(self, recipient):
-
-        if not hasattr(self, "partitionedServers"):
-            self.partitionedServers = {}
-
-        partition = recipient.principal.partitionURI()
-        if partition not in self.partitionedServers:
-            self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
-            self.partitionedServers[partition].unNormalizeAddresses = False
-            self.partitionedServers[partition].moreHeaders.append(recipient.principal.server().secretHeader())
-
-        return self.partitionedServers[partition]
-
-
-    def _getServerForOtherServerUser(self, recipient):
-
-        if not hasattr(self, "otherServers"):
-            self.otherServers = {}
-
-        serverURI = recipient.principal.serverURI()
-        if serverURI not in self.otherServers:
-            self.otherServers[serverURI] = IScheduleServerRecord(uri=joinURL(serverURI, "/ischedule"))
-            self.otherServers[serverURI].unNormalizeAddresses = not recipient.principal.server().isImplicit
-            self.otherServers[serverURI].moreHeaders.append(recipient.principal.server().secretHeader())
-
-        return self.otherServers[serverURI]
-
-
-
-class IScheduleRequest(object):
-
-    def __init__(self, scheduler, server, recipients, responses, refreshOnly=False):
-
-        self.scheduler = scheduler
-        self.server = server
-        self.recipients = recipients
-        self.responses = responses
-        self.refreshOnly = refreshOnly
-        self.headers = None
-        self.data = None
-
-
-    @inlineCallbacks
-    def doRequest(self):
-
-        # Generate an HTTP client request
-        try:
-            if not hasattr(self.scheduler.request, "extendedLogItems"):
-                self.scheduler.request.extendedLogItems = {}
-            if "itip.ischedule" not in self.scheduler.request.extendedLogItems:
-                self.scheduler.request.extendedLogItems["itip.ischedule"] = 0
-            self.scheduler.request.extendedLogItems["itip.ischedule"] += 1
-
-            # Loop over at most 3 redirects
-            ssl, host, port, path = self.server.details()
-            for _ignore in xrange(3):
-                self._prepareRequest(host, port)
-                response = (yield self._processRequest(ssl, host, port, path))
-                if response.code not in (responsecode.MOVED_PERMANENTLY, responsecode.TEMPORARY_REDIRECT,):
-                    break
-                if response.code == responsecode.MOVED_PERMANENTLY:
-                    self.server.redirect(response.headers.getRawHeaders("location")[0])
-                    ssl, host, port, path = self.server.details()
-                else:
-                    scheme, netloc, path, _ignore_query, _ignore_fragment = urlsplit(response.headers.getRawHeaders("location")[0])
-                    ssl = scheme.lower() == "https"
-                    host = netloc.split(":")
-                    if ":" in netloc:
-                        host, port = netloc.split(":")
-                        port = int(port)
-                    else:
-                        host = netloc
-                        port = 443 if ssl else 80
-            else:
-                raise ValueError("Too many redirects")
-
-            if accountingEnabledForCategory("iSchedule"):
-                self.loggedResponse = yield self.logResponse(response)
-                emitAccounting("iSchedule", "", self.loggedRequest + "\n" + self.loggedResponse, "POST")
-
-            if response.code in (responsecode.OK,):
-                xml = (yield davXMLFromStream(response.stream))
-                self._parseResponse(xml)
-            else:
-                raise ValueError("Incorrect server response status code: %s" % (response.code,))
-
-        except Exception, e:
-            # Generated failed responses for each recipient
-            log.err("Could not do server-to-server request : %s %s" % (self, e))
-            for recipient in self.recipients:
-                err = HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (ischedule_namespace, "recipient-failed"),
-                    "Server-to-server request failed",
-                ))
-                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
-
-    @inlineCallbacks
-    def logRequest(self, request):
-        """
-        Log an HTTP request.
-        """
-
-        iostr = StringIO()
-        iostr.write(">>>> Request start\n\n")
-        if hasattr(request, "clientproto"):
-            protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
-        else:
-            protocol = "HTTP/1.1"
-        iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
-        for name, valuelist in request.headers.getAllRawHeaders():
-            for value in valuelist:
-                # Do not log authorization details
-                if name not in ("Authorization",):
-                    iostr.write("%s: %s\n" % (name, value))
-                else:
-                    iostr.write("%s: xxxxxxxxx\n" % (name,))
-        iostr.write("\n")
-
-        # We need to play a trick with the request stream as we can only read it once. So we
-        # read it, store the value in a MemoryStream, and replace the request's stream with that,
-        # so the data can be read again.
-        data = (yield allDataFromStream(request.stream))
-        iostr.write(data)
-        request.stream = MemoryStream(data if data is not None else "")
-        request.stream.doStartReading = None
-
-        iostr.write("\n\n>>>> Request end\n")
-        returnValue(iostr.getvalue())
-
-
-    @inlineCallbacks
-    def logResponse(self, response):
-        """
-        Log an HTTP request.
-        """
-        iostr = StringIO()
-        iostr.write(">>>> Response start\n\n")
-        code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
-        iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
-        for name, valuelist in response.headers.getAllRawHeaders():
-            for value in valuelist:
-                # Do not log authorization details
-                if name not in ("WWW-Authenticate",):
-                    iostr.write("%s: %s\n" % (name, value))
-                else:
-                    iostr.write("%s: xxxxxxxxx\n" % (name,))
-        iostr.write("\n")
-
-        # We need to play a trick with the response stream to ensure we don't mess it up. So we
-        # read it, store the value in a MemoryStream, and replace the response's stream with that,
-        # so the data can be read again.
-        data = (yield allDataFromStream(response.stream))
-        iostr.write(data)
-        response.stream = MemoryStream(data if data is not None else "")
-        response.stream.doStartReading = None
-
-        iostr.write("\n\n>>>> Response end\n")
-        returnValue(iostr.getvalue())
-
-
-    def _prepareRequest(self, host, port):
-        """
-        Setup the request for sending. We might need to do this several times
-        whilst following redirects.
-        """
-
-        self._prepareHeaders(host, port)
-        self._prepareData()
-
-
-    def _prepareHeaders(self, host, port):
-        """
-        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.
-        """
-        self.sign_headers = []
-
-        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
-        originator = normalizeCUAddress(originator, normalizationLookup, self.scheduler.resource.principalForCalendarUserAddress, toUUID=False)
-        self.headers.addRawHeader("Originator", utf8String(originator))
-        self.sign_headers.append("Originator")
-
-        for recipient in self.recipients:
-            self.headers.addRawHeader("Recipient", utf8String(recipient.cuaddr))
-
-        # Remember to "over sign" the Recipient header
-        self.sign_headers.append("Recipient+")
-
-        self._doAuthentication()
-
-        self.headers.setHeader("Content-Type", MimeType("text", "calendar", params={"charset": "utf-8"}))
-        self.sign_headers.append("Content-Type")
-
-        # Add any additional headers
-        for name, value in self.server.moreHeaders:
-            self.headers.addRawHeader(name, value)
-
-        if self.refreshOnly:
-            self.headers.addRawHeader("X-CALENDARSERVER-ITIP-REFRESHONLY", "T")
-
-
-    def _doAuthentication(self):
-        if self.server.authentication and self.server.authentication[0] == "basic":
-            self.headers.setHeader(
-                "Authorization",
-                ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
-            )
-            self.sign_headers.append("Authorization")
-
-
-    def _prepareData(self):
-        """
-        Prepare data via normalization etc. Only need to do this once even when
-        redirects occur.
-        """
-
-        if self.data is None:
-            # Need to remap cuaddrs from urn:uuid
-            if self.server.unNormalizeAddresses and self.scheduler.method == "PUT":
-                normalizedCalendar = self.scheduler.calendar.duplicate()
-                normalizedCalendar.normalizeCalendarUserAddresses(
-                    normalizationLookup,
-                    self.scheduler.resource.principalForCalendarUserAddress,
-                    toUUID=False)
-            else:
-                normalizedCalendar = self.scheduler.calendar
-
-            # For VFREEBUSY we need to strip out ATTENDEEs that do not match the recipient list
-            if self.scheduler.isfreebusy:
-                normalizedCalendar.removeAllButTheseAttendees([recipient.cuaddr for recipient in self.recipients])
-
-            self.data = str(normalizedCalendar)
-
-
-    @inlineCallbacks
-    def _processRequest(self, ssl, host, port, path):
-        from twisted.internet import reactor
-        f = Factory()
-        f.protocol = HTTPClientProtocol
-        if ssl:
-            ep = GAIEndpoint(reactor, host, port,
-                             _configuredClientContextFactory())
-        else:
-            ep = GAIEndpoint(reactor, host, port)
-        proto = (yield ep.connect(f))
-
-        if config.Scheduling.iSchedule.DKIM.Enabled:
-            domain, selector, key_file, algorithm, useDNSKey, useHTTPKey, usePrivateExchangeKey, expire = DKIMUtils.getConfiguration(config)
-            request = DKIMRequest(
-                "POST",
-                path,
-                self.headers,
-                self.data,
-                domain,
-                selector,
-                key_file,
-                algorithm,
-                self.sign_headers,
-                useDNSKey,
-                useHTTPKey,
-                usePrivateExchangeKey,
-                expire,
-            )
-            yield request.sign()
-        else:
-            request = ClientRequest("POST", path, self.headers, self.data)
-
-        if accountingEnabledForCategory("iSchedule"):
-            self.loggedRequest = yield self.logRequest(request)
-
-        response = (yield proto.submitRequest(request))
-
-        returnValue(response)
-
-
-    def _parseResponse(self, xml):
-
-        # Check for correct root element
-        schedule_response = xml.root_element
-        if not isinstance(schedule_response, ScheduleResponse) or not schedule_response.children:
-            raise HTTPError(responsecode.BAD_REQUEST)
-
-        # Parse each response - do this twice: once looking for errors that will
-        # result in all recipients shown as failures; the second loop adds all the
-        # valid responses to the actual result.
-        for response in schedule_response.children:
-            if not isinstance(response, Response) or not response.children:
-                raise HTTPError(responsecode.BAD_REQUEST)
-            recipient = response.childOfType(Recipient)
-            request_status = response.childOfType(RequestStatus)
-            if not recipient or not request_status:
-                raise HTTPError(responsecode.BAD_REQUEST)
-        for response in schedule_response.children:
-            recipient = str(response.childOfType(Recipient))
-            request_status = str(response.childOfType(RequestStatus))
-            calendar_data = response.childOfType(CalendarData)
-            if calendar_data:
-                calendar_data = str(calendar_data)
-            error = response.childOfType(Error)
-            if error:
-                error = error.children
-            desc = response.childOfType(ResponseDescription)
-            if desc:
-                desc = str(desc)
-            self.responses.clone(
-                recipient,
-                request_status,
-                calendar_data,
-                error,
-                desc,
-            )

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/delivery.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/delivery.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/delivery.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,498 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All 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 StringIO import StringIO
+
+from twisted.internet.defer import inlineCallbacks, DeferredList, returnValue
+from twisted.internet.protocol import Factory
+
+from twisted.python.failure import Failure
+
+from twext.web2 import responsecode
+from twext.web2.client.http import ClientRequest
+from twext.web2.client.http import HTTPClientProtocol
+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 twistedcaldav.accounting import accountingEnabledForCategory, emitAccounting
+from twistedcaldav.client.pool import _configuredClientContextFactory
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser, RemoteCalendarUser, \
+    OtherServerCalendarUser
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.ischedule.dkim import DKIMRequest, DKIMUtils
+from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServerRecord
+from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServers
+from twistedcaldav.scheduling.ischedule.utils import lookupServerViaSRV
+from twistedcaldav.scheduling.ischedule.xml import ScheduleResponse, Response, \
+    RequestStatus, Recipient, ischedule_namespace, CalendarData, \
+    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,
+as well as internal partitioning or podding.
+"""
+
+__all__ = [
+    "ScheduleViaISchedule",
+]
+
+log = Logger()
+
+
+
+class ScheduleViaISchedule(DeliveryService):
+
+    domainServerMap = {}
+
+    @classmethod
+    def serviceType(cls):
+        return DeliveryService.serviceType_ischedule
+
+
+    @classmethod
+    @inlineCallbacks
+    def matchCalendarUserAddress(cls, cuaddr):
+
+        # TODO: here is where we would attempt service discovery based on the cuaddr.
+
+        # Only handle mailtos:
+        if cuaddr.lower().startswith("mailto:"):
+            _ignore_local, domain = cuaddr[7:].split("@", 1)
+            server = (yield cls.serverForDomain(domain))
+            returnValue(server is not None)
+
+        # Do default match
+        result = (yield super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr))
+        returnValue(result)
+
+
+    @classmethod
+    @inlineCallbacks
+    def serverForDomain(cls, domain):
+        if domain not in cls.domainServerMap:
+
+            # First check built-in list of remote servers
+            servermgr = IScheduleServers()
+            server = servermgr.mapDomain(domain)
+            if server is not None:
+                cls.domainServerMap[domain] = server
+            else:
+                # Lookup domain
+                result = (yield lookupServerViaSRV(domain))
+                if result is None:
+                    # Lookup domain
+                    result = (yield lookupServerViaSRV(domain, service="_ischedule"))
+                    if result is None:
+                        cls.domainServerMap[domain] = None
+                    else:
+                        # Create the iSchedule server record for this server
+                        cls.domainServerMap[domain] = IScheduleServerRecord(uri="http://%s:%s/.well-known/ischedule" % result)
+                else:
+                    # Create the iSchedule server record for this server
+                    cls.domainServerMap[domain] = IScheduleServerRecord(uri="https://%s:%s/.well-known/ischedule" % result)
+
+        returnValue(cls.domainServerMap[domain])
+
+
+    @inlineCallbacks
+    def generateSchedulingResponses(self, refreshOnly=False):
+        """
+        Generate scheduling responses for remote recipients.
+        """
+
+        # Group recipients by server so that we can do a single request with multiple recipients
+        # to each different server.
+        groups = {}
+        for recipient in self.recipients:
+            if isinstance(recipient, RemoteCalendarUser):
+                # Map the recipient's domain to a server
+                server = (yield self.serverForDomain(recipient.domain))
+            elif isinstance(recipient, PartitionedCalendarUser):
+                server = self._getServerForPartitionedUser(recipient)
+            elif isinstance(recipient, OtherServerCalendarUser):
+                server = self._getServerForOtherServerUser(recipient)
+            else:
+                assert False, "Incorrect calendar user address class"
+            if not server:
+                # Cannot do server-to-server for this recipient.
+                err = HTTPError(ErrorResponse(
+                    responsecode.NOT_FOUND,
+                    (ischedule_namespace, "recipient-allowed"),
+                    "No server for recipient",
+                ))
+                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
+
+                # Process next recipient
+                continue
+
+            if not server.allow_to:
+                # Cannot do server-to-server outgoing requests for this server.
+                err = HTTPError(ErrorResponse(
+                    responsecode.NOT_FOUND,
+                    (ischedule_namespace, "recipient-allowed"),
+                    "Cannot send to recipient's server",
+                ))
+                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+                # Process next recipient
+                continue
+
+            groups.setdefault(server, []).append(recipient)
+
+        if len(groups) == 0:
+            returnValue(None)
+
+        # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
+        # we will generate for each request. That way we can have parallel requests in progress
+        # rather than serialize them.
+        deferreds = []
+        for server, recipients in groups.iteritems():
+            requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses, refreshOnly)
+            deferreds.append(requestor.doRequest())
+
+        yield DeferredList(deferreds)
+
+
+    def _getServerForPartitionedUser(self, recipient):
+
+        if not hasattr(self, "partitionedServers"):
+            self.partitionedServers = {}
+
+        partition = recipient.principal.partitionURI()
+        if partition not in self.partitionedServers:
+            self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
+            self.partitionedServers[partition].unNormalizeAddresses = False
+            self.partitionedServers[partition].moreHeaders.append(recipient.principal.server().secretHeader())
+
+        return self.partitionedServers[partition]
+
+
+    def _getServerForOtherServerUser(self, recipient):
+
+        if not hasattr(self, "otherServers"):
+            self.otherServers = {}
+
+        serverURI = recipient.principal.serverURI()
+        if serverURI not in self.otherServers:
+            self.otherServers[serverURI] = IScheduleServerRecord(uri=joinURL(serverURI, "/ischedule"))
+            self.otherServers[serverURI].unNormalizeAddresses = not recipient.principal.server().isImplicit
+            self.otherServers[serverURI].moreHeaders.append(recipient.principal.server().secretHeader())
+
+        return self.otherServers[serverURI]
+
+
+
+class IScheduleRequest(object):
+
+    def __init__(self, scheduler, server, recipients, responses, refreshOnly=False):
+
+        self.scheduler = scheduler
+        self.server = server
+        self.recipients = recipients
+        self.responses = responses
+        self.refreshOnly = refreshOnly
+        self.headers = None
+        self.data = None
+
+
+    @inlineCallbacks
+    def doRequest(self):
+
+        # Generate an HTTP client request
+        try:
+            if not hasattr(self.scheduler.request, "extendedLogItems"):
+                self.scheduler.request.extendedLogItems = {}
+            if "itip.ischedule" not in self.scheduler.request.extendedLogItems:
+                self.scheduler.request.extendedLogItems["itip.ischedule"] = 0
+            self.scheduler.request.extendedLogItems["itip.ischedule"] += 1
+
+            # Loop over at most 3 redirects
+            ssl, host, port, path = self.server.details()
+            for _ignore in xrange(3):
+                self._prepareRequest(host, port)
+                response = (yield self._processRequest(ssl, host, port, path))
+                if response.code not in (responsecode.MOVED_PERMANENTLY, responsecode.TEMPORARY_REDIRECT,):
+                    break
+                if response.code == responsecode.MOVED_PERMANENTLY:
+                    self.server.redirect(response.headers.getRawHeaders("location")[0])
+                    ssl, host, port, path = self.server.details()
+                else:
+                    scheme, netloc, path, _ignore_query, _ignore_fragment = urlsplit(response.headers.getRawHeaders("location")[0])
+                    ssl = scheme.lower() == "https"
+                    host = netloc.split(":")
+                    if ":" in netloc:
+                        host, port = netloc.split(":")
+                        port = int(port)
+                    else:
+                        host = netloc
+                        port = 443 if ssl else 80
+            else:
+                raise ValueError("Too many redirects")
+
+            if accountingEnabledForCategory("iSchedule"):
+                self.loggedResponse = yield self.logResponse(response)
+                emitAccounting("iSchedule", "", self.loggedRequest + "\n" + self.loggedResponse, "POST")
+
+            if response.code in (responsecode.OK,):
+                xml = (yield davXMLFromStream(response.stream))
+                self._parseResponse(xml)
+            else:
+                raise ValueError("Incorrect server response status code: %s" % (response.code,))
+
+        except Exception, e:
+            # Generated failed responses for each recipient
+            log.err("Could not do server-to-server request : %s %s" % (self, e))
+            for recipient in self.recipients:
+                err = HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (ischedule_namespace, "recipient-failed"),
+                    "Server-to-server request failed",
+                ))
+                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+
+    @inlineCallbacks
+    def logRequest(self, request):
+        """
+        Log an HTTP request.
+        """
+
+        iostr = StringIO()
+        iostr.write(">>>> Request start\n\n")
+        if hasattr(request, "clientproto"):
+            protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
+        else:
+            protocol = "HTTP/1.1"
+        iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
+        for name, valuelist in request.headers.getAllRawHeaders():
+            for value in valuelist:
+                # Do not log authorization details
+                if name not in ("Authorization",):
+                    iostr.write("%s: %s\n" % (name, value))
+                else:
+                    iostr.write("%s: xxxxxxxxx\n" % (name,))
+        iostr.write("\n")
+
+        # We need to play a trick with the request stream as we can only read it once. So we
+        # read it, store the value in a MemoryStream, and replace the request's stream with that,
+        # so the data can be read again.
+        data = (yield allDataFromStream(request.stream))
+        iostr.write(data)
+        request.stream = MemoryStream(data if data is not None else "")
+        request.stream.doStartReading = None
+
+        iostr.write("\n\n>>>> Request end\n")
+        returnValue(iostr.getvalue())
+
+
+    @inlineCallbacks
+    def logResponse(self, response):
+        """
+        Log an HTTP request.
+        """
+        iostr = StringIO()
+        iostr.write(">>>> Response start\n\n")
+        code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
+        iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
+        for name, valuelist in response.headers.getAllRawHeaders():
+            for value in valuelist:
+                # Do not log authorization details
+                if name not in ("WWW-Authenticate",):
+                    iostr.write("%s: %s\n" % (name, value))
+                else:
+                    iostr.write("%s: xxxxxxxxx\n" % (name,))
+        iostr.write("\n")
+
+        # We need to play a trick with the response stream to ensure we don't mess it up. So we
+        # read it, store the value in a MemoryStream, and replace the response's stream with that,
+        # so the data can be read again.
+        data = (yield allDataFromStream(response.stream))
+        iostr.write(data)
+        response.stream = MemoryStream(data if data is not None else "")
+        response.stream.doStartReading = None
+
+        iostr.write("\n\n>>>> Response end\n")
+        returnValue(iostr.getvalue())
+
+
+    def _prepareRequest(self, host, port):
+        """
+        Setup the request for sending. We might need to do this several times
+        whilst following redirects.
+        """
+
+        self._prepareHeaders(host, port)
+        self._prepareData()
+
+
+    def _prepareHeaders(self, host, port):
+        """
+        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.
+        """
+        self.sign_headers = []
+
+        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
+        originator = normalizeCUAddress(originator, normalizationLookup, self.scheduler.resource.principalForCalendarUserAddress, toUUID=False)
+        self.headers.addRawHeader("Originator", utf8String(originator))
+        self.sign_headers.append("Originator")
+
+        for recipient in self.recipients:
+            self.headers.addRawHeader("Recipient", utf8String(recipient.cuaddr))
+
+        # Remember to "over sign" the Recipient header
+        self.sign_headers.append("Recipient+")
+
+        self._doAuthentication()
+
+        self.headers.setHeader("Content-Type", MimeType("text", "calendar", params={"charset": "utf-8"}))
+        self.sign_headers.append("Content-Type")
+
+        # Add any additional headers
+        for name, value in self.server.moreHeaders:
+            self.headers.addRawHeader(name, value)
+
+        if self.refreshOnly:
+            self.headers.addRawHeader("X-CALENDARSERVER-ITIP-REFRESHONLY", "T")
+
+
+    def _doAuthentication(self):
+        if self.server.authentication and self.server.authentication[0] == "basic":
+            self.headers.setHeader(
+                "Authorization",
+                ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
+            )
+            self.sign_headers.append("Authorization")
+
+
+    def _prepareData(self):
+        """
+        Prepare data via normalization etc. Only need to do this once even when
+        redirects occur.
+        """
+
+        if self.data is None:
+            # Need to remap cuaddrs from urn:uuid
+            if self.server.unNormalizeAddresses and self.scheduler.method == "PUT":
+                normalizedCalendar = self.scheduler.calendar.duplicate()
+                normalizedCalendar.normalizeCalendarUserAddresses(
+                    normalizationLookup,
+                    self.scheduler.resource.principalForCalendarUserAddress,
+                    toUUID=False)
+            else:
+                normalizedCalendar = self.scheduler.calendar
+
+            # For VFREEBUSY we need to strip out ATTENDEEs that do not match the recipient list
+            if self.scheduler.isfreebusy:
+                normalizedCalendar.removeAllButTheseAttendees([recipient.cuaddr for recipient in self.recipients])
+
+            self.data = str(normalizedCalendar)
+
+
+    @inlineCallbacks
+    def _processRequest(self, ssl, host, port, path):
+        from twisted.internet import reactor
+        f = Factory()
+        f.protocol = HTTPClientProtocol
+        if ssl:
+            ep = GAIEndpoint(reactor, host, port,
+                             _configuredClientContextFactory())
+        else:
+            ep = GAIEndpoint(reactor, host, port)
+        proto = (yield ep.connect(f))
+
+        if config.Scheduling.iSchedule.DKIM.Enabled:
+            domain, selector, key_file, algorithm, useDNSKey, useHTTPKey, usePrivateExchangeKey, expire = DKIMUtils.getConfiguration(config)
+            request = DKIMRequest(
+                "POST",
+                path,
+                self.headers,
+                self.data,
+                domain,
+                selector,
+                key_file,
+                algorithm,
+                self.sign_headers,
+                useDNSKey,
+                useHTTPKey,
+                usePrivateExchangeKey,
+                expire,
+            )
+            yield request.sign()
+        else:
+            request = ClientRequest("POST", path, self.headers, self.data)
+
+        if accountingEnabledForCategory("iSchedule"):
+            self.loggedRequest = yield self.logRequest(request)
+
+        response = (yield proto.submitRequest(request))
+
+        returnValue(response)
+
+
+    def _parseResponse(self, xml):
+
+        # Check for correct root element
+        schedule_response = xml.root_element
+        if not isinstance(schedule_response, ScheduleResponse) or not schedule_response.children:
+            raise HTTPError(responsecode.BAD_REQUEST)
+
+        # Parse each response - do this twice: once looking for errors that will
+        # result in all recipients shown as failures; the second loop adds all the
+        # valid responses to the actual result.
+        for response in schedule_response.children:
+            if not isinstance(response, Response) or not response.children:
+                raise HTTPError(responsecode.BAD_REQUEST)
+            recipient = response.childOfType(Recipient)
+            request_status = response.childOfType(RequestStatus)
+            if not recipient or not request_status:
+                raise HTTPError(responsecode.BAD_REQUEST)
+        for response in schedule_response.children:
+            recipient = str(response.childOfType(Recipient))
+            request_status = str(response.childOfType(RequestStatus))
+            calendar_data = response.childOfType(CalendarData)
+            if calendar_data:
+                calendar_data = str(calendar_data)
+            error = response.childOfType(Error)
+            if error:
+                error = error.children
+            desc = response.childOfType(ResponseDescription)
+            if desc:
+                desc = str(desc)
+            self.responses.clone(
+                recipient,
+                request_status,
+                calendar_data,
+                error,
+                desc,
+            )

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/dkim.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/dkim.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/dkim.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,981 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All 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.python.log import Logger
-from twext.web2.client.http import ClientRequest
-from twext.web2.dav.util import allDataFromStream, joinURL
-from twext.web2.http import Response
-from twext.web2.http_headers import MimeType
-from twext.web2.stream import MemoryStream
-
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
-from twistedcaldav.client.geturl import getURL
-from twistedcaldav.config import ConfigurationError
-from twistedcaldav.simpleresource import SimpleResource, SimpleDataResource
-from twistedcaldav.scheduling.ischedule.utils import lookupDataViaTXT, \
-    lookupServerViaSRV
-
-from Crypto.Hash import SHA, SHA256
-from Crypto.PublicKey import RSA
-from Crypto.Signature import PKCS1_v1_5
-
-import base64
-import binascii
-import collections
-import hashlib
-import os
-import textwrap
-import time
-import uuid
-
-"""
-DKIM HTTP message generation and validation,
-"""
-
-log = Logger()
-
-# DKIM/iSchedule Constants
-RSA1 = "rsa-sha1"
-RSA256 = "rsa-sha256"
-Q_DNS = "dns/txt"
-Q_HTTP = "http/well-known"
-Q_PRIVATE = "private-exchange"
-
-KEY_SERVICE_TYPE = "ischedule"
-
-# Headers
-DKIM_SIGNATURE = "DKIM-Signature"
-ISCHEDULE_VERSION = "iSchedule-Version"
-ISCHEDULE_VERSION_VALUE = "1.0"
-ISCHEDULE_MESSAGE_ID = "iSchedule-Message-ID"
-
-
-
-class DKIMUtils(object):
-    """
-    Some useful functions.
-    """
-
-    @staticmethod
-    def validConfiguration(config):
-        if config.Scheduling.iSchedule.DKIM.Enabled:
-
-            if not config.Scheduling.iSchedule.DKIM.Domain and not config.ServerHostName:
-                msg = "DKIM: No domain specified"
-                log.error(msg)
-                raise ConfigurationError(msg)
-
-            if not config.Scheduling.iSchedule.DKIM.KeySelector:
-                msg = "DKIM: No selector specified"
-                log.error(msg)
-                raise ConfigurationError(msg)
-
-            if config.Scheduling.iSchedule.DKIM.SignatureAlgorithm not in (RSA1, RSA256):
-                msg = "DKIM: Invalid algorithm: %s" % (config.Scheduling.iSchedule.SignatureAlgorithm,)
-                log.error(msg)
-                raise ConfigurationError(msg)
-
-            try:
-                with open(config.Scheduling.iSchedule.DKIM.PrivateKeyFile) as f:
-                    key_data = f.read()
-            except IOError, e:
-                msg = "DKIM: Cannot read private key file: %s %s" % (config.Scheduling.iSchedule.DKIM.PrivateKeyFile, e,)
-                log.error(msg)
-                raise ConfigurationError(msg)
-            try:
-                RSA.importKey(key_data)
-            except:
-                msg = "DKIM: Invalid private key file: %s" % (config.Scheduling.iSchedule.DKIM.PrivateKeyFile,)
-                log.error(msg)
-                raise ConfigurationError(msg)
-
-            try:
-                with open(config.Scheduling.iSchedule.DKIM.PublicKeyFile) as f:
-                    key_data = f.read()
-            except IOError, e:
-                msg = "DKIM: Cannot read public key file: %s %s" % (config.Scheduling.iSchedule.DKIM.PublicKeyFile, e,)
-                log.error(msg)
-                raise ConfigurationError(msg)
-            try:
-                RSA.importKey(key_data)
-            except:
-                msg = "DKIM: Invalid public key file: %s" % (config.Scheduling.iSchedule.DKIM.PublicKeyFile,)
-                log.error(msg)
-                raise ConfigurationError(msg)
-
-            if config.Scheduling.iSchedule.DKIM.PrivateExchanges:
-                if not os.path.exists(config.Scheduling.iSchedule.DKIM.PrivateExchanges):
-                    try:
-                        os.makedirs(config.Scheduling.iSchedule.DKIM.PrivateExchanges)
-                    except IOError, e:
-                        msg = "DKIM: Cannot create public key private exchange directory: %s" % (config.Scheduling.iSchedule.DKIM.PrivateExchanges,)
-                        log.error(msg)
-                        raise ConfigurationError(msg)
-                if not os.path.isdir(config.Scheduling.iSchedule.DKIM.PrivateExchanges):
-                    msg = "DKIM: Invalid public key private exchange directory: %s" % (config.Scheduling.iSchedule.DKIM.PrivateExchanges,)
-                    log.error(msg)
-                    raise ConfigurationError(msg)
-                PublicKeyLookup_PrivateExchange.directory = config.Scheduling.iSchedule.DKIM.PrivateExchanges
-
-            log.info("DKIM: Enabled")
-        else:
-            log.info("DKIM: Disabled")
-
-
-    @staticmethod
-    def getConfiguration(config):
-        """
-        Return a tuple of the parameters derived from the config that are used to initialize the DKIMRequest.
-
-        @param config: configuration to look at
-        @type config: L{Config}
-        """
-
-        domain = config.Scheduling.iSchedule.DKIM.Domain if config.Scheduling.iSchedule.DKIM.Domain else config.ServerHostName
-        selector = config.Scheduling.iSchedule.DKIM.KeySelector
-        key_file = config.Scheduling.iSchedule.DKIM.PrivateKeyFile
-        algorithm = config.Scheduling.iSchedule.DKIM.SignatureAlgorithm
-        useDNSKey = config.Scheduling.iSchedule.DKIM.UseDNSKey
-        useHTTPKey = config.Scheduling.iSchedule.DKIM.UseHTTPKey
-        usePrivateExchangeKey = config.Scheduling.iSchedule.DKIM.UsePrivateExchangeKey
-        expire = config.Scheduling.iSchedule.DKIM.ExpireSeconds
-
-        return domain, selector, key_file, algorithm, useDNSKey, useHTTPKey, usePrivateExchangeKey, expire
-
-
-    @staticmethod
-    def hashlib_method(algorithm):
-        """
-        Return hashlib function for DKIM algorithm.
-        """
-        return {
-            RSA1  : hashlib.sha1,
-            RSA256: hashlib.sha256,
-        }[algorithm]
-
-
-    @staticmethod
-    def hash_name(algorithm):
-        """
-        Return RSA hash name for DKIM algorithm.
-        """
-        return {
-            RSA1  : "SHA-1",
-            RSA256: "SHA-256",
-        }[algorithm]
-
-
-    @staticmethod
-    def hash_func(algorithm):
-        """
-        Return RSA hash name for DKIM algorithm.
-        """
-        return {
-            RSA1  : SHA,
-            RSA256: SHA256,
-        }[algorithm]
-
-
-    @staticmethod
-    def extractTags(data):
-        """
-        Split a DKIM tag list into a dict, removing unneeded whitespace.
-        """
-        # Extract tags from the data
-        splits = [item.strip() for item in data.split(";")]
-        dkim_tags = {}
-        for item in splits:
-            try:
-                name, value = item.split("=", 1)
-                dkim_tags[name.strip()] = value.strip()
-            except ValueError:
-                pass
-        return dkim_tags
-
-
-    @staticmethod
-    def canonicalizeHeader(name, value, remove_b=None):
-        """
-        Canonicalize the header using "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 remove_b: the b= value to remove, or C{None} if no removal needed
-        @type remove_b: C{str} or C{None}
-        """
-
-        # Basic relaxed behavior
-        name = name.lower()
-        value = " ".join(value.split())
-
-        # 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())
-
-        crlf = "" if name == DKIM_SIGNATURE.lower() else "\r\n"
-        return "%s:%s%s" % (name, value, crlf)
-
-
-    @staticmethod
-    def canonicalizeBody(data):
-        if not data.endswith("\r\n"):
-            data += "\r\n"
-        return data
-
-
-    @staticmethod
-    def sign(data, privkey, hashfunc):
-        h = hashfunc.new(data)
-        signer = PKCS1_v1_5.new(privkey)
-        return base64.b64encode(signer.sign(h))
-
-
-    @staticmethod
-    def verify(data, signature, pubkey, hashfunc):
-        h = hashfunc.new(data)
-        verifier = PKCS1_v1_5.new(pubkey)
-        if not verifier.verify(h, base64.b64decode(signature)):
-            raise ValueError()
-
-
-
-class DKIMRequest(ClientRequest):
-    """
-    A ClientRequest that optionally creates a DKIM signature.
-    """
-
-    keys = {}
-
-    def __init__(
-        self,
-        method,
-        uri,
-        headers,
-        stream,
-        domain,
-        selector,
-        key_file,
-        algorithm,
-        sign_headers,
-        useDNSKey,
-        useHTTPKey,
-        usePrivateExchangeKey,
-        expire,
-    ):
-        """
-        Create a DKIM request, which is a regular client request with the additional information needed to sign the message.
-
-        @param method: HTTP method to use
-        @type method: C{str}
-        @param uri: request-URI
-        @type uri: C{str}
-        @param headers: request headers
-        @type headers: L{http_headers}
-        @param stream: body data
-        @type stream: L{Stream}
-        @param domain: the signing domain
-        @type domain: C{str}
-        @param selector: the signing key selector
-        @type selector: C{str}
-        @param key_file: path to a private key file
-        @type key_file: C{str}
-        @param algorithm: the signing algorithm to use
-        @type algorithm: C{str}
-        @param sign_headers: list of header names to sign - to "over sign" a header append a "+" to the name
-        @type sign_headers: C{tuple}
-        @param useDNSKey: whether or not to add DNS TXT lookup as a key lookup option
-        @type useDNSKey: C{bool}
-        @param useHTTPKey: whether or not to add HTTP .well-known as a key lookup option
-        @type useHTTPKey: C{bool}
-        @param usePrivateExchangeKey: whether or not to add private-exchange as a key lookup option
-        @type usePrivateExchangeKey: C{bool}
-        @param expire: number of seconds to expiration of signature
-        @type expire: C{int}
-        """
-        super(DKIMRequest, self).__init__(method, uri, headers, stream)
-        self.domain = domain
-        self.selector = selector
-        self.algorithm = algorithm
-        self.key_file = key_file
-        self.sign_headers = sign_headers
-        self.time = str(int(time.time()))
-        self.expire = str(int(time.time() + expire))
-
-        assert self.domain
-        assert self.selector
-        assert self.algorithm in (RSA1, RSA256,)
-        assert useDNSKey or useHTTPKey or usePrivateExchangeKey
-
-        self.hash_method = DKIMUtils.hashlib_method(self.algorithm)
-        self.hash_name = DKIMUtils.hash_name(self.algorithm)
-        self.hash_func = DKIMUtils.hash_func(self.algorithm)
-
-        self.keyMethods = []
-        if usePrivateExchangeKey:
-            self.keyMethods.append(Q_PRIVATE)
-        if useHTTPKey:
-            self.keyMethods.append(Q_HTTP)
-        if useDNSKey:
-            self.keyMethods.append(Q_DNS)
-
-        self.message_id = str(uuid.uuid4())
-
-
-    @inlineCallbacks
-    def sign(self):
-        """
-        Generate the DKIM headers by signing the request. This should only be called once on the request and there must
-        be no changes to the request (no headers, no body change) after it is called.
-        """
-
-        # Get the headers and the DKIM-Signature tags
-        headers, dkim_tags = (yield self.signatureHeaders())
-
-        # Sign the hash
-        signature = self.generateSignature(headers)
-
-        # Complete the header
-        dkim_tags[-1] = ("b", signature,)
-        dkim_header = "; ".join(["%s=%s" % item for item in dkim_tags])
-        self.headers.addRawHeader(DKIM_SIGNATURE, dkim_header)
-
-        log.debug("DKIM: Generated header: DKIM-Signature:%s" % (dkim_header,))
-        log.debug("DKIM: Signed headers:\n%s" % (headers,))
-
-        returnValue(signature)
-
-
-    @inlineCallbacks
-    def bodyHash(self):
-        """
-        Generate the hash of the request body data.
-        """
-
-        # We need to play a trick with the request stream as we can only read it once. So we
-        # read it, store the value in a MemoryStream, and replace the request's stream with that,
-        # so the data can be read again.
-        data = (yield allDataFromStream(self.stream))
-        self.stream = MemoryStream(data if data is not None else "")
-        self.stream.doStartReading = None
-
-        returnValue(base64.b64encode(self.hash_method(DKIMUtils.canonicalizeBody(data)).digest()))
-
-
-    @inlineCallbacks
-    def signatureHeaders(self):
-        """
-        Generate the headers that are going to be signed as well as the DKIM-Signature tags.
-        """
-
-        # Make sure we have the required iSchedule headers
-        self.headers.addRawHeader(ISCHEDULE_VERSION, ISCHEDULE_VERSION_VALUE)
-        self.headers.addRawHeader(ISCHEDULE_MESSAGE_ID, self.message_id)
-        self.sign_headers += (ISCHEDULE_VERSION, ISCHEDULE_MESSAGE_ID,)
-
-        # Need Cache-Control
-        self.headers.setRawHeaders("Cache-Control", ("no-cache", "no-transform",))
-
-        # Figure out all the existing headers to sign
-        headers = []
-        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)
-
-        # Generate the DKIM header tags we care about
-        dkim_tags = []
-        dkim_tags.append(("v", "1",))
-        dkim_tags.append(("d", self.domain,))
-        dkim_tags.append(("s", self.selector,))
-        dkim_tags.append(("t", self.time,))
-        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(("h", ":".join(sign_headers),))
-        dkim_tags.append(("bh", (yield self.bodyHash()),))
-        dkim_tags.append(("b", "",))
-        dkim_header = "; ".join(["%s=%s" % item for item in dkim_tags])
-
-        headers.append(DKIMUtils.canonicalizeHeader(DKIM_SIGNATURE, dkim_header))
-        headers = "".join(headers)
-
-        returnValue((headers, dkim_tags,))
-
-
-    def generateSignature(self, headers):
-        # Sign the hash
-        if self.key_file not in self.keys:
-            self.keys[self.key_file] = RSA.importKey(open(self.key_file).read())
-        return DKIMUtils.sign(headers, self.keys[self.key_file], self.hash_func)
-
-
-
-class DKIMMissingError(Exception):
-    """
-    Used to indicate that the DKIM-Signature header is not present when
-    attempting verification.
-    """
-    pass
-
-
-
-class DKIMVerificationError(Exception):
-    """
-    Used to indicate a DKIM verification error.
-    """
-    pass
-
-
-
-class DKIMVerifier(object):
-    """
-    Class used to verify an DKIM-signed HTTP request.
-    """
-
-    def __init__(self, request, key_lookup=None, protocol_debug=False):
-        """
-        @param request: The HTTP request to process
-        @type request: L{twext.server.Request}
-        """
-        self.request = request
-        self._debug = protocol_debug
-        self.dkim_tags = {}
-
-        # Prefer private exchange over HTTP over DNS when multiple are present
-        self.key_lookup_methods = (
-            PublicKeyLookup_PrivateExchange,
-            PublicKeyLookup_HTTP_WellKnown,
-            PublicKeyLookup_DNSTXT,
-        ) if key_lookup is None else key_lookup
-
-        self.time = int(time.time())
-
-
-    @inlineCallbacks
-    def verify(self):
-        """
-        @raise: DKIMVerificationError
-        """
-
-        # Check presence of DKIM header
-        self.processDKIMHeader()
-
-        # Extract the set of canonicalized headers being signed
-        headers = self.extractSignedHeaders()
-        log.debug("DKIM: Signed headers:\n%s" % (headers,))
-
-        # Locate the public key
-        pubkey = (yield self.locatePublicKey())
-        if pubkey is None:
-            raise DKIMVerificationError("No public key to verify the DKIM signature")
-
-        # Do header verification
-        try:
-            DKIMUtils.verify(headers, self.dkim_tags["b"], pubkey, self.hash_func)
-        except ValueError:
-            msg = "Could not verify signature"
-            _debug_msg = """
-DKIM-Signature:%s
-
-Headers to evaluate:
-%s
-
-Public key used:
-%s
-""" % (self.request.headers.getRawHeaders(DKIM_SIGNATURE)[0], headers, pubkey._original_data,)
-            log.debug("DKIM: %s:%s" % (msg, _debug_msg,))
-            if self._debug:
-                msg = "%s:%s" % (msg, _debug_msg,)
-            raise DKIMVerificationError(msg)
-
-        # Do body validation
-        data = (yield allDataFromStream(self.request.stream))
-        self.request.stream = MemoryStream(data if data is not None else "")
-        self.request.stream.doStartReading = None
-        body = DKIMUtils.canonicalizeBody(data)
-        bh = base64.b64encode(self.hash_method(body).digest())
-        if bh != self.dkim_tags["bh"]:
-            msg = "Could not verify the DKIM body hash"
-            _debug_msg = """
-DKIM-Signature:%s
-
-Hash Method: %s
-
-Base64 encoded body:
-%s
-""" % (self.request.headers.getRawHeaders(DKIM_SIGNATURE), self.hash_method.__name__, base64.b64encode(body),)
-            log.debug("DKIM: %s:%s" % (msg, _debug_msg,))
-            if self._debug:
-                msg = "%s:%s" % (msg, _debug_msg,)
-            raise DKIMVerificationError(msg)
-
-
-    def processDKIMHeader(self):
-        """
-        Extract the DKIM-Signature header and process the tags.
-
-        @raise: DKIMVerificationError
-        """
-
-        # Check presence of header
-        dkim = self.request.headers.getRawHeaders(DKIM_SIGNATURE)
-        if dkim is None:
-            msg = "No DKIM-Signature header present in the request"
-            log.debug("DKIM: " + msg)
-            raise DKIMMissingError(msg)
-        if len(dkim) != 1:
-            # TODO: This might need to be changed if we ever support forwarding of iSchedule messages - the forwarder
-            # might also sign the message and add its own header
-            msg = "Only one DKIM-Signature allowed in the request"
-            log.debug("DKIM: " + msg)
-            raise DKIMVerificationError(msg)
-        dkim = dkim[0]
-        log.debug("DKIM: Found header: DKIM-Signature:%s" % (dkim,))
-
-        # Extract tags from the header
-        self.dkim_tags = DKIMUtils.extractTags(dkim)
-
-        # Verify validity of tags
-        required_tags = ("v", "a", "b", "bh", "c", "d", "h", "s", "http",)
-        for tag in required_tags:
-            if tag not in self.dkim_tags:
-                msg = "Missing DKIM-Signature tag: %s" % (tag,)
-                log.debug("DKIM: " + msg)
-                raise DKIMVerificationError(msg)
-
-        check_values = {
-            "v": ("1",),
-            "a": (RSA1, RSA256,),
-            "c": ("relaxed", "relaxed/simple",),
-            "q": (Q_DNS, Q_HTTP, Q_PRIVATE,),
-        }
-        for tag, values in check_values.items():
-            if tag not in required_tags and tag not in self.dkim_tags:
-                pass
-
-            # Handle some structured values
-            if tag == "q":
-                test = self.dkim_tags[tag].split(":")
-            else:
-                test = (self.dkim_tags[tag],)
-            for item in test:
-                if item not in values:
-                    msg = "Tag: %s has incorrect value: %s" % (tag, self.dkim_tags[tag],)
-                    log.debug("DKIM: " + msg)
-                    raise DKIMVerificationError(msg)
-
-        # Check expiration
-        if "x" in self.dkim_tags:
-            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)
-
-        # Some useful bits
-        self.hash_method = DKIMUtils.hashlib_method(self.dkim_tags["a"])
-        self.hash_func = DKIMUtils.hash_func(self.dkim_tags["a"])
-        self.key_methods = self.dkim_tags["q"].split(":")
-
-
-    def extractSignedHeaders(self):
-        """
-        Extract the set of headers from the request that are supposed to be signed. Canonicalize them
-        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
-        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
-
-        # 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])
-
-
-    @inlineCallbacks
-    def locatePublicKey(self):
-        """
-        Try to lookup the public key matching the signature.
-        """
-
-        for lookup in self.key_lookup_methods:
-            if lookup.method in self.key_methods or lookup.method == "*":
-                pubkey = (yield lookup(self.dkim_tags).getPublicKey())
-                if pubkey is not None:
-                    returnValue(pubkey)
-        else:
-            returnValue(None)
-
-
-
-class PublicKeyLookup(object):
-    """
-    Abstract base class for public key lookup methods.
-
-    The L{method} attribute indicated the DKIM q= lookup method that the class will support, or if set to "*",
-    the class will handle any q= value.
-    """
-
-    keyCache = {}
-    method = None
-
-    def __init__(self, dkim_tags):
-        self.dkim_tags = dkim_tags
-
-
-    @inlineCallbacks
-    def getPublicKey(self, useCache=True):
-        """
-        Get key from cache or directly do query.
-
-        @param useCache: whether or not to use the cache
-        @type useCache: C{bool}
-        """
-        key = self._getSelectorKey()
-        if key not in PublicKeyLookup.keyCache or not useCache:
-            pubkeys = (yield self._lookupKeys())
-            PublicKeyLookup.keyCache[key] = pubkeys
-
-        returnValue(self._selectKey())
-
-
-    def _getSelectorKey(self):
-        """
-        Get a token used to uniquely identify the key being looked up. Token format will
-        depend on the lookup method.
-        """
-        raise NotImplementedError
-
-
-    def _lookupKeys(self):
-        """
-        Do the key lookup using the actual lookup method. Return a C{list} of C{dict}
-        that contains the key tag-list. Return a L{Deferred}.
-        """
-        raise NotImplementedError
-
-
-    def _selectKey(self):
-        """
-        Select a specific key from the list that best matches the DKIM-Signature tags
-        """
-
-        pubkeys = PublicKeyLookup.keyCache.get(self._getSelectorKey(), [])
-        for pkey in pubkeys:
-            # Check validity
-            if pkey.get("v", "DKIM1") != "DKIM1":
-                continue
-
-            # Check key type
-            if pkey.get("k", "rsa") != "rsa":
-                continue
-
-            # Check valid hash algorithms
-            hashes = set([hash.strip() for hash in pkey.get("h", "sha1:sha256").split(":")])
-            if self.dkim_tags["a"][4:] not in hashes:
-                continue
-
-            # Service type
-            if pkey.get("s", KEY_SERVICE_TYPE) not in ("*", KEY_SERVICE_TYPE,):
-                continue
-
-            # Non-revoked key
-            if len(pkey.get("p", "")) == 0:
-                continue
-
-            return self._makeKey(pkey)
-
-        log.debug("DKIM: No valid public key: %s %s" % (self._getSelectorKey(), pubkeys,))
-        return None
-
-
-    def _makeKey(self, pkey):
-        """
-        Turn the key tag list into an actual RSA public key object
-
-        @param pkey: key tag list
-        @type pkey: C{list}
-        """
-        key_data = """-----BEGIN PUBLIC KEY-----
-%s
------END PUBLIC KEY-----
-""" % ("\n".join(textwrap.wrap(pkey["p"], 64)),)
-
-        try:
-            key = RSA.importKey(key_data)
-            key._original_data = key_data
-            return key
-        except:
-            log.debug("DKIM: Unable to make public key:\n%s" % (key_data,))
-            return None
-
-
-    def flushCache(self):
-        PublicKeyLookup.keyCache = {}
-
-
-
-class PublicKeyLookup_DNSTXT(PublicKeyLookup):
-
-    method = Q_DNS
-
-    def _getSelectorKey(self):
-        """
-        Get a token used to uniquely identify the key being looked up. Token format will
-        depend on the lookup method.
-        """
-        return "%s._domainkey.%s" % (self.dkim_tags["s"], self.dkim_tags["d"],)
-
-
-    @inlineCallbacks
-    def _lookupKeys(self):
-        """
-        Do the key lookup using the actual lookup method.
-        """
-        log.debug("DKIM: TXT lookup: %s" % (self._getSelectorKey(),))
-        data = (yield lookupDataViaTXT(self._getSelectorKey()))
-        log.debug("DKIM: TXT lookup results: %s\n%s" % (self._getSelectorKey(), "\n".join(data),))
-        returnValue(tuple([DKIMUtils.extractTags(line) for line in data]))
-
-
-
-class PublicKeyLookup_HTTP_WellKnown(PublicKeyLookup):
-
-    method = Q_HTTP
-
-    def _getSelectorKey(self):
-        """
-        Get a token used to uniquely identify the key being looked up. Token format will
-        depend on the lookup method.
-        """
-
-        host = ".".join(self.dkim_tags["d"].split(".")[-2:])
-        return "https://%s/.well-known/domainkey/%s/%s" % (host, self.dkim_tags["d"], self.dkim_tags["s"],)
-
-
-    @inlineCallbacks
-    def _getURI(self):
-        """
-        Determine the well-known URI for the public key service.
-        """
-
-        # First we do an SRV lookup for _domainkey to get the public key server host/port
-        result = (yield lookupServerViaSRV(self.dkim_tags["d"], service="_domainkey"))
-        if result is None:
-            log.debug("DKIM: SRV _domainkey failed on: %s trying domain directly" % (self.dkim_tags["d"],))
-            host = self.dkim_tags["d"]
-            port = ""
-            scheme = "https"
-        else:
-            host, port = result
-            scheme = "http" if port in (80, 8008, 8080,) else "https"
-            if port == 80 and scheme == "http" or port == 443 and scheme == "https":
-                port = ""
-            else:
-                port = ":%s" % (port,)
-
-        returnValue("%s://%s%s/.well-known/domainkey/%s/%s" % (scheme, host, port, self.dkim_tags["d"], self.dkim_tags["s"],))
-
-
-    @inlineCallbacks
-    def _lookupKeys(self):
-        """
-        Do the key lookup using the actual lookup method.
-        """
-
-        # First we do an SRV lookup for _domainkey to get the public key server URI
-        uri = (yield self._getURI())
-
-        log.debug("DKIM: HTTP/.well-known lookup: %s" % (uri,))
-        response = (yield getURL(uri))
-        if response is None or response.code / 100 != 2:
-            log.debug("DKIM: Failed http/well-known lookup: %s %s" % (uri, response,))
-            returnValue(())
-
-        ct = response.headers.getRawHeaders("content-type", ("bogus/type",))[0]
-        ct = ct.split(";", 1)
-        ct = ct[0].strip()
-        if ct not in ("text/plain",):
-            log.debug("DKIM: Failed http/well-known lookup: wrong content-type returned %s %s" % (uri, ct,))
-            returnValue(())
-
-        log.debug("DKIM: HTTP/.well-known lookup results: %s\n%s" % (uri, response.data,))
-        returnValue(tuple([DKIMUtils.extractTags(line) for line in response.data.splitlines()]))
-
-
-
-class PublicKeyLookup_PrivateExchange(PublicKeyLookup):
-
-    method = Q_PRIVATE
-    directory = None
-
-    def _getSelectorKey(self):
-        """
-        Get a token used to uniquely identify the key being looked up. Token format will
-        depend on the lookup method.
-        """
-        return "%s#%s" % (self.dkim_tags["d"], self.dkim_tags["s"],)
-
-
-    def _lookupKeys(self):
-        """
-        Key information is stored in a file, one record per line.
-        """
-
-        # Check validity of paths
-        if PublicKeyLookup_PrivateExchange.directory is None:
-            log.debug("DKIM: Failed private-exchange lookup: no directory configured")
-            return succeed(())
-        keyfile = os.path.join(PublicKeyLookup_PrivateExchange.directory, self._getSelectorKey())
-        if not os.path.exists(keyfile):
-            log.debug("DKIM: Failed private-exchange lookup: no path %s" % (keyfile,))
-            return succeed(())
-
-        # Now read the data
-        log.debug("DKIM: Private exchange lookup: %s" % (keyfile,))
-        try:
-            with open(keyfile) as f:
-                keys = f.read()
-        except IOError, e:
-            log.debug("DKIM: Failed private-exchange lookup: could not read %s %s" % (keyfile, e,))
-            return succeed(())
-
-        log.debug("DKIM: Private exchange lookup results: %s\n%s" % (keyfile, keys))
-        return succeed(tuple([DKIMUtils.extractTags(line) for line in keys.splitlines()]))
-
-
-
-class DomainKeyResource (SimpleResource):
-    """
-    Domainkey well-known resource.
-    """
-
-    def __init__(self, domain, selector, pubkeyfile):
-        """
-        """
-        assert domain
-        assert selector
-
-        SimpleResource.__init__(self, principalCollections=None, isdir=True, defaultACL=SimpleResource.allReadACL)
-        self.makeKeyData(domain, selector, pubkeyfile)
-        self.domain = domain
-        self.selector = selector
-
-
-    def makeKeyData(self, domain, selector, pubkeyfile):
-        """
-        Check that a valid key exists, create the TXT record format data and make the needed child resources.
-        """
-
-        # Get data from file
-        try:
-            with open(pubkeyfile) as f:
-                key_data = f.read()
-        except IOError, e:
-            log.error("DKIM: Unable to open the public key file: %s because of %s" % (pubkeyfile, e,))
-            raise
-
-        # Make sure we can parse a valid public key
-        try:
-            RSA.importKey(key_data)
-        except:
-            log.error("DKIM: Invalid public key file: %s" % (pubkeyfile,))
-            raise
-
-        # Make the TXT record
-        key_data = "".join(key_data.strip().splitlines()[1:-1])
-        txt_data = "v=DKIM1; s=ischedule; p=%s\n" % (key_data,)
-
-        # Setup resource hierarchy
-        domainResource = SimpleResource(principalCollections=None, isdir=True, defaultACL=SimpleResource.allReadACL)
-        self.putChild(domain, domainResource)
-
-        selectorResource = SimpleDataResource(principalCollections=None, content_type=MimeType.fromString("text/plain"), data=txt_data, defaultACL=SimpleResource.allReadACL)
-        domainResource.putChild(selector, selectorResource)
-
-
-    def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8")
-
-
-    def render(self, request):
-        output = """<html>
-<head>
-<title>DomainKey Resource</title>
-</head>
-<body>
-<h1>DomainKey Resource.</h1>
-<a href="%s">Domain: %s<br>
-Selector: %s</a>
-</body
-</html>""" % (joinURL(request.uri, self.domain, self.selector), self.domain, self.selector,)
-
-        response = Response(200, {}, output)
-        response.headers.setHeader("content-type", MimeType("text", "html"))
-        return response

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/dkim.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/dkim.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/dkim.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/dkim.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,981 @@
+##
+# Copyright (c) 2012 Apple Inc. All 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.python.log import Logger
+from twext.web2.client.http import ClientRequest
+from twext.web2.dav.util import allDataFromStream, joinURL
+from twext.web2.http import Response
+from twext.web2.http_headers import MimeType
+from twext.web2.stream import MemoryStream
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twistedcaldav.client.geturl import getURL
+from twistedcaldav.config import ConfigurationError
+from twistedcaldav.simpleresource import SimpleResource, SimpleDataResource
+from twistedcaldav.scheduling.ischedule.utils import lookupDataViaTXT, \
+    lookupServerViaSRV
+
+from Crypto.Hash import SHA, SHA256
+from Crypto.PublicKey import RSA
+from Crypto.Signature import PKCS1_v1_5
+
+import base64
+import binascii
+import collections
+import hashlib
+import os
+import textwrap
+import time
+import uuid
+
+"""
+DKIM HTTP message generation and validation,
+"""
+
+log = Logger()
+
+# DKIM/iSchedule Constants
+RSA1 = "rsa-sha1"
+RSA256 = "rsa-sha256"
+Q_DNS = "dns/txt"
+Q_HTTP = "http/well-known"
+Q_PRIVATE = "private-exchange"
+
+KEY_SERVICE_TYPE = "ischedule"
+
+# Headers
+DKIM_SIGNATURE = "DKIM-Signature"
+ISCHEDULE_VERSION = "iSchedule-Version"
+ISCHEDULE_VERSION_VALUE = "1.0"
+ISCHEDULE_MESSAGE_ID = "iSchedule-Message-ID"
+
+
+
+class DKIMUtils(object):
+    """
+    Some useful functions.
+    """
+
+    @staticmethod
+    def validConfiguration(config):
+        if config.Scheduling.iSchedule.DKIM.Enabled:
+
+            if not config.Scheduling.iSchedule.DKIM.Domain and not config.ServerHostName:
+                msg = "DKIM: No domain specified"
+                log.error(msg)
+                raise ConfigurationError(msg)
+
+            if not config.Scheduling.iSchedule.DKIM.KeySelector:
+                msg = "DKIM: No selector specified"
+                log.error(msg)
+                raise ConfigurationError(msg)
+
+            if config.Scheduling.iSchedule.DKIM.SignatureAlgorithm not in (RSA1, RSA256):
+                msg = "DKIM: Invalid algorithm: %s" % (config.Scheduling.iSchedule.SignatureAlgorithm,)
+                log.error(msg)
+                raise ConfigurationError(msg)
+
+            try:
+                with open(config.Scheduling.iSchedule.DKIM.PrivateKeyFile) as f:
+                    key_data = f.read()
+            except IOError, e:
+                msg = "DKIM: Cannot read private key file: %s %s" % (config.Scheduling.iSchedule.DKIM.PrivateKeyFile, e,)
+                log.error(msg)
+                raise ConfigurationError(msg)
+            try:
+                RSA.importKey(key_data)
+            except:
+                msg = "DKIM: Invalid private key file: %s" % (config.Scheduling.iSchedule.DKIM.PrivateKeyFile,)
+                log.error(msg)
+                raise ConfigurationError(msg)
+
+            try:
+                with open(config.Scheduling.iSchedule.DKIM.PublicKeyFile) as f:
+                    key_data = f.read()
+            except IOError, e:
+                msg = "DKIM: Cannot read public key file: %s %s" % (config.Scheduling.iSchedule.DKIM.PublicKeyFile, e,)
+                log.error(msg)
+                raise ConfigurationError(msg)
+            try:
+                RSA.importKey(key_data)
+            except:
+                msg = "DKIM: Invalid public key file: %s" % (config.Scheduling.iSchedule.DKIM.PublicKeyFile,)
+                log.error(msg)
+                raise ConfigurationError(msg)
+
+            if config.Scheduling.iSchedule.DKIM.PrivateExchanges:
+                if not os.path.exists(config.Scheduling.iSchedule.DKIM.PrivateExchanges):
+                    try:
+                        os.makedirs(config.Scheduling.iSchedule.DKIM.PrivateExchanges)
+                    except IOError, e:
+                        msg = "DKIM: Cannot create public key private exchange directory: %s" % (config.Scheduling.iSchedule.DKIM.PrivateExchanges,)
+                        log.error(msg)
+                        raise ConfigurationError(msg)
+                if not os.path.isdir(config.Scheduling.iSchedule.DKIM.PrivateExchanges):
+                    msg = "DKIM: Invalid public key private exchange directory: %s" % (config.Scheduling.iSchedule.DKIM.PrivateExchanges,)
+                    log.error(msg)
+                    raise ConfigurationError(msg)
+                PublicKeyLookup_PrivateExchange.directory = config.Scheduling.iSchedule.DKIM.PrivateExchanges
+
+            log.info("DKIM: Enabled")
+        else:
+            log.info("DKIM: Disabled")
+
+
+    @staticmethod
+    def getConfiguration(config):
+        """
+        Return a tuple of the parameters derived from the config that are used to initialize the DKIMRequest.
+
+        @param config: configuration to look at
+        @type config: L{Config}
+        """
+
+        domain = config.Scheduling.iSchedule.DKIM.Domain if config.Scheduling.iSchedule.DKIM.Domain else config.ServerHostName
+        selector = config.Scheduling.iSchedule.DKIM.KeySelector
+        key_file = config.Scheduling.iSchedule.DKIM.PrivateKeyFile
+        algorithm = config.Scheduling.iSchedule.DKIM.SignatureAlgorithm
+        useDNSKey = config.Scheduling.iSchedule.DKIM.UseDNSKey
+        useHTTPKey = config.Scheduling.iSchedule.DKIM.UseHTTPKey
+        usePrivateExchangeKey = config.Scheduling.iSchedule.DKIM.UsePrivateExchangeKey
+        expire = config.Scheduling.iSchedule.DKIM.ExpireSeconds
+
+        return domain, selector, key_file, algorithm, useDNSKey, useHTTPKey, usePrivateExchangeKey, expire
+
+
+    @staticmethod
+    def hashlib_method(algorithm):
+        """
+        Return hashlib function for DKIM algorithm.
+        """
+        return {
+            RSA1  : hashlib.sha1,
+            RSA256: hashlib.sha256,
+        }[algorithm]
+
+
+    @staticmethod
+    def hash_name(algorithm):
+        """
+        Return RSA hash name for DKIM algorithm.
+        """
+        return {
+            RSA1  : "SHA-1",
+            RSA256: "SHA-256",
+        }[algorithm]
+
+
+    @staticmethod
+    def hash_func(algorithm):
+        """
+        Return RSA hash name for DKIM algorithm.
+        """
+        return {
+            RSA1  : SHA,
+            RSA256: SHA256,
+        }[algorithm]
+
+
+    @staticmethod
+    def extractTags(data):
+        """
+        Split a DKIM tag list into a dict, removing unneeded whitespace.
+        """
+        # Extract tags from the data
+        splits = [item.strip() for item in data.split(";")]
+        dkim_tags = {}
+        for item in splits:
+            try:
+                name, value = item.split("=", 1)
+                dkim_tags[name.strip()] = value.strip()
+            except ValueError:
+                pass
+        return dkim_tags
+
+
+    @staticmethod
+    def canonicalizeHeader(name, value, remove_b=None):
+        """
+        Canonicalize the header using "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 remove_b: the b= value to remove, or C{None} if no removal needed
+        @type remove_b: C{str} or C{None}
+        """
+
+        # Basic relaxed behavior
+        name = name.lower()
+        value = " ".join(value.split())
+
+        # 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())
+
+        crlf = "" if name == DKIM_SIGNATURE.lower() else "\r\n"
+        return "%s:%s%s" % (name, value, crlf)
+
+
+    @staticmethod
+    def canonicalizeBody(data):
+        if not data.endswith("\r\n"):
+            data += "\r\n"
+        return data
+
+
+    @staticmethod
+    def sign(data, privkey, hashfunc):
+        h = hashfunc.new(data)
+        signer = PKCS1_v1_5.new(privkey)
+        return base64.b64encode(signer.sign(h))
+
+
+    @staticmethod
+    def verify(data, signature, pubkey, hashfunc):
+        h = hashfunc.new(data)
+        verifier = PKCS1_v1_5.new(pubkey)
+        if not verifier.verify(h, base64.b64decode(signature)):
+            raise ValueError()
+
+
+
+class DKIMRequest(ClientRequest):
+    """
+    A ClientRequest that optionally creates a DKIM signature.
+    """
+
+    keys = {}
+
+    def __init__(
+        self,
+        method,
+        uri,
+        headers,
+        stream,
+        domain,
+        selector,
+        key_file,
+        algorithm,
+        sign_headers,
+        useDNSKey,
+        useHTTPKey,
+        usePrivateExchangeKey,
+        expire,
+    ):
+        """
+        Create a DKIM request, which is a regular client request with the additional information needed to sign the message.
+
+        @param method: HTTP method to use
+        @type method: C{str}
+        @param uri: request-URI
+        @type uri: C{str}
+        @param headers: request headers
+        @type headers: L{http_headers}
+        @param stream: body data
+        @type stream: L{Stream}
+        @param domain: the signing domain
+        @type domain: C{str}
+        @param selector: the signing key selector
+        @type selector: C{str}
+        @param key_file: path to a private key file
+        @type key_file: C{str}
+        @param algorithm: the signing algorithm to use
+        @type algorithm: C{str}
+        @param sign_headers: list of header names to sign - to "over sign" a header append a "+" to the name
+        @type sign_headers: C{tuple}
+        @param useDNSKey: whether or not to add DNS TXT lookup as a key lookup option
+        @type useDNSKey: C{bool}
+        @param useHTTPKey: whether or not to add HTTP .well-known as a key lookup option
+        @type useHTTPKey: C{bool}
+        @param usePrivateExchangeKey: whether or not to add private-exchange as a key lookup option
+        @type usePrivateExchangeKey: C{bool}
+        @param expire: number of seconds to expiration of signature
+        @type expire: C{int}
+        """
+        super(DKIMRequest, self).__init__(method, uri, headers, stream)
+        self.domain = domain
+        self.selector = selector
+        self.algorithm = algorithm
+        self.key_file = key_file
+        self.sign_headers = sign_headers
+        self.time = str(int(time.time()))
+        self.expire = str(int(time.time() + expire))
+
+        assert self.domain
+        assert self.selector
+        assert self.algorithm in (RSA1, RSA256,)
+        assert useDNSKey or useHTTPKey or usePrivateExchangeKey
+
+        self.hash_method = DKIMUtils.hashlib_method(self.algorithm)
+        self.hash_name = DKIMUtils.hash_name(self.algorithm)
+        self.hash_func = DKIMUtils.hash_func(self.algorithm)
+
+        self.keyMethods = []
+        if usePrivateExchangeKey:
+            self.keyMethods.append(Q_PRIVATE)
+        if useHTTPKey:
+            self.keyMethods.append(Q_HTTP)
+        if useDNSKey:
+            self.keyMethods.append(Q_DNS)
+
+        self.message_id = str(uuid.uuid4())
+
+
+    @inlineCallbacks
+    def sign(self):
+        """
+        Generate the DKIM headers by signing the request. This should only be called once on the request and there must
+        be no changes to the request (no headers, no body change) after it is called.
+        """
+
+        # Get the headers and the DKIM-Signature tags
+        headers, dkim_tags = (yield self.signatureHeaders())
+
+        # Sign the hash
+        signature = self.generateSignature(headers)
+
+        # Complete the header
+        dkim_tags[-1] = ("b", signature,)
+        dkim_header = "; ".join(["%s=%s" % item for item in dkim_tags])
+        self.headers.addRawHeader(DKIM_SIGNATURE, dkim_header)
+
+        log.debug("DKIM: Generated header: DKIM-Signature:%s" % (dkim_header,))
+        log.debug("DKIM: Signed headers:\n%s" % (headers,))
+
+        returnValue(signature)
+
+
+    @inlineCallbacks
+    def bodyHash(self):
+        """
+        Generate the hash of the request body data.
+        """
+
+        # We need to play a trick with the request stream as we can only read it once. So we
+        # read it, store the value in a MemoryStream, and replace the request's stream with that,
+        # so the data can be read again.
+        data = (yield allDataFromStream(self.stream))
+        self.stream = MemoryStream(data if data is not None else "")
+        self.stream.doStartReading = None
+
+        returnValue(base64.b64encode(self.hash_method(DKIMUtils.canonicalizeBody(data)).digest()))
+
+
+    @inlineCallbacks
+    def signatureHeaders(self):
+        """
+        Generate the headers that are going to be signed as well as the DKIM-Signature tags.
+        """
+
+        # Make sure we have the required iSchedule headers
+        self.headers.addRawHeader(ISCHEDULE_VERSION, ISCHEDULE_VERSION_VALUE)
+        self.headers.addRawHeader(ISCHEDULE_MESSAGE_ID, self.message_id)
+        self.sign_headers += (ISCHEDULE_VERSION, ISCHEDULE_MESSAGE_ID,)
+
+        # Need Cache-Control
+        self.headers.setRawHeaders("Cache-Control", ("no-cache", "no-transform",))
+
+        # Figure out all the existing headers to sign
+        headers = []
+        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)
+
+        # Generate the DKIM header tags we care about
+        dkim_tags = []
+        dkim_tags.append(("v", "1",))
+        dkim_tags.append(("d", self.domain,))
+        dkim_tags.append(("s", self.selector,))
+        dkim_tags.append(("t", self.time,))
+        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(("h", ":".join(sign_headers),))
+        dkim_tags.append(("bh", (yield self.bodyHash()),))
+        dkim_tags.append(("b", "",))
+        dkim_header = "; ".join(["%s=%s" % item for item in dkim_tags])
+
+        headers.append(DKIMUtils.canonicalizeHeader(DKIM_SIGNATURE, dkim_header))
+        headers = "".join(headers)
+
+        returnValue((headers, dkim_tags,))
+
+
+    def generateSignature(self, headers):
+        # Sign the hash
+        if self.key_file not in self.keys:
+            self.keys[self.key_file] = RSA.importKey(open(self.key_file).read())
+        return DKIMUtils.sign(headers, self.keys[self.key_file], self.hash_func)
+
+
+
+class DKIMMissingError(Exception):
+    """
+    Used to indicate that the DKIM-Signature header is not present when
+    attempting verification.
+    """
+    pass
+
+
+
+class DKIMVerificationError(Exception):
+    """
+    Used to indicate a DKIM verification error.
+    """
+    pass
+
+
+
+class DKIMVerifier(object):
+    """
+    Class used to verify an DKIM-signed HTTP request.
+    """
+
+    def __init__(self, request, key_lookup=None, protocol_debug=False):
+        """
+        @param request: The HTTP request to process
+        @type request: L{twext.server.Request}
+        """
+        self.request = request
+        self._debug = protocol_debug
+        self.dkim_tags = {}
+
+        # Prefer private exchange over HTTP over DNS when multiple are present
+        self.key_lookup_methods = (
+            PublicKeyLookup_PrivateExchange,
+            PublicKeyLookup_HTTP_WellKnown,
+            PublicKeyLookup_DNSTXT,
+        ) if key_lookup is None else key_lookup
+
+        self.time = int(time.time())
+
+
+    @inlineCallbacks
+    def verify(self):
+        """
+        @raise: DKIMVerificationError
+        """
+
+        # Check presence of DKIM header
+        self.processDKIMHeader()
+
+        # Extract the set of canonicalized headers being signed
+        headers = self.extractSignedHeaders()
+        log.debug("DKIM: Signed headers:\n%s" % (headers,))
+
+        # Locate the public key
+        pubkey = (yield self.locatePublicKey())
+        if pubkey is None:
+            raise DKIMVerificationError("No public key to verify the DKIM signature")
+
+        # Do header verification
+        try:
+            DKIMUtils.verify(headers, self.dkim_tags["b"], pubkey, self.hash_func)
+        except ValueError:
+            msg = "Could not verify signature"
+            _debug_msg = """
+DKIM-Signature:%s
+
+Headers to evaluate:
+%s
+
+Public key used:
+%s
+""" % (self.request.headers.getRawHeaders(DKIM_SIGNATURE)[0], headers, pubkey._original_data,)
+            log.debug("DKIM: %s:%s" % (msg, _debug_msg,))
+            if self._debug:
+                msg = "%s:%s" % (msg, _debug_msg,)
+            raise DKIMVerificationError(msg)
+
+        # Do body validation
+        data = (yield allDataFromStream(self.request.stream))
+        self.request.stream = MemoryStream(data if data is not None else "")
+        self.request.stream.doStartReading = None
+        body = DKIMUtils.canonicalizeBody(data)
+        bh = base64.b64encode(self.hash_method(body).digest())
+        if bh != self.dkim_tags["bh"]:
+            msg = "Could not verify the DKIM body hash"
+            _debug_msg = """
+DKIM-Signature:%s
+
+Hash Method: %s
+
+Base64 encoded body:
+%s
+""" % (self.request.headers.getRawHeaders(DKIM_SIGNATURE), self.hash_method.__name__, base64.b64encode(body),)
+            log.debug("DKIM: %s:%s" % (msg, _debug_msg,))
+            if self._debug:
+                msg = "%s:%s" % (msg, _debug_msg,)
+            raise DKIMVerificationError(msg)
+
+
+    def processDKIMHeader(self):
+        """
+        Extract the DKIM-Signature header and process the tags.
+
+        @raise: DKIMVerificationError
+        """
+
+        # Check presence of header
+        dkim = self.request.headers.getRawHeaders(DKIM_SIGNATURE)
+        if dkim is None:
+            msg = "No DKIM-Signature header present in the request"
+            log.debug("DKIM: " + msg)
+            raise DKIMMissingError(msg)
+        if len(dkim) != 1:
+            # TODO: This might need to be changed if we ever support forwarding of iSchedule messages - the forwarder
+            # might also sign the message and add its own header
+            msg = "Only one DKIM-Signature allowed in the request"
+            log.debug("DKIM: " + msg)
+            raise DKIMVerificationError(msg)
+        dkim = dkim[0]
+        log.debug("DKIM: Found header: DKIM-Signature:%s" % (dkim,))
+
+        # Extract tags from the header
+        self.dkim_tags = DKIMUtils.extractTags(dkim)
+
+        # Verify validity of tags
+        required_tags = ("v", "a", "b", "bh", "c", "d", "h", "s", "http",)
+        for tag in required_tags:
+            if tag not in self.dkim_tags:
+                msg = "Missing DKIM-Signature tag: %s" % (tag,)
+                log.debug("DKIM: " + msg)
+                raise DKIMVerificationError(msg)
+
+        check_values = {
+            "v": ("1",),
+            "a": (RSA1, RSA256,),
+            "c": ("relaxed", "relaxed/simple",),
+            "q": (Q_DNS, Q_HTTP, Q_PRIVATE,),
+        }
+        for tag, values in check_values.items():
+            if tag not in required_tags and tag not in self.dkim_tags:
+                pass
+
+            # Handle some structured values
+            if tag == "q":
+                test = self.dkim_tags[tag].split(":")
+            else:
+                test = (self.dkim_tags[tag],)
+            for item in test:
+                if item not in values:
+                    msg = "Tag: %s has incorrect value: %s" % (tag, self.dkim_tags[tag],)
+                    log.debug("DKIM: " + msg)
+                    raise DKIMVerificationError(msg)
+
+        # Check expiration
+        if "x" in self.dkim_tags:
+            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)
+
+        # Some useful bits
+        self.hash_method = DKIMUtils.hashlib_method(self.dkim_tags["a"])
+        self.hash_func = DKIMUtils.hash_func(self.dkim_tags["a"])
+        self.key_methods = self.dkim_tags["q"].split(":")
+
+
+    def extractSignedHeaders(self):
+        """
+        Extract the set of headers from the request that are supposed to be signed. Canonicalize them
+        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
+        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
+
+        # 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])
+
+
+    @inlineCallbacks
+    def locatePublicKey(self):
+        """
+        Try to lookup the public key matching the signature.
+        """
+
+        for lookup in self.key_lookup_methods:
+            if lookup.method in self.key_methods or lookup.method == "*":
+                pubkey = (yield lookup(self.dkim_tags).getPublicKey())
+                if pubkey is not None:
+                    returnValue(pubkey)
+        else:
+            returnValue(None)
+
+
+
+class PublicKeyLookup(object):
+    """
+    Abstract base class for public key lookup methods.
+
+    The L{method} attribute indicated the DKIM q= lookup method that the class will support, or if set to "*",
+    the class will handle any q= value.
+    """
+
+    keyCache = {}
+    method = None
+
+    def __init__(self, dkim_tags):
+        self.dkim_tags = dkim_tags
+
+
+    @inlineCallbacks
+    def getPublicKey(self, useCache=True):
+        """
+        Get key from cache or directly do query.
+
+        @param useCache: whether or not to use the cache
+        @type useCache: C{bool}
+        """
+        key = self._getSelectorKey()
+        if key not in PublicKeyLookup.keyCache or not useCache:
+            pubkeys = (yield self._lookupKeys())
+            PublicKeyLookup.keyCache[key] = pubkeys
+
+        returnValue(self._selectKey())
+
+
+    def _getSelectorKey(self):
+        """
+        Get a token used to uniquely identify the key being looked up. Token format will
+        depend on the lookup method.
+        """
+        raise NotImplementedError
+
+
+    def _lookupKeys(self):
+        """
+        Do the key lookup using the actual lookup method. Return a C{list} of C{dict}
+        that contains the key tag-list. Return a L{Deferred}.
+        """
+        raise NotImplementedError
+
+
+    def _selectKey(self):
+        """
+        Select a specific key from the list that best matches the DKIM-Signature tags
+        """
+
+        pubkeys = PublicKeyLookup.keyCache.get(self._getSelectorKey(), [])
+        for pkey in pubkeys:
+            # Check validity
+            if pkey.get("v", "DKIM1") != "DKIM1":
+                continue
+
+            # Check key type
+            if pkey.get("k", "rsa") != "rsa":
+                continue
+
+            # Check valid hash algorithms
+            hashes = set([hash.strip() for hash in pkey.get("h", "sha1:sha256").split(":")])
+            if self.dkim_tags["a"][4:] not in hashes:
+                continue
+
+            # Service type
+            if pkey.get("s", KEY_SERVICE_TYPE) not in ("*", KEY_SERVICE_TYPE,):
+                continue
+
+            # Non-revoked key
+            if len(pkey.get("p", "")) == 0:
+                continue
+
+            return self._makeKey(pkey)
+
+        log.debug("DKIM: No valid public key: %s %s" % (self._getSelectorKey(), pubkeys,))
+        return None
+
+
+    def _makeKey(self, pkey):
+        """
+        Turn the key tag list into an actual RSA public key object
+
+        @param pkey: key tag list
+        @type pkey: C{list}
+        """
+        key_data = """-----BEGIN PUBLIC KEY-----
+%s
+-----END PUBLIC KEY-----
+""" % ("\n".join(textwrap.wrap(pkey["p"], 64)),)
+
+        try:
+            key = RSA.importKey(key_data)
+            key._original_data = key_data
+            return key
+        except:
+            log.debug("DKIM: Unable to make public key:\n%s" % (key_data,))
+            return None
+
+
+    def flushCache(self):
+        PublicKeyLookup.keyCache = {}
+
+
+
+class PublicKeyLookup_DNSTXT(PublicKeyLookup):
+
+    method = Q_DNS
+
+    def _getSelectorKey(self):
+        """
+        Get a token used to uniquely identify the key being looked up. Token format will
+        depend on the lookup method.
+        """
+        return "%s._domainkey.%s" % (self.dkim_tags["s"], self.dkim_tags["d"],)
+
+
+    @inlineCallbacks
+    def _lookupKeys(self):
+        """
+        Do the key lookup using the actual lookup method.
+        """
+        log.debug("DKIM: TXT lookup: %s" % (self._getSelectorKey(),))
+        data = (yield lookupDataViaTXT(self._getSelectorKey()))
+        log.debug("DKIM: TXT lookup results: %s\n%s" % (self._getSelectorKey(), "\n".join(data),))
+        returnValue(tuple([DKIMUtils.extractTags(line) for line in data]))
+
+
+
+class PublicKeyLookup_HTTP_WellKnown(PublicKeyLookup):
+
+    method = Q_HTTP
+
+    def _getSelectorKey(self):
+        """
+        Get a token used to uniquely identify the key being looked up. Token format will
+        depend on the lookup method.
+        """
+
+        host = ".".join(self.dkim_tags["d"].split(".")[-2:])
+        return "https://%s/.well-known/domainkey/%s/%s" % (host, self.dkim_tags["d"], self.dkim_tags["s"],)
+
+
+    @inlineCallbacks
+    def _getURI(self):
+        """
+        Determine the well-known URI for the public key service.
+        """
+
+        # First we do an SRV lookup for _domainkey to get the public key server host/port
+        result = (yield lookupServerViaSRV(self.dkim_tags["d"], service="_domainkey"))
+        if result is None:
+            log.debug("DKIM: SRV _domainkey failed on: %s trying domain directly" % (self.dkim_tags["d"],))
+            host = self.dkim_tags["d"]
+            port = ""
+            scheme = "https"
+        else:
+            host, port = result
+            scheme = "http" if port in (80, 8008, 8080,) else "https"
+            if port == 80 and scheme == "http" or port == 443 and scheme == "https":
+                port = ""
+            else:
+                port = ":%s" % (port,)
+
+        returnValue("%s://%s%s/.well-known/domainkey/%s/%s" % (scheme, host, port, self.dkim_tags["d"], self.dkim_tags["s"],))
+
+
+    @inlineCallbacks
+    def _lookupKeys(self):
+        """
+        Do the key lookup using the actual lookup method.
+        """
+
+        # First we do an SRV lookup for _domainkey to get the public key server URI
+        uri = (yield self._getURI())
+
+        log.debug("DKIM: HTTP/.well-known lookup: %s" % (uri,))
+        response = (yield getURL(uri))
+        if response is None or response.code / 100 != 2:
+            log.debug("DKIM: Failed http/well-known lookup: %s %s" % (uri, response,))
+            returnValue(())
+
+        ct = response.headers.getRawHeaders("content-type", ("bogus/type",))[0]
+        ct = ct.split(";", 1)
+        ct = ct[0].strip()
+        if ct not in ("text/plain",):
+            log.debug("DKIM: Failed http/well-known lookup: wrong content-type returned %s %s" % (uri, ct,))
+            returnValue(())
+
+        log.debug("DKIM: HTTP/.well-known lookup results: %s\n%s" % (uri, response.data,))
+        returnValue(tuple([DKIMUtils.extractTags(line) for line in response.data.splitlines()]))
+
+
+
+class PublicKeyLookup_PrivateExchange(PublicKeyLookup):
+
+    method = Q_PRIVATE
+    directory = None
+
+    def _getSelectorKey(self):
+        """
+        Get a token used to uniquely identify the key being looked up. Token format will
+        depend on the lookup method.
+        """
+        return "%s#%s" % (self.dkim_tags["d"], self.dkim_tags["s"],)
+
+
+    def _lookupKeys(self):
+        """
+        Key information is stored in a file, one record per line.
+        """
+
+        # Check validity of paths
+        if PublicKeyLookup_PrivateExchange.directory is None:
+            log.debug("DKIM: Failed private-exchange lookup: no directory configured")
+            return succeed(())
+        keyfile = os.path.join(PublicKeyLookup_PrivateExchange.directory, self._getSelectorKey())
+        if not os.path.exists(keyfile):
+            log.debug("DKIM: Failed private-exchange lookup: no path %s" % (keyfile,))
+            return succeed(())
+
+        # Now read the data
+        log.debug("DKIM: Private exchange lookup: %s" % (keyfile,))
+        try:
+            with open(keyfile) as f:
+                keys = f.read()
+        except IOError, e:
+            log.debug("DKIM: Failed private-exchange lookup: could not read %s %s" % (keyfile, e,))
+            return succeed(())
+
+        log.debug("DKIM: Private exchange lookup results: %s\n%s" % (keyfile, keys))
+        return succeed(tuple([DKIMUtils.extractTags(line) for line in keys.splitlines()]))
+
+
+
+class DomainKeyResource (SimpleResource):
+    """
+    Domainkey well-known resource.
+    """
+
+    def __init__(self, domain, selector, pubkeyfile):
+        """
+        """
+        assert domain
+        assert selector
+
+        SimpleResource.__init__(self, principalCollections=None, isdir=True, defaultACL=SimpleResource.allReadACL)
+        self.makeKeyData(domain, selector, pubkeyfile)
+        self.domain = domain
+        self.selector = selector
+
+
+    def makeKeyData(self, domain, selector, pubkeyfile):
+        """
+        Check that a valid key exists, create the TXT record format data and make the needed child resources.
+        """
+
+        # Get data from file
+        try:
+            with open(pubkeyfile) as f:
+                key_data = f.read()
+        except IOError, e:
+            log.error("DKIM: Unable to open the public key file: %s because of %s" % (pubkeyfile, e,))
+            raise
+
+        # Make sure we can parse a valid public key
+        try:
+            RSA.importKey(key_data)
+        except:
+            log.error("DKIM: Invalid public key file: %s" % (pubkeyfile,))
+            raise
+
+        # Make the TXT record
+        key_data = "".join(key_data.strip().splitlines()[1:-1])
+        txt_data = "v=DKIM1; s=ischedule; p=%s\n" % (key_data,)
+
+        # Setup resource hierarchy
+        domainResource = SimpleResource(principalCollections=None, isdir=True, defaultACL=SimpleResource.allReadACL)
+        self.putChild(domain, domainResource)
+
+        selectorResource = SimpleDataResource(principalCollections=None, content_type=MimeType.fromString("text/plain"), data=txt_data, defaultACL=SimpleResource.allReadACL)
+        domainResource.putChild(selector, selectorResource)
+
+
+    def contentType(self):
+        return MimeType.fromString("text/html; charset=utf-8")
+
+
+    def render(self, request):
+        output = """<html>
+<head>
+<title>DomainKey Resource</title>
+</head>
+<body>
+<h1>DomainKey Resource.</h1>
+<a href="%s">Domain: %s<br>
+Selector: %s</a>
+</body
+</html>""" % (joinURL(request.uri, self.domain, self.selector), self.domain, self.selector,)
+
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/localservers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/localservers.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/localservers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,323 +0,0 @@
-##
-# Copyright (c) 2011-2012 Apple Inc. All 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.python.log import Logger
-from twisted.internet.abstract import isIPAddress
-from twistedcaldav.client.pool import installPool
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.xmlutil import readXML
-import socket
-import urlparse
-from twistedcaldav.scheduling.ischedule.utils import getIPsFromHost
-
-"""
-XML based server configuration file handling.
-
-This is used in an environment where more than one server is being used within a single domain. i.e., all
-the principals across the whole domain need to be able to directly schedule each other and know of each others
-existence. A common scenario would be a production server and a development/test server.
-
-Each server is identified by an id and url. The id is used when assigning principals to a specific server. Each
-server can also support multiple partitions, and each of those is identified by an id and url, with the id also
-being used to assign principals to a specific partition.
-
-These servers support the concept of "partitioning" and "podding".
-
-A "partitioned" service is one that spreads its
-users out across multiple stores and does reverse proxying of incoming requests to the appropriate partitioned host.
-All servers within the same partition have to be running the same version of the software etc.
-
-A "podded" service is one where different groups of users are hosted on different servers, which may be of
-different versions etc. A "pod" may itself be "partitioned", but the partitioning is "invisible" to the outside world.
-"""
-
-__all__ = [
-    "Servers",
-]
-
-log = Logger()
-
-SERVER_SECRET_HEADER = "X-CALENDARSERVER-ISCHEDULE"
-
-class ServersDB(object):
-    """
-    Represents the set of servers within the same domain.
-    """
-
-    def __init__(self):
-
-        self._servers = {}
-        self._xmlFile = None
-        self._thisServer = None
-
-
-    def load(self, xmlFile=None, ignoreIPLookupFailures=False):
-        if self._xmlFile is None or xmlFile is not None:
-            self._servers = {}
-            if xmlFile:
-                self._xmlFile = xmlFile
-            else:
-                self._xmlFile = fullServerPath(
-                    config.ConfigRoot,
-                    config.Servers.ConfigFile
-                )
-        self._servers = ServersParser.parse(self._xmlFile, ignoreIPLookupFailures=ignoreIPLookupFailures)
-        for server in self._servers.values():
-            if server.thisServer:
-                self._thisServer = server
-                break
-        else:
-            raise ValueError("No server in %s matches this server." % (self._xmlFile,))
-
-
-    def clear(self):
-        self._servers = {}
-        self._xmlFile = None
-        self._thisServer = None
-
-
-    def getServerById(self, id):
-        return self._servers.get(id)
-
-
-    def getServerURIById(self, id):
-        try:
-            return self._servers[id].uri
-        except KeyError:
-            return None
-
-
-    def getThisServer(self):
-        return self._thisServer
-
-Servers = ServersDB()   # Global server DB
-
-
-
-class Server(object):
-    """
-    Represents a server which may itself be partitioned.
-    """
-
-    def __init__(self):
-        self.id = None
-        self.uri = None
-        self.thisServer = False
-        self.ips = set()
-        self.allowed_from_ips = set()
-        self.shared_secret = None
-        self.partitions = {}
-        self.partitions_ips = set()
-        self.isImplicit = True
-
-
-    def check(self, ignoreIPLookupFailures=False):
-        # Check whether this matches the current server
-        parsed_uri = urlparse.urlparse(self.uri)
-        if parsed_uri.hostname == config.ServerHostName:
-            if parsed_uri.scheme == "http":
-                if config.HTTPPort:
-                    self.thisServer = parsed_uri.port in (config.HTTPPort,) + tuple(config.BindHTTPPorts)
-            elif parsed_uri.scheme == "https":
-                if config.SSLPort:
-                    self.thisServer = parsed_uri.port in (config.SSLPort,) + tuple(config.BindSSLPorts)
-
-        # Need to cache IP addresses
-        try:
-            ips = getIPsFromHost(parsed_uri.hostname)
-        except socket.gaierror, e:
-            msg = "Unable to lookup ip-addr for server '%s': %s" % (parsed_uri.hostname, str(e))
-            log.error(msg)
-            if ignoreIPLookupFailures:
-                ips = ()
-            else:
-                raise ValueError(msg)
-        self.ips = set(ips)
-
-        actual_ips = set()
-        for item in self.allowed_from_ips:
-            if not isIPAddress(item):
-                try:
-                    ips = getIPsFromHost(item)
-                except socket.gaierror, e:
-                    msg = "Unable to lookup ip-addr for allowed-from '%s': %s" % (item, str(e))
-                    log.error(msg)
-                    if not ignoreIPLookupFailures:
-                        raise ValueError(msg)
-                else:
-                    actual_ips.update(ips)
-            else:
-                actual_ips.add(item)
-        self.allowed_from_ips = actual_ips
-
-        for uri in self.partitions.values():
-            parsed_uri = urlparse.urlparse(uri)
-            try:
-                ips = getIPsFromHost(parsed_uri.hostname)
-            except socket.gaierror, e:
-                msg = "Unable to lookup ip-addr for partition '%s': %s" % (parsed_uri.hostname, str(e))
-                log.error(msg)
-                if ignoreIPLookupFailures:
-                    ips = ()
-                else:
-                    raise ValueError(msg)
-            self.partitions_ips.update(ips)
-
-
-    def checkThisIP(self, ip):
-        """
-        Check that the passed in IP address corresponds to this server or one of its partitions.
-        """
-        return (ip in self.ips) or (ip in self.partitions_ips)
-
-
-    def hasAllowedFromIP(self):
-        return len(self.allowed_from_ips) > 0
-
-
-    def checkAllowedFromIP(self, ip):
-        return ip in self.allowed_from_ips
-
-
-    def checkSharedSecret(self, request):
-
-        # Get header from the request
-        request_secret = request.headers.getRawHeaders(SERVER_SECRET_HEADER)
-
-        if request_secret is not None and self.shared_secret is None:
-            log.error("iSchedule request included unexpected %s header" % (SERVER_SECRET_HEADER,))
-            return False
-        elif request_secret is None and self.shared_secret is not None:
-            log.error("iSchedule request did not include required %s header" % (SERVER_SECRET_HEADER,))
-            return False
-        elif (request_secret[0] if request_secret else None) != self.shared_secret:
-            log.error("iSchedule request %s header did not match" % (SERVER_SECRET_HEADER,))
-            return False
-        else:
-            return True
-
-
-    def secretHeader(self):
-        """
-        Return a tuple of header name, header value
-        """
-        return (SERVER_SECRET_HEADER, self.shared_secret,)
-
-
-    def addPartition(self, id, uri):
-        self.partitions[id] = uri
-
-
-    def getPartitionURIForId(self, id):
-        return self.partitions.get(id)
-
-
-    def isPartitioned(self):
-        return len(self.partitions) != 0
-
-
-    def installReverseProxies(self, ownUID, maxClients):
-
-        for partition, url in self.partitions.iteritems():
-            if partition != ownUID:
-                installPool(
-                    partition,
-                    url,
-                    maxClients,
-                )
-
-
-
-ELEMENT_SERVERS = "servers"
-ELEMENT_SERVER = "server"
-ELEMENT_ID = "id"
-ELEMENT_URI = "uri"
-ELEMENT_ALLOWED_FROM = "allowed-from"
-ELEMENT_SHARED_SECRET = "shared-secret"
-ELEMENT_PARTITIONS = "partitions"
-ELEMENT_PARTITION = "partition"
-ATTR_IMPLICIT = "implicit"
-ATTR_VALUE_YES = "yes"
-ATTR_VALUE_NO = "no"
-
-class ServersParser(object):
-    """
-    Servers configuration file parser.
-    """
-    @staticmethod
-    def parse(xmlFile, ignoreIPLookupFailures=False):
-
-        results = {}
-
-        # Read in XML
-        try:
-            _ignore_tree, servers_node = readXML(xmlFile, ELEMENT_SERVERS)
-        except ValueError, e:
-            log.error("XML parse error for '%s' because: %s" % (xmlFile, e,), raiseException=RuntimeError)
-
-        for child in servers_node.getchildren():
-
-            if child.tag != ELEMENT_SERVER:
-                log.error("Unknown server type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
-            server = Server()
-            server.isImplicit = child.get(ATTR_IMPLICIT, ATTR_VALUE_YES) == ATTR_VALUE_YES
-
-            for node in child.getchildren():
-                if node.tag == ELEMENT_ID:
-                    server.id = node.text
-                elif node.tag == ELEMENT_URI:
-                    server.uri = node.text
-                elif node.tag == ELEMENT_ALLOWED_FROM:
-                    server.allowed_from_ips.add(node.text)
-                elif node.tag == ELEMENT_SHARED_SECRET:
-                    server.shared_secret = node.text
-                elif node.tag == ELEMENT_PARTITIONS:
-                    ServersParser._parsePartition(xmlFile, node, server)
-                else:
-                    log.error("Invalid element '%s' in servers file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
-
-            if server.id is None or server.uri is None:
-                log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
-            server.check(ignoreIPLookupFailures=ignoreIPLookupFailures)
-            results[server.id] = server
-
-        return results
-
-
-    @staticmethod
-    def _parsePartition(xmlFile, partitions, server):
-
-        for child in partitions.getchildren():
-
-            if child.tag != ELEMENT_PARTITION:
-                log.error("Unknown partition type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
-            id = None
-            uri = None
-            for node in child.getchildren():
-                if node.tag == ELEMENT_ID:
-                    id = node.text
-                elif node.tag == ELEMENT_URI:
-                    uri = node.text
-                else:
-                    log.error("Invalid element '%s' in augment file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
-
-            if id is None or uri is None:
-                log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
-            server.addPartition(id, uri)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/localservers.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/localservers.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/localservers.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/localservers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,323 @@
+##
+# Copyright (c) 2011-2012 Apple Inc. All 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.python.log import Logger
+from twisted.internet.abstract import isIPAddress
+from twistedcaldav.client.pool import installPool
+from twistedcaldav.config import config, fullServerPath
+from twistedcaldav.xmlutil import readXML
+import socket
+import urlparse
+from twistedcaldav.scheduling.ischedule.utils import getIPsFromHost
+
+"""
+XML based server configuration file handling.
+
+This is used in an environment where more than one server is being used within a single domain. i.e., all
+the principals across the whole domain need to be able to directly schedule each other and know of each others
+existence. A common scenario would be a production server and a development/test server.
+
+Each server is identified by an id and url. The id is used when assigning principals to a specific server. Each
+server can also support multiple partitions, and each of those is identified by an id and url, with the id also
+being used to assign principals to a specific partition.
+
+These servers support the concept of "partitioning" and "podding".
+
+A "partitioned" service is one that spreads its
+users out across multiple stores and does reverse proxying of incoming requests to the appropriate partitioned host.
+All servers within the same partition have to be running the same version of the software etc.
+
+A "podded" service is one where different groups of users are hosted on different servers, which may be of
+different versions etc. A "pod" may itself be "partitioned", but the partitioning is "invisible" to the outside world.
+"""
+
+__all__ = [
+    "Servers",
+]
+
+log = Logger()
+
+SERVER_SECRET_HEADER = "X-CALENDARSERVER-ISCHEDULE"
+
+class ServersDB(object):
+    """
+    Represents the set of servers within the same domain.
+    """
+
+    def __init__(self):
+
+        self._servers = {}
+        self._xmlFile = None
+        self._thisServer = None
+
+
+    def load(self, xmlFile=None, ignoreIPLookupFailures=False):
+        if self._xmlFile is None or xmlFile is not None:
+            self._servers = {}
+            if xmlFile:
+                self._xmlFile = xmlFile
+            else:
+                self._xmlFile = fullServerPath(
+                    config.ConfigRoot,
+                    config.Servers.ConfigFile
+                )
+        self._servers = ServersParser.parse(self._xmlFile, ignoreIPLookupFailures=ignoreIPLookupFailures)
+        for server in self._servers.values():
+            if server.thisServer:
+                self._thisServer = server
+                break
+        else:
+            raise ValueError("No server in %s matches this server." % (self._xmlFile,))
+
+
+    def clear(self):
+        self._servers = {}
+        self._xmlFile = None
+        self._thisServer = None
+
+
+    def getServerById(self, id):
+        return self._servers.get(id)
+
+
+    def getServerURIById(self, id):
+        try:
+            return self._servers[id].uri
+        except KeyError:
+            return None
+
+
+    def getThisServer(self):
+        return self._thisServer
+
+Servers = ServersDB()   # Global server DB
+
+
+
+class Server(object):
+    """
+    Represents a server which may itself be partitioned.
+    """
+
+    def __init__(self):
+        self.id = None
+        self.uri = None
+        self.thisServer = False
+        self.ips = set()
+        self.allowed_from_ips = set()
+        self.shared_secret = None
+        self.partitions = {}
+        self.partitions_ips = set()
+        self.isImplicit = True
+
+
+    def check(self, ignoreIPLookupFailures=False):
+        # Check whether this matches the current server
+        parsed_uri = urlparse.urlparse(self.uri)
+        if parsed_uri.hostname == config.ServerHostName:
+            if parsed_uri.scheme == "http":
+                if config.HTTPPort:
+                    self.thisServer = parsed_uri.port in (config.HTTPPort,) + tuple(config.BindHTTPPorts)
+            elif parsed_uri.scheme == "https":
+                if config.SSLPort:
+                    self.thisServer = parsed_uri.port in (config.SSLPort,) + tuple(config.BindSSLPorts)
+
+        # Need to cache IP addresses
+        try:
+            ips = getIPsFromHost(parsed_uri.hostname)
+        except socket.gaierror, e:
+            msg = "Unable to lookup ip-addr for server '%s': %s" % (parsed_uri.hostname, str(e))
+            log.error(msg)
+            if ignoreIPLookupFailures:
+                ips = ()
+            else:
+                raise ValueError(msg)
+        self.ips = set(ips)
+
+        actual_ips = set()
+        for item in self.allowed_from_ips:
+            if not isIPAddress(item):
+                try:
+                    ips = getIPsFromHost(item)
+                except socket.gaierror, e:
+                    msg = "Unable to lookup ip-addr for allowed-from '%s': %s" % (item, str(e))
+                    log.error(msg)
+                    if not ignoreIPLookupFailures:
+                        raise ValueError(msg)
+                else:
+                    actual_ips.update(ips)
+            else:
+                actual_ips.add(item)
+        self.allowed_from_ips = actual_ips
+
+        for uri in self.partitions.values():
+            parsed_uri = urlparse.urlparse(uri)
+            try:
+                ips = getIPsFromHost(parsed_uri.hostname)
+            except socket.gaierror, e:
+                msg = "Unable to lookup ip-addr for partition '%s': %s" % (parsed_uri.hostname, str(e))
+                log.error(msg)
+                if ignoreIPLookupFailures:
+                    ips = ()
+                else:
+                    raise ValueError(msg)
+            self.partitions_ips.update(ips)
+
+
+    def checkThisIP(self, ip):
+        """
+        Check that the passed in IP address corresponds to this server or one of its partitions.
+        """
+        return (ip in self.ips) or (ip in self.partitions_ips)
+
+
+    def hasAllowedFromIP(self):
+        return len(self.allowed_from_ips) > 0
+
+
+    def checkAllowedFromIP(self, ip):
+        return ip in self.allowed_from_ips
+
+
+    def checkSharedSecret(self, request):
+
+        # Get header from the request
+        request_secret = request.headers.getRawHeaders(SERVER_SECRET_HEADER)
+
+        if request_secret is not None and self.shared_secret is None:
+            log.error("iSchedule request included unexpected %s header" % (SERVER_SECRET_HEADER,))
+            return False
+        elif request_secret is None and self.shared_secret is not None:
+            log.error("iSchedule request did not include required %s header" % (SERVER_SECRET_HEADER,))
+            return False
+        elif (request_secret[0] if request_secret else None) != self.shared_secret:
+            log.error("iSchedule request %s header did not match" % (SERVER_SECRET_HEADER,))
+            return False
+        else:
+            return True
+
+
+    def secretHeader(self):
+        """
+        Return a tuple of header name, header value
+        """
+        return (SERVER_SECRET_HEADER, self.shared_secret,)
+
+
+    def addPartition(self, id, uri):
+        self.partitions[id] = uri
+
+
+    def getPartitionURIForId(self, id):
+        return self.partitions.get(id)
+
+
+    def isPartitioned(self):
+        return len(self.partitions) != 0
+
+
+    def installReverseProxies(self, ownUID, maxClients):
+
+        for partition, url in self.partitions.iteritems():
+            if partition != ownUID:
+                installPool(
+                    partition,
+                    url,
+                    maxClients,
+                )
+
+
+
+ELEMENT_SERVERS = "servers"
+ELEMENT_SERVER = "server"
+ELEMENT_ID = "id"
+ELEMENT_URI = "uri"
+ELEMENT_ALLOWED_FROM = "allowed-from"
+ELEMENT_SHARED_SECRET = "shared-secret"
+ELEMENT_PARTITIONS = "partitions"
+ELEMENT_PARTITION = "partition"
+ATTR_IMPLICIT = "implicit"
+ATTR_VALUE_YES = "yes"
+ATTR_VALUE_NO = "no"
+
+class ServersParser(object):
+    """
+    Servers configuration file parser.
+    """
+    @staticmethod
+    def parse(xmlFile, ignoreIPLookupFailures=False):
+
+        results = {}
+
+        # Read in XML
+        try:
+            _ignore_tree, servers_node = readXML(xmlFile, ELEMENT_SERVERS)
+        except ValueError, e:
+            log.error("XML parse error for '%s' because: %s" % (xmlFile, e,), raiseException=RuntimeError)
+
+        for child in servers_node.getchildren():
+
+            if child.tag != ELEMENT_SERVER:
+                log.error("Unknown server type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
+
+            server = Server()
+            server.isImplicit = child.get(ATTR_IMPLICIT, ATTR_VALUE_YES) == ATTR_VALUE_YES
+
+            for node in child.getchildren():
+                if node.tag == ELEMENT_ID:
+                    server.id = node.text
+                elif node.tag == ELEMENT_URI:
+                    server.uri = node.text
+                elif node.tag == ELEMENT_ALLOWED_FROM:
+                    server.allowed_from_ips.add(node.text)
+                elif node.tag == ELEMENT_SHARED_SECRET:
+                    server.shared_secret = node.text
+                elif node.tag == ELEMENT_PARTITIONS:
+                    ServersParser._parsePartition(xmlFile, node, server)
+                else:
+                    log.error("Invalid element '%s' in servers file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
+
+            if server.id is None or server.uri is None:
+                log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
+
+            server.check(ignoreIPLookupFailures=ignoreIPLookupFailures)
+            results[server.id] = server
+
+        return results
+
+
+    @staticmethod
+    def _parsePartition(xmlFile, partitions, server):
+
+        for child in partitions.getchildren():
+
+            if child.tag != ELEMENT_PARTITION:
+                log.error("Unknown partition type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
+
+            id = None
+            uri = None
+            for node in child.getchildren():
+                if node.tag == ELEMENT_ID:
+                    id = node.text
+                elif node.tag == ELEMENT_URI:
+                    uri = node.text
+                else:
+                    log.error("Invalid element '%s' in augment file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
+
+            if id is None or uri is None:
+                log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
+
+            server.addPartition(id, uri)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/remoteservers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/remoteservers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,227 +0,0 @@
-##
-# Copyright (c) 2006-2012 Apple Inc. All 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.python.filepath import CachingFilePath as FilePath
-
-from twext.python.log import Logger
-
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav import xmlutil
-
-"""
-XML based iSchedule configuration file handling. This is for handling of remote servers. The localservers.py module
-handles servers that are local (partitioned or podded).
-"""
-
-__all__ = [
-    "IScheduleServers",
-]
-
-log = Logger()
-
-
-
-class IScheduleServers(object):
-
-    _fileInfo = None
-    _xmlFile = None
-    _servers = None
-    _domainMap = None
-
-    def __init__(self):
-
-        self._loadConfig()
-
-
-    def _loadConfig(self):
-        if config.Scheduling.iSchedule.RemoteServers:
-            if IScheduleServers._servers is None:
-                IScheduleServers._xmlFile = FilePath(
-                    fullServerPath(
-                        config.ConfigRoot,
-                        config.Scheduling.iSchedule.RemoteServers,
-                    )
-                )
-            if IScheduleServers._xmlFile.exists():
-                IScheduleServers._xmlFile.restat()
-                fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
-                if fileInfo != IScheduleServers._fileInfo:
-                    parser = IScheduleServersParser(IScheduleServers._xmlFile)
-                    IScheduleServers._servers = parser.servers
-                    self._mapDomains()
-                    IScheduleServers._fileInfo = fileInfo
-            else:
-                IScheduleServers._servers = ()
-                IScheduleServers._domainMap = {}
-
-        else:
-            IScheduleServers._servers = ()
-            IScheduleServers._domainMap = {}
-
-
-    def _mapDomains(self):
-        IScheduleServers._domainMap = {}
-        for server in IScheduleServers._servers:
-            for domain in server.domains:
-                IScheduleServers._domainMap[domain] = server
-
-
-    def mapDomain(self, domain):
-        """
-        Map a calendar user address domain to a suitable server that can
-        handle server-to-server requests for that user.
-        """
-        return IScheduleServers._domainMap.get(domain)
-
-ELEMENT_SERVERS = "servers"
-ELEMENT_SERVER = "server"
-ELEMENT_URI = "uri"
-ELEMENT_AUTHENTICATION = "authentication"
-ATTRIBUTE_TYPE = "type"
-ATTRIBUTE_BASICAUTH = "basic"
-ELEMENT_USER = "user"
-ELEMENT_PASSWORD = "password"
-ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
-ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
-ELEMENT_DOMAINS = "domains"
-ELEMENT_DOMAIN = "domain"
-ELEMENT_CLIENT_HOSTS = "hosts"
-ELEMENT_HOST = "host"
-
-
-
-class IScheduleServersParser(object):
-    """
-    Server-to-server configuration file parser.
-    """
-    def __repr__(self):
-        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
-
-
-    def __init__(self, xmlFile):
-
-        self.servers = []
-
-        # Read in XML
-        _ignore_etree, servers_node = xmlutil.readXML(xmlFile.path, ELEMENT_SERVERS)
-        self._parseXML(servers_node)
-
-
-    def _parseXML(self, node):
-        """
-        Parse the XML root node from the server-to-server configuration document.
-        @param node: the L{Node} to parse.
-        """
-
-        for child in node.getchildren():
-            if child.tag == ELEMENT_SERVER:
-                self.servers.append(IScheduleServerRecord())
-                self.servers[-1].parseXML(child)
-
-
-
-class IScheduleServerRecord (object):
-    """
-    Contains server-to-server details.
-    """
-    def __init__(self, uri=None):
-        """
-        @param recordType: record type for directory entry.
-        """
-        self.uri = ""
-        self.authentication = None
-        self.allow_from = False
-        self.allow_to = True
-        self.domains = []
-        self.client_hosts = []
-        self.unNormalizeAddresses = True
-        self.moreHeaders = []
-
-        if uri:
-            self.uri = uri
-            self._parseDetails()
-
-
-    def details(self):
-        return (self.ssl, self.host, self.port, self.path,)
-
-
-    def redirect(self, location):
-        """
-        Permanent redirect for the lifetime of this record.
-        """
-        self.uri = location
-        self._parseDetails()
-
-
-    def parseXML(self, node):
-        for child in node.getchildren():
-            if child.tag == ELEMENT_URI:
-                self.uri = child.text
-            elif child.tag == ELEMENT_AUTHENTICATION:
-                self._parseAuthentication(child)
-            elif child.tag == ELEMENT_ALLOW_REQUESTS_FROM:
-                self.allow_from = True
-            elif child.tag == ELEMENT_ALLOW_REQUESTS_TO:
-                self.allow_to = True
-            elif child.tag == ELEMENT_DOMAINS:
-                self._parseList(child, ELEMENT_DOMAIN, self.domains)
-            elif child.tag == ELEMENT_CLIENT_HOSTS:
-                self._parseList(child, ELEMENT_HOST, self.client_hosts)
-            else:
-                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child.tag,))
-
-        self._parseDetails()
-
-
-    def _parseList(self, node, element_name, appendto):
-        for child in node.getchildren():
-            if child.tag == element_name:
-                appendto.append(child.text)
-
-
-    def _parseAuthentication(self, node):
-        if node.get(ATTRIBUTE_TYPE) != ATTRIBUTE_BASICAUTH:
-            return
-
-        for child in node.getchildren():
-            if child.tag == ELEMENT_USER:
-                user = child.text
-            elif child.tag == ELEMENT_PASSWORD:
-                password = child.text
-
-        self.authentication = ("basic", user, password,)
-
-
-    def _parseDetails(self):
-        # Extract scheme, host, port and path
-        if self.uri.startswith("http://"):
-            self.ssl = False
-            rest = self.uri[7:]
-        elif self.uri.startswith("https://"):
-            self.ssl = True
-            rest = self.uri[8:]
-
-        splits = rest.split("/", 1)
-        hostport = splits[0].split(":")
-        self.host = hostport[0]
-        if len(hostport) > 1:
-            self.port = int(hostport[1])
-        else:
-            self.port = {False: 80, True: 443}[self.ssl]
-        self.path = "/"
-        if len(splits) > 1:
-            self.path += splits[1]

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/remoteservers.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/remoteservers.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/remoteservers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,227 @@
+##
+# Copyright (c) 2006-2012 Apple Inc. All 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.python.filepath import CachingFilePath as FilePath
+
+from twext.python.log import Logger
+
+from twistedcaldav.config import config, fullServerPath
+from twistedcaldav import xmlutil
+
+"""
+XML based iSchedule configuration file handling. This is for handling of remote servers. The localservers.py module
+handles servers that are local (partitioned or podded).
+"""
+
+__all__ = [
+    "IScheduleServers",
+]
+
+log = Logger()
+
+
+
+class IScheduleServers(object):
+
+    _fileInfo = None
+    _xmlFile = None
+    _servers = None
+    _domainMap = None
+
+    def __init__(self):
+
+        self._loadConfig()
+
+
+    def _loadConfig(self):
+        if config.Scheduling.iSchedule.RemoteServers:
+            if IScheduleServers._servers is None:
+                IScheduleServers._xmlFile = FilePath(
+                    fullServerPath(
+                        config.ConfigRoot,
+                        config.Scheduling.iSchedule.RemoteServers,
+                    )
+                )
+            if IScheduleServers._xmlFile.exists():
+                IScheduleServers._xmlFile.restat()
+                fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
+                if fileInfo != IScheduleServers._fileInfo:
+                    parser = IScheduleServersParser(IScheduleServers._xmlFile)
+                    IScheduleServers._servers = parser.servers
+                    self._mapDomains()
+                    IScheduleServers._fileInfo = fileInfo
+            else:
+                IScheduleServers._servers = ()
+                IScheduleServers._domainMap = {}
+
+        else:
+            IScheduleServers._servers = ()
+            IScheduleServers._domainMap = {}
+
+
+    def _mapDomains(self):
+        IScheduleServers._domainMap = {}
+        for server in IScheduleServers._servers:
+            for domain in server.domains:
+                IScheduleServers._domainMap[domain] = server
+
+
+    def mapDomain(self, domain):
+        """
+        Map a calendar user address domain to a suitable server that can
+        handle server-to-server requests for that user.
+        """
+        return IScheduleServers._domainMap.get(domain)
+
+ELEMENT_SERVERS = "servers"
+ELEMENT_SERVER = "server"
+ELEMENT_URI = "uri"
+ELEMENT_AUTHENTICATION = "authentication"
+ATTRIBUTE_TYPE = "type"
+ATTRIBUTE_BASICAUTH = "basic"
+ELEMENT_USER = "user"
+ELEMENT_PASSWORD = "password"
+ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
+ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
+ELEMENT_DOMAINS = "domains"
+ELEMENT_DOMAIN = "domain"
+ELEMENT_CLIENT_HOSTS = "hosts"
+ELEMENT_HOST = "host"
+
+
+
+class IScheduleServersParser(object):
+    """
+    Server-to-server configuration file parser.
+    """
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+
+    def __init__(self, xmlFile):
+
+        self.servers = []
+
+        # Read in XML
+        _ignore_etree, servers_node = xmlutil.readXML(xmlFile.path, ELEMENT_SERVERS)
+        self._parseXML(servers_node)
+
+
+    def _parseXML(self, node):
+        """
+        Parse the XML root node from the server-to-server configuration document.
+        @param node: the L{Node} to parse.
+        """
+
+        for child in node.getchildren():
+            if child.tag == ELEMENT_SERVER:
+                self.servers.append(IScheduleServerRecord())
+                self.servers[-1].parseXML(child)
+
+
+
+class IScheduleServerRecord (object):
+    """
+    Contains server-to-server details.
+    """
+    def __init__(self, uri=None):
+        """
+        @param recordType: record type for directory entry.
+        """
+        self.uri = ""
+        self.authentication = None
+        self.allow_from = False
+        self.allow_to = True
+        self.domains = []
+        self.client_hosts = []
+        self.unNormalizeAddresses = True
+        self.moreHeaders = []
+
+        if uri:
+            self.uri = uri
+            self._parseDetails()
+
+
+    def details(self):
+        return (self.ssl, self.host, self.port, self.path,)
+
+
+    def redirect(self, location):
+        """
+        Permanent redirect for the lifetime of this record.
+        """
+        self.uri = location
+        self._parseDetails()
+
+
+    def parseXML(self, node):
+        for child in node.getchildren():
+            if child.tag == ELEMENT_URI:
+                self.uri = child.text
+            elif child.tag == ELEMENT_AUTHENTICATION:
+                self._parseAuthentication(child)
+            elif child.tag == ELEMENT_ALLOW_REQUESTS_FROM:
+                self.allow_from = True
+            elif child.tag == ELEMENT_ALLOW_REQUESTS_TO:
+                self.allow_to = True
+            elif child.tag == ELEMENT_DOMAINS:
+                self._parseList(child, ELEMENT_DOMAIN, self.domains)
+            elif child.tag == ELEMENT_CLIENT_HOSTS:
+                self._parseList(child, ELEMENT_HOST, self.client_hosts)
+            else:
+                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child.tag,))
+
+        self._parseDetails()
+
+
+    def _parseList(self, node, element_name, appendto):
+        for child in node.getchildren():
+            if child.tag == element_name:
+                appendto.append(child.text)
+
+
+    def _parseAuthentication(self, node):
+        if node.get(ATTRIBUTE_TYPE) != ATTRIBUTE_BASICAUTH:
+            return
+
+        for child in node.getchildren():
+            if child.tag == ELEMENT_USER:
+                user = child.text
+            elif child.tag == ELEMENT_PASSWORD:
+                password = child.text
+
+        self.authentication = ("basic", user, password,)
+
+
+    def _parseDetails(self):
+        # Extract scheme, host, port and path
+        if self.uri.startswith("http://"):
+            self.ssl = False
+            rest = self.uri[7:]
+        elif self.uri.startswith("https://"):
+            self.ssl = True
+            rest = self.uri[8:]
+
+        splits = rest.split("/", 1)
+        hostport = splits[0].split(":")
+        self.host = hostport[0]
+        if len(hostport) > 1:
+            self.port = int(hostport[1])
+        else:
+            self.port = {False: 80, True: 443}[self.ssl]
+        self.path = "/"
+        if len(splits) > 1:
+            self.path += splits[1]

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,245 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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 pycalendar.datetime import PyCalendarDateTime
-from pycalendar.timezone import PyCalendarTimezone
-from twext.web2 import responsecode
-from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.http import Response, HTTPError, StatusResponse, XMLResponse
-from twext.web2.http_headers import MimeType
-from twisted.internet.defer import succeed, returnValue, inlineCallbacks
-from twistedcaldav import caldavxml
-from twistedcaldav.config import config
-from twistedcaldav.extensions import DAVResource, \
-    DAVResourceWithoutChildrenMixin
-from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
-from twistedcaldav.scheduling.caldav.resource import deliverSchedulePrivilegeSet
-from twistedcaldav.scheduling.ischedule.scheduler import IScheduleScheduler
-from txdav.xml import element as davxml
-import twistedcaldav.scheduling.ischedule.xml  as ischedulexml
-
-__all__ = [
-    "IScheduleInboxResource",
-]
-
-class IScheduleInboxResource (ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
-    """
-    iSchedule Inbox resource.
-
-    Extends L{DAVResource} to provide iSchedule inbox functionality.
-    """
-
-    def __init__(self, parent, store):
-        """
-        @param parent: the parent resource of this one.
-        """
-        assert parent is not None
-
-        DAVResource.__init__(self, principalCollections=parent.principalCollections())
-
-        self.parent = parent
-        self._newStore = store
-
-
-    def deadProperties(self):
-        if not hasattr(self, "_dead_properties"):
-            self._dead_properties = NonePropertyStore(self)
-        return self._dead_properties
-
-
-    def etag(self):
-        return succeed(None)
-
-
-    def checkPreconditions(self, request):
-        return None
-
-
-    def resourceType(self):
-        return davxml.ResourceType.ischeduleinbox
-
-
-    def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8")
-
-
-    def isCollection(self):
-        return False
-
-
-    def isCalendarCollection(self):
-        return False
-
-
-    def isPseudoCalendarCollection(self):
-        return False
-
-
-    def principalForCalendarUserAddress(self, address):
-        for principalCollection in self.principalCollections():
-            principal = principalCollection.principalForCalendarUserAddress(address)
-            if principal is not None:
-                return principal
-        return None
-
-
-    def render(self, request):
-        output = """<html>
-<head>
-<title>Server To Server Inbox Resource</title>
-</head>
-<body>
-<h1>Server To Server Inbox Resource.</h1>
-</body
-</html>"""
-
-        response = Response(200, {}, output)
-        response.headers.setHeader("content-type", MimeType("text", "html"))
-        return response
-
-
-    def http_GET(self, request):
-        """
-        The iSchedule GET method.
-        """
-
-        if not request.args:
-            # Do normal GET behavior
-            return self.render(request)
-
-        action = request.args.get("action", ("",))
-        if len(action) != 1:
-            raise HTTPError(StatusResponse(
-                responsecode.BAD_REQUEST,
-                "Invalid action parameter",
-            ))
-        action = action[0]
-
-        action = {
-            "capabilities"  : self.doCapabilities,
-        }.get(action, None)
-
-        if action is None:
-            raise HTTPError(StatusResponse(
-                responsecode.BAD_REQUEST,
-                "Unknown action action parameter",
-            ))
-
-        return action(request)
-
-
-    def doCapabilities(self, request):
-        """
-        Return a list of all timezones known to the server.
-        """
-
-        # Determine min/max date-time for iSchedule
-        now = PyCalendarDateTime.getNowUTC()
-        minDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
-        minDateTime.offsetYear(-1)
-        maxDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
-        maxDateTime.offsetYear(10)
-
-        result = ischedulexml.QueryResult(
-
-            ischedulexml.Capabilities(
-                ischedulexml.Versions(
-                    ischedulexml.Version.fromString("1.0"),
-                ),
-                ischedulexml.SchedulingMessages(
-                    ischedulexml.Component(
-                        ischedulexml.Method(name="REQUEST"),
-                        ischedulexml.Method(name="CANCEL"),
-                        ischedulexml.Method(name="REPLY"),
-                        name="VEVENT"
-                    ),
-                    ischedulexml.Component(
-                        ischedulexml.Method(name="REQUEST"),
-                        ischedulexml.Method(name="CANCEL"),
-                        ischedulexml.Method(name="REPLY"),
-                        name="VTODO"
-                    ),
-                    ischedulexml.Component(
-                        ischedulexml.Method(name="REQUEST"),
-                        name="VFREEBUSY"
-                    ),
-                ),
-                ischedulexml.CalendarDataTypes(
-                    ischedulexml.CalendarDataType(**{
-                            "content-type": "text/calendar",
-                            "version": "2.0",
-                    }),
-                ),
-                ischedulexml.Attachments(
-                    ischedulexml.External(),
-                ),
-                ischedulexml.MaxContentLength.fromString(config.MaxResourceSize),
-                ischedulexml.MinDateTime.fromString(minDateTime.getText()),
-                ischedulexml.MaxDateTime.fromString(maxDateTime.getText()),
-                ischedulexml.MaxInstances.fromString(config.MaxAllowedInstances),
-                ischedulexml.MaxRecipients.fromString(config.MaxAttendeesPerInstance),
-                ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")),
-            ),
-        )
-        return XMLResponse(responsecode.OK, result)
-
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The server-to-server POST method.
-        """
-
-        # This is a server-to-server scheduling operation.
-        scheduler = IScheduleScheduler(request, self)
-
-        # Need a transaction to work with
-        txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
-        request._newStoreTransaction = txn
-
-        # Do the POST processing treating this as a non-local schedule
-        try:
-            result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
-        except Exception, e:
-            yield txn.abort()
-            raise e
-        else:
-            yield txn.commit()
-        returnValue(result.response())
-
-    ##
-    # ACL
-    ##
-
-
-    def supportedPrivileges(self, request):
-        return succeed(deliverSchedulePrivilegeSet)
-
-
-    def defaultAccessControlList(self):
-        privs = (
-            davxml.Privilege(davxml.Read()),
-            davxml.Privilege(caldavxml.ScheduleDeliver()),
-        )
-
-        return davxml.ACL(
-            # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(*privs),
-                davxml.Protected(),
-            ),
-        )

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/resource.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/resource.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,245 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All 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 pycalendar.datetime import PyCalendarDateTime
+from pycalendar.timezone import PyCalendarTimezone
+from twext.web2 import responsecode
+from twext.web2.dav.noneprops import NonePropertyStore
+from twext.web2.http import Response, HTTPError, StatusResponse, XMLResponse
+from twext.web2.http_headers import MimeType
+from twisted.internet.defer import succeed, returnValue, inlineCallbacks
+from twistedcaldav import caldavxml
+from twistedcaldav.config import config
+from twistedcaldav.extensions import DAVResource, \
+    DAVResourceWithoutChildrenMixin
+from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
+from twistedcaldav.scheduling.caldav.resource import deliverSchedulePrivilegeSet
+from twistedcaldav.scheduling.ischedule.scheduler import IScheduleScheduler
+from txdav.xml import element as davxml
+import twistedcaldav.scheduling.ischedule.xml  as ischedulexml
+
+__all__ = [
+    "IScheduleInboxResource",
+]
+
+class IScheduleInboxResource (ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
+    """
+    iSchedule Inbox resource.
+
+    Extends L{DAVResource} to provide iSchedule inbox functionality.
+    """
+
+    def __init__(self, parent, store):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
+
+        DAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+        self.parent = parent
+        self._newStore = store
+
+
+    def deadProperties(self):
+        if not hasattr(self, "_dead_properties"):
+            self._dead_properties = NonePropertyStore(self)
+        return self._dead_properties
+
+
+    def etag(self):
+        return succeed(None)
+
+
+    def checkPreconditions(self, request):
+        return None
+
+
+    def resourceType(self):
+        return davxml.ResourceType.ischeduleinbox
+
+
+    def contentType(self):
+        return MimeType.fromString("text/html; charset=utf-8")
+
+
+    def isCollection(self):
+        return False
+
+
+    def isCalendarCollection(self):
+        return False
+
+
+    def isPseudoCalendarCollection(self):
+        return False
+
+
+    def principalForCalendarUserAddress(self, address):
+        for principalCollection in self.principalCollections():
+            principal = principalCollection.principalForCalendarUserAddress(address)
+            if principal is not None:
+                return principal
+        return None
+
+
+    def render(self, request):
+        output = """<html>
+<head>
+<title>Server To Server Inbox Resource</title>
+</head>
+<body>
+<h1>Server To Server Inbox Resource.</h1>
+</body
+</html>"""
+
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
+
+
+    def http_GET(self, request):
+        """
+        The iSchedule GET method.
+        """
+
+        if not request.args:
+            # Do normal GET behavior
+            return self.render(request)
+
+        action = request.args.get("action", ("",))
+        if len(action) != 1:
+            raise HTTPError(StatusResponse(
+                responsecode.BAD_REQUEST,
+                "Invalid action parameter",
+            ))
+        action = action[0]
+
+        action = {
+            "capabilities"  : self.doCapabilities,
+        }.get(action, None)
+
+        if action is None:
+            raise HTTPError(StatusResponse(
+                responsecode.BAD_REQUEST,
+                "Unknown action action parameter",
+            ))
+
+        return action(request)
+
+
+    def doCapabilities(self, request):
+        """
+        Return a list of all timezones known to the server.
+        """
+
+        # Determine min/max date-time for iSchedule
+        now = PyCalendarDateTime.getNowUTC()
+        minDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
+        minDateTime.offsetYear(-1)
+        maxDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
+        maxDateTime.offsetYear(10)
+
+        result = ischedulexml.QueryResult(
+
+            ischedulexml.Capabilities(
+                ischedulexml.Versions(
+                    ischedulexml.Version.fromString("1.0"),
+                ),
+                ischedulexml.SchedulingMessages(
+                    ischedulexml.Component(
+                        ischedulexml.Method(name="REQUEST"),
+                        ischedulexml.Method(name="CANCEL"),
+                        ischedulexml.Method(name="REPLY"),
+                        name="VEVENT"
+                    ),
+                    ischedulexml.Component(
+                        ischedulexml.Method(name="REQUEST"),
+                        ischedulexml.Method(name="CANCEL"),
+                        ischedulexml.Method(name="REPLY"),
+                        name="VTODO"
+                    ),
+                    ischedulexml.Component(
+                        ischedulexml.Method(name="REQUEST"),
+                        name="VFREEBUSY"
+                    ),
+                ),
+                ischedulexml.CalendarDataTypes(
+                    ischedulexml.CalendarDataType(**{
+                            "content-type": "text/calendar",
+                            "version": "2.0",
+                    }),
+                ),
+                ischedulexml.Attachments(
+                    ischedulexml.External(),
+                ),
+                ischedulexml.MaxContentLength.fromString(config.MaxResourceSize),
+                ischedulexml.MinDateTime.fromString(minDateTime.getText()),
+                ischedulexml.MaxDateTime.fromString(maxDateTime.getText()),
+                ischedulexml.MaxInstances.fromString(config.MaxAllowedInstances),
+                ischedulexml.MaxRecipients.fromString(config.MaxAttendeesPerInstance),
+                ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")),
+            ),
+        )
+        return XMLResponse(responsecode.OK, result)
+
+
+    @inlineCallbacks
+    def http_POST(self, request):
+        """
+        The server-to-server POST method.
+        """
+
+        # This is a server-to-server scheduling operation.
+        scheduler = IScheduleScheduler(request, self)
+
+        # Need a transaction to work with
+        txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
+        request._newStoreTransaction = txn
+
+        # Do the POST processing treating this as a non-local schedule
+        try:
+            result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
+        except Exception, e:
+            yield txn.abort()
+            raise e
+        else:
+            yield txn.commit()
+        returnValue(result.response())
+
+    ##
+    # ACL
+    ##
+
+
+    def supportedPrivileges(self, request):
+        return succeed(deliverSchedulePrivilegeSet)
+
+
+    def defaultAccessControlList(self):
+        privs = (
+            davxml.Privilege(davxml.Read()),
+            davxml.Privilege(caldavxml.ScheduleDeliver()),
+        )
+
+        return davxml.ACL(
+            # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
+            davxml.ACE(
+                davxml.Principal(davxml.All()),
+                davxml.Grant(*privs),
+                davxml.Protected(),
+            ),
+        )

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/scheduler.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/scheduler.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/scheduler.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,455 +0,0 @@
-
-# Copyright (c) 2005-2012 Apple Inc. All 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.python.log import Logger
-from twext.web2 import responsecode
-from twext.web2.http import HTTPError, Response
-from twisted.internet.abstract import isIPAddress
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twistedcaldav.scheduling import addressmapping
-from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
-from twistedcaldav.scheduling.cuaddress import calendarUserFromPrincipal
-from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServers
-from twistedcaldav.scheduling.scheduler import RemoteScheduler, \
-    ScheduleResponseQueue
-import twistedcaldav.scheduling.ischedule.xml as ixml
-from twistedcaldav.scheduling.ischedule.localservers import Servers
-from twistedcaldav.util import normalizationLookup
-from txdav.xml import element as davxml
-import itertools
-import re
-import socket
-import urlparse
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule.dkim import DKIMVerifier, \
-    DKIMVerificationError, DKIMMissingError
-from twext.web2.http_headers import MimeType
-from twistedcaldav.scheduling.ischedule.xml import ischedule_namespace
-from txdav.xml.base import WebDAVUnknownElement
-from twistedcaldav.scheduling.ischedule.utils import getIPsFromHost
-from twistedcaldav.scheduling.ischedule import xml
-from twistedcaldav.ical import normalizeCUAddress
-
-"""
-L{IScheduleScheduler} - handles deliveries for scheduling messages being POSTed to the iSchedule inbox.
-"""
-
-__all__ = [
-    "IScheduleScheduler",
-]
-
-
-log = Logger()
-
-class ErrorResponse(Response):
-    """
-    A L{Response} object which contains a status code and a L{element.Error}
-    element.
-    Renders itself as a DAV:error XML document.
-    """
-    error = None
-    unregistered = True     # base class is already registered
-
-    def __init__(self, code, error, description=None):
-        """
-        @param code: a response code.
-        @param error: an L{WebDAVElement} identifying the error, or a
-            tuple C{(namespace, name)} with which to create an empty element
-            denoting the error.  (The latter is useful in the case of
-            preconditions and postconditions, not all of which have defined
-            XML element classes.)
-        @param description: an optional string that, if present, will get
-            wrapped in a (twisted_dav_namespace, error-description) element.
-        """
-        if type(error) is tuple:
-            xml_namespace, xml_name = error
-            error = WebDAVUnknownElement()
-            error.namespace = xml_namespace
-            error.name = xml_name
-
-        self.description = description
-        if self.description:
-            output = ixml.Error(error, ixml.ResponseDescription(self.description)).toxml()
-        else:
-            output = ixml.Error(error).toxml()
-
-        Response.__init__(self, code=code, stream=output)
-
-        self.headers.setHeader("content-type", MimeType("text", "xml"))
-
-        self.error = error
-
-
-    def __repr__(self):
-        return "<%s %s %s>" % (self.__class__.__name__, self.code, self.error.sname())
-
-
-
-class IScheduleResponseQueue (ScheduleResponseQueue):
-    """
-    Stores a list of (typically error) responses for use in a
-    L{ScheduleResponse}.
-    """
-
-    schedule_response_element = xml.ScheduleResponse
-    response_element = xml.Response
-    recipient_element = xml.Recipient
-    recipient_uses_href = False
-    request_status_element = xml.RequestStatus
-    error_element = xml.Error
-    response_description_element = xml.ResponseDescription
-    calendar_data_element = xml.CalendarData
-
-
-
-class IScheduleScheduler(RemoteScheduler):
-
-    scheduleResponse = IScheduleResponseQueue
-
-    errorResponse = ErrorResponse
-
-    errorElements = {
-        "originator-missing": (ischedule_namespace, "originator-missing"),
-        "originator-invalid": (ischedule_namespace, "originator-invalid"),
-        "originator-denied": (ischedule_namespace, "originator-denied"),
-        "recipient-missing": (ischedule_namespace, "recipient-missing"),
-        "recipient-invalid": (ischedule_namespace, "recipient-invalid"),
-        "organizer-denied": (ischedule_namespace, "organizer-denied"),
-        "attendee-denied": (ischedule_namespace, "attendee-denied"),
-        "invalid-calendar-data-type": (ischedule_namespace, "invalid-calendar-data-type"),
-        "invalid-calendar-data": (ischedule_namespace, "invalid-calendar-data"),
-        "invalid-scheduling-message": (ischedule_namespace, "invalid-scheduling-message"),
-        "max-recipients": (ischedule_namespace, "max-recipients"),
-    }
-
-    @inlineCallbacks
-    def doSchedulingViaPOST(self, transaction, use_request_headers=False):
-        """
-        Carry out iSchedule specific processing.
-        """
-
-        self.verified = False
-        if config.Scheduling.iSchedule.DKIM.Enabled:
-            verifier = DKIMVerifier(self.request, protocol_debug=config.Scheduling.iSchedule.DKIM.ProtocolDebug)
-            try:
-                yield verifier.verify()
-                self.verified = True
-
-            except DKIMMissingError:
-                # Carry on processing, but we will do extra checks on the originator as we would
-                # when DKIM is not enabled, so that any local policy via remoteservers.xml can be used.
-                pass
-
-            except DKIMVerificationError, e:
-                # If DKIM is enabled and there was a DKIM header present, then fail
-                msg = "Failed to verify DKIM signature"
-                _debug_msg = str(e)
-                log.debug("%s:%s" % (msg, _debug_msg,))
-                if config.Scheduling.iSchedule.DKIM.ProtocolDebug:
-                    msg = "%s:%s" % (msg, _debug_msg,)
-                raise HTTPError(self.errorResponse(
-                    responsecode.FORBIDDEN,
-                    (ischedule_namespace, "verification-failed"),
-                    msg,
-                ))
-
-        result = (yield super(IScheduleScheduler, self).doSchedulingViaPOST(transaction, use_request_headers))
-        returnValue(result)
-
-
-    def loadFromRequestHeaders(self):
-        """
-        Load Originator and Recipient from request headers.
-        """
-        super(IScheduleScheduler, self).loadFromRequestHeaders()
-
-        if self.request.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
-            self.request.doing_attendee_refresh = 1
-
-
-    def preProcessCalendarData(self):
-        """
-        For data coming in from outside we need to normalize the calendar user addresses so that later iTIP
-        processing will match calendar users against those in stored calendar data. Only do that for invites
-        not freebusy.
-        """
-
-        if not self.checkForFreeBusy():
-            # Need to normalize the calendar data and recipient values to keep those in sync,
-            # as we might later try to match them
-            self.calendar.normalizeCalendarUserAddresses(normalizationLookup, self.resource.principalForCalendarUserAddress)
-
-
-    def loadRecipientsFromRequestHeaders(self):
-        """
-        Need to normalize the calendar data and recipient values to keep those in sync,
-        as we might later try to match them
-        """
-        super(IScheduleScheduler, self).loadRecipientsFromRequestHeaders()
-        self.recipients = [normalizeCUAddress(recipient, normalizationLookup, self.resource.principalForCalendarUserAddress) for recipient in self.recipients]
-
-
-    def checkAuthorization(self):
-        # Must have an unauthenticated user
-        if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
-            log.err("Authenticated originators not allowed: %s" % (self.originator,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["originator-denied"],
-                "Authentication not allowed",
-            ))
-
-
-    @inlineCallbacks
-    def checkOriginator(self):
-        """
-        Check the validity of the Originator header.
-        """
-
-        # For remote requests we do not allow the originator to be a local user or one within our domain.
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
-        if originatorPrincipal or localUser:
-            if originatorPrincipal.locallyHosted():
-                log.err("Cannot use originator that is on this server: %s" % (self.originator,))
-                raise HTTPError(self.errorResponse(
-                    responsecode.FORBIDDEN,
-                    self.errorElements["originator-denied"],
-                    "Originator cannot be local to server",
-                ))
-            else:
-                self.originator = calendarUserFromPrincipal(self.originator, originatorPrincipal)
-                self._validAlternateServer(originatorPrincipal)
-        else:
-            self.originator = RemoteCalendarUser(self.originator)
-            self._validiScheduleServer()
-
-
-    def _validiScheduleServer(self):
-        """
-        Check the validity of the iSchedule host.
-        """
-
-        # Check for DKIM verification first and treat as valid
-        if self.verified:
-            return
-
-        # We will only accept originator in known domains.
-        servermgr = IScheduleServers()
-        server = servermgr.mapDomain(self.originator.domain)
-        if not server or not server.allow_from:
-            log.err("Originator not on recognized server: %s" % (self.originator,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["originator-denied"],
-                "Originator not recognized by server",
-            ))
-        else:
-            # Get the request IP and map to hostname.
-            clientip = self.request.remoteAddr.host
-
-            # First compare as dotted IP
-            matched = False
-            compare_with = (server.host,) + tuple(server.client_hosts)
-            if clientip in compare_with:
-                matched = True
-            else:
-                # Now do hostname lookup
-                try:
-                    host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
-                    for host in itertools.chain((host,), aliases):
-                        # Try simple match first
-                        if host in compare_with:
-                            matched = True
-                            break
-
-                        # Try pattern match next
-                        for pattern in compare_with:
-                            try:
-                                if re.match(pattern, host) is not None:
-                                    matched = True
-                                    break
-                            except re.error:
-                                log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
-                        else:
-                            continue
-                        break
-                except socket.herror, e:
-                    log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-
-            if not matched:
-                log.err("Originator not on allowed server: %s" % (self.originator,))
-                raise HTTPError(self.errorResponse(
-                    responsecode.FORBIDDEN,
-                    self.errorElements["originator-denied"],
-                    "Originator not allowed to send to this server",
-                ))
-
-
-    def _validAlternateServer(self, principal):
-        """
-        Check the validity of the partitioned host.
-        """
-
-        # Extract expected host/port. This will be the partitionURI, or if no partitions,
-        # the serverURI
-        expected_uri = principal.partitionURI()
-        if expected_uri is None:
-            expected_uri = principal.serverURI()
-        expected_uri = urlparse.urlparse(expected_uri)
-
-        # Get the request IP and map to hostname.
-        clientip = self.request.remoteAddr.host
-
-        # Check against this server (or any of its partitions). We need this because an external iTIP message
-        # may be addressed to users on different partitions, and the node receiving the iTIP message will need to
-        # forward it to the partition nodes, thus the client ip seen by the partitions will in fact be the initial
-        # receiving node.
-        matched = False
-        if Servers.getThisServer().checkThisIP(clientip):
-            matched = True
-
-        # Checked allowed IPs - if any were defined we only check against them, we do not
-        # go on to check the expected server host ip
-        elif Servers.getThisServer().hasAllowedFromIP():
-            matched = Servers.getThisServer().checkAllowedFromIP(clientip)
-            if not matched:
-                log.error("Invalid iSchedule connection from client: %s" % (clientip,))
-
-        # Next compare as dotted IP
-        elif isIPAddress(expected_uri.hostname):
-            if clientip == expected_uri.hostname:
-                matched = True
-        else:
-            # Now do expected hostname -> IP lookup
-            try:
-                # So now try the lookup of the expected host
-                for ip in getIPsFromHost(expected_uri.hostname):
-                    if ip == clientip:
-                        matched = True
-                        break
-            except socket.herror, e:
-                log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-
-        # Check possible shared secret
-        if matched and not Servers.getThisServer().checkSharedSecret(self.request):
-            log.err("Invalid iSchedule shared secret")
-            matched = False
-
-        if not matched:
-            log.err("Originator not on allowed server: %s" % (self.originator,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["originator-denied"],
-                "Originator not allowed to send to this server",
-            ))
-
-
-    @inlineCallbacks
-    def checkOrganizerAsOriginator(self):
-        """
-        Check the validity of the ORGANIZER value. ORGANIZER must not be local.
-        """
-
-        # Verify that the ORGANIZER's cu address does not map to a valid user
-        organizer = self.calendar.getOrganizer()
-        if organizer:
-            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
-            if organizerPrincipal:
-                if organizerPrincipal.locallyHosted():
-                    log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
-                    raise HTTPError(self.errorResponse(
-                        responsecode.FORBIDDEN,
-                        self.errorElements["organizer-denied"],
-                        "Organizer is not local to server",
-                    ))
-                else:
-                    # Check that the origin server is the correct partition
-                    self.organizer = calendarUserFromPrincipal(organizer, organizerPrincipal)
-                    self._validAlternateServer(self.organizer.principal)
-            else:
-                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
-                if localUser:
-                    log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendar,))
-                    raise HTTPError(self.errorResponse(
-                        responsecode.FORBIDDEN,
-                        self.errorElements["organizer-denied"],
-                        "Organizer not allowed to be originator",
-                    ))
-                else:
-                    self.organizer = RemoteCalendarUser(organizer)
-        else:
-            log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["organizer-denied"],
-                "No organizer in calendar data",
-            ))
-
-
-    @inlineCallbacks
-    def checkAttendeeAsOriginator(self):
-        """
-        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
-        Only local attendees are allowed for message originating from this server.
-        """
-
-        # Attendee cannot be local.
-        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
-        if attendeePrincipal:
-            if attendeePrincipal.locallyHosted():
-                log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))
-                raise HTTPError(self.errorResponse(
-                    responsecode.FORBIDDEN,
-                    self.errorElements["attendee-denied"],
-                    "Local attendee cannot send to this server",
-                ))
-            else:
-                self._validAlternateServer(attendeePrincipal)
-        else:
-            localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
-            if localUser:
-                log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
-                raise HTTPError(self.errorResponse(
-                    responsecode.FORBIDDEN,
-                    self.errorElements["attendee-denied"],
-                    "Attendee not allowed to schedule",
-                ))
-
-        # TODO: in this case we should check that the ORGANIZER is the sole recipient.
-
-
-    @inlineCallbacks
-    def securityChecks(self):
-        """
-        Check that the originator has the appropriate rights to send this type of iTIP message.
-        """
-
-        # Prevent spoofing of ORGANIZER with specific METHODs when local
-        if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
-            yield self.checkOrganizerAsOriginator()
-
-        # Prevent spoofing when doing reply-like METHODs
-        elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
-            yield self.checkAttendeeAsOriginator()
-
-        else:
-            log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["invalid-scheduling-message"],
-                "Unknown iTIP method",
-            ))

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/scheduler.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/scheduler.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/scheduler.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/scheduler.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,455 @@
+
+# Copyright (c) 2005-2012 Apple Inc. All 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.python.log import Logger
+from twext.web2 import responsecode
+from twext.web2.http import HTTPError, Response
+from twisted.internet.abstract import isIPAddress
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twistedcaldav.scheduling import addressmapping
+from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
+from twistedcaldav.scheduling.cuaddress import calendarUserFromPrincipal
+from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServers
+from twistedcaldav.scheduling.scheduler import RemoteScheduler, \
+    ScheduleResponseQueue
+import twistedcaldav.scheduling.ischedule.xml as ixml
+from twistedcaldav.scheduling.ischedule.localservers import Servers
+from twistedcaldav.util import normalizationLookup
+from txdav.xml import element as davxml
+import itertools
+import re
+import socket
+import urlparse
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule.dkim import DKIMVerifier, \
+    DKIMVerificationError, DKIMMissingError
+from twext.web2.http_headers import MimeType
+from twistedcaldav.scheduling.ischedule.xml import ischedule_namespace
+from txdav.xml.base import WebDAVUnknownElement
+from twistedcaldav.scheduling.ischedule.utils import getIPsFromHost
+from twistedcaldav.scheduling.ischedule import xml
+from twistedcaldav.ical import normalizeCUAddress
+
+"""
+L{IScheduleScheduler} - handles deliveries for scheduling messages being POSTed to the iSchedule inbox.
+"""
+
+__all__ = [
+    "IScheduleScheduler",
+]
+
+
+log = Logger()
+
+class ErrorResponse(Response):
+    """
+    A L{Response} object which contains a status code and a L{element.Error}
+    element.
+    Renders itself as a DAV:error XML document.
+    """
+    error = None
+    unregistered = True     # base class is already registered
+
+    def __init__(self, code, error, description=None):
+        """
+        @param code: a response code.
+        @param error: an L{WebDAVElement} identifying the error, or a
+            tuple C{(namespace, name)} with which to create an empty element
+            denoting the error.  (The latter is useful in the case of
+            preconditions and postconditions, not all of which have defined
+            XML element classes.)
+        @param description: an optional string that, if present, will get
+            wrapped in a (twisted_dav_namespace, error-description) element.
+        """
+        if type(error) is tuple:
+            xml_namespace, xml_name = error
+            error = WebDAVUnknownElement()
+            error.namespace = xml_namespace
+            error.name = xml_name
+
+        self.description = description
+        if self.description:
+            output = ixml.Error(error, ixml.ResponseDescription(self.description)).toxml()
+        else:
+            output = ixml.Error(error).toxml()
+
+        Response.__init__(self, code=code, stream=output)
+
+        self.headers.setHeader("content-type", MimeType("text", "xml"))
+
+        self.error = error
+
+
+    def __repr__(self):
+        return "<%s %s %s>" % (self.__class__.__name__, self.code, self.error.sname())
+
+
+
+class IScheduleResponseQueue (ScheduleResponseQueue):
+    """
+    Stores a list of (typically error) responses for use in a
+    L{ScheduleResponse}.
+    """
+
+    schedule_response_element = xml.ScheduleResponse
+    response_element = xml.Response
+    recipient_element = xml.Recipient
+    recipient_uses_href = False
+    request_status_element = xml.RequestStatus
+    error_element = xml.Error
+    response_description_element = xml.ResponseDescription
+    calendar_data_element = xml.CalendarData
+
+
+
+class IScheduleScheduler(RemoteScheduler):
+
+    scheduleResponse = IScheduleResponseQueue
+
+    errorResponse = ErrorResponse
+
+    errorElements = {
+        "originator-missing": (ischedule_namespace, "originator-missing"),
+        "originator-invalid": (ischedule_namespace, "originator-invalid"),
+        "originator-denied": (ischedule_namespace, "originator-denied"),
+        "recipient-missing": (ischedule_namespace, "recipient-missing"),
+        "recipient-invalid": (ischedule_namespace, "recipient-invalid"),
+        "organizer-denied": (ischedule_namespace, "organizer-denied"),
+        "attendee-denied": (ischedule_namespace, "attendee-denied"),
+        "invalid-calendar-data-type": (ischedule_namespace, "invalid-calendar-data-type"),
+        "invalid-calendar-data": (ischedule_namespace, "invalid-calendar-data"),
+        "invalid-scheduling-message": (ischedule_namespace, "invalid-scheduling-message"),
+        "max-recipients": (ischedule_namespace, "max-recipients"),
+    }
+
+    @inlineCallbacks
+    def doSchedulingViaPOST(self, transaction, use_request_headers=False):
+        """
+        Carry out iSchedule specific processing.
+        """
+
+        self.verified = False
+        if config.Scheduling.iSchedule.DKIM.Enabled:
+            verifier = DKIMVerifier(self.request, protocol_debug=config.Scheduling.iSchedule.DKIM.ProtocolDebug)
+            try:
+                yield verifier.verify()
+                self.verified = True
+
+            except DKIMMissingError:
+                # Carry on processing, but we will do extra checks on the originator as we would
+                # when DKIM is not enabled, so that any local policy via remoteservers.xml can be used.
+                pass
+
+            except DKIMVerificationError, e:
+                # If DKIM is enabled and there was a DKIM header present, then fail
+                msg = "Failed to verify DKIM signature"
+                _debug_msg = str(e)
+                log.debug("%s:%s" % (msg, _debug_msg,))
+                if config.Scheduling.iSchedule.DKIM.ProtocolDebug:
+                    msg = "%s:%s" % (msg, _debug_msg,)
+                raise HTTPError(self.errorResponse(
+                    responsecode.FORBIDDEN,
+                    (ischedule_namespace, "verification-failed"),
+                    msg,
+                ))
+
+        result = (yield super(IScheduleScheduler, self).doSchedulingViaPOST(transaction, use_request_headers))
+        returnValue(result)
+
+
+    def loadFromRequestHeaders(self):
+        """
+        Load Originator and Recipient from request headers.
+        """
+        super(IScheduleScheduler, self).loadFromRequestHeaders()
+
+        if self.request.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
+            self.request.doing_attendee_refresh = 1
+
+
+    def preProcessCalendarData(self):
+        """
+        For data coming in from outside we need to normalize the calendar user addresses so that later iTIP
+        processing will match calendar users against those in stored calendar data. Only do that for invites
+        not freebusy.
+        """
+
+        if not self.checkForFreeBusy():
+            # Need to normalize the calendar data and recipient values to keep those in sync,
+            # as we might later try to match them
+            self.calendar.normalizeCalendarUserAddresses(normalizationLookup, self.resource.principalForCalendarUserAddress)
+
+
+    def loadRecipientsFromRequestHeaders(self):
+        """
+        Need to normalize the calendar data and recipient values to keep those in sync,
+        as we might later try to match them
+        """
+        super(IScheduleScheduler, self).loadRecipientsFromRequestHeaders()
+        self.recipients = [normalizeCUAddress(recipient, normalizationLookup, self.resource.principalForCalendarUserAddress) for recipient in self.recipients]
+
+
+    def checkAuthorization(self):
+        # Must have an unauthenticated user
+        if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
+            log.err("Authenticated originators not allowed: %s" % (self.originator,))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["originator-denied"],
+                "Authentication not allowed",
+            ))
+
+
+    @inlineCallbacks
+    def checkOriginator(self):
+        """
+        Check the validity of the Originator header.
+        """
+
+        # For remote requests we do not allow the originator to be a local user or one within our domain.
+        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
+        if originatorPrincipal or localUser:
+            if originatorPrincipal.locallyHosted():
+                log.err("Cannot use originator that is on this server: %s" % (self.originator,))
+                raise HTTPError(self.errorResponse(
+                    responsecode.FORBIDDEN,
+                    self.errorElements["originator-denied"],
+                    "Originator cannot be local to server",
+                ))
+            else:
+                self.originator = calendarUserFromPrincipal(self.originator, originatorPrincipal)
+                self._validAlternateServer(originatorPrincipal)
+        else:
+            self.originator = RemoteCalendarUser(self.originator)
+            self._validiScheduleServer()
+
+
+    def _validiScheduleServer(self):
+        """
+        Check the validity of the iSchedule host.
+        """
+
+        # Check for DKIM verification first and treat as valid
+        if self.verified:
+            return
+
+        # We will only accept originator in known domains.
+        servermgr = IScheduleServers()
+        server = servermgr.mapDomain(self.originator.domain)
+        if not server or not server.allow_from:
+            log.err("Originator not on recognized server: %s" % (self.originator,))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["originator-denied"],
+                "Originator not recognized by server",
+            ))
+        else:
+            # Get the request IP and map to hostname.
+            clientip = self.request.remoteAddr.host
+
+            # First compare as dotted IP
+            matched = False
+            compare_with = (server.host,) + tuple(server.client_hosts)
+            if clientip in compare_with:
+                matched = True
+            else:
+                # Now do hostname lookup
+                try:
+                    host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
+                    for host in itertools.chain((host,), aliases):
+                        # Try simple match first
+                        if host in compare_with:
+                            matched = True
+                            break
+
+                        # Try pattern match next
+                        for pattern in compare_with:
+                            try:
+                                if re.match(pattern, host) is not None:
+                                    matched = True
+                                    break
+                            except re.error:
+                                log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
+                        else:
+                            continue
+                        break
+                except socket.herror, e:
+                    log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
+
+            if not matched:
+                log.err("Originator not on allowed server: %s" % (self.originator,))
+                raise HTTPError(self.errorResponse(
+                    responsecode.FORBIDDEN,
+                    self.errorElements["originator-denied"],
+                    "Originator not allowed to send to this server",
+                ))
+
+
+    def _validAlternateServer(self, principal):
+        """
+        Check the validity of the partitioned host.
+        """
+
+        # Extract expected host/port. This will be the partitionURI, or if no partitions,
+        # the serverURI
+        expected_uri = principal.partitionURI()
+        if expected_uri is None:
+            expected_uri = principal.serverURI()
+        expected_uri = urlparse.urlparse(expected_uri)
+
+        # Get the request IP and map to hostname.
+        clientip = self.request.remoteAddr.host
+
+        # Check against this server (or any of its partitions). We need this because an external iTIP message
+        # may be addressed to users on different partitions, and the node receiving the iTIP message will need to
+        # forward it to the partition nodes, thus the client ip seen by the partitions will in fact be the initial
+        # receiving node.
+        matched = False
+        if Servers.getThisServer().checkThisIP(clientip):
+            matched = True
+
+        # Checked allowed IPs - if any were defined we only check against them, we do not
+        # go on to check the expected server host ip
+        elif Servers.getThisServer().hasAllowedFromIP():
+            matched = Servers.getThisServer().checkAllowedFromIP(clientip)
+            if not matched:
+                log.error("Invalid iSchedule connection from client: %s" % (clientip,))
+
+        # Next compare as dotted IP
+        elif isIPAddress(expected_uri.hostname):
+            if clientip == expected_uri.hostname:
+                matched = True
+        else:
+            # Now do expected hostname -> IP lookup
+            try:
+                # So now try the lookup of the expected host
+                for ip in getIPsFromHost(expected_uri.hostname):
+                    if ip == clientip:
+                        matched = True
+                        break
+            except socket.herror, e:
+                log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
+
+        # Check possible shared secret
+        if matched and not Servers.getThisServer().checkSharedSecret(self.request):
+            log.err("Invalid iSchedule shared secret")
+            matched = False
+
+        if not matched:
+            log.err("Originator not on allowed server: %s" % (self.originator,))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["originator-denied"],
+                "Originator not allowed to send to this server",
+            ))
+
+
+    @inlineCallbacks
+    def checkOrganizerAsOriginator(self):
+        """
+        Check the validity of the ORGANIZER value. ORGANIZER must not be local.
+        """
+
+        # Verify that the ORGANIZER's cu address does not map to a valid user
+        organizer = self.calendar.getOrganizer()
+        if organizer:
+            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+            if organizerPrincipal:
+                if organizerPrincipal.locallyHosted():
+                    log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
+                    raise HTTPError(self.errorResponse(
+                        responsecode.FORBIDDEN,
+                        self.errorElements["organizer-denied"],
+                        "Organizer is not local to server",
+                    ))
+                else:
+                    # Check that the origin server is the correct partition
+                    self.organizer = calendarUserFromPrincipal(organizer, organizerPrincipal)
+                    self._validAlternateServer(self.organizer.principal)
+            else:
+                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
+                if localUser:
+                    log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendar,))
+                    raise HTTPError(self.errorResponse(
+                        responsecode.FORBIDDEN,
+                        self.errorElements["organizer-denied"],
+                        "Organizer not allowed to be originator",
+                    ))
+                else:
+                    self.organizer = RemoteCalendarUser(organizer)
+        else:
+            log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["organizer-denied"],
+                "No organizer in calendar data",
+            ))
+
+
+    @inlineCallbacks
+    def checkAttendeeAsOriginator(self):
+        """
+        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+        Only local attendees are allowed for message originating from this server.
+        """
+
+        # Attendee cannot be local.
+        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
+        if attendeePrincipal:
+            if attendeePrincipal.locallyHosted():
+                log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))
+                raise HTTPError(self.errorResponse(
+                    responsecode.FORBIDDEN,
+                    self.errorElements["attendee-denied"],
+                    "Local attendee cannot send to this server",
+                ))
+            else:
+                self._validAlternateServer(attendeePrincipal)
+        else:
+            localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
+            if localUser:
+                log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
+                raise HTTPError(self.errorResponse(
+                    responsecode.FORBIDDEN,
+                    self.errorElements["attendee-denied"],
+                    "Attendee not allowed to schedule",
+                ))
+
+        # TODO: in this case we should check that the ORGANIZER is the sole recipient.
+
+
+    @inlineCallbacks
+    def securityChecks(self):
+        """
+        Check that the originator has the appropriate rights to send this type of iTIP message.
+        """
+
+        # Prevent spoofing of ORGANIZER with specific METHODs when local
+        if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
+            yield self.checkOrganizerAsOriginator()
+
+        # Prevent spoofing when doing reply-like METHODs
+        elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+            yield self.checkAttendeeAsOriginator()
+
+        else:
+            log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
+            raise HTTPError(self.errorResponse(
+                responsecode.FORBIDDEN,
+                self.errorElements["invalid-scheduling-message"],
+                "Unknown iTIP method",
+            ))

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.example.com
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.example.com	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,19 +0,0 @@
-example.com.		  10800 IN SOA	ns.example.com. 	admin.example.com. (
-                                                        2012090810 ; serial
-                                                        3600       ; refresh (1 hour)
-                                                        900        ; retry (15 minutes)
-                                                        1209600    ; expire (2 weeks)
-                                                        86400      ; minimum (1 day)
-							)
-									10800 IN NS		ns.example.com.
-									10800 IN A		127.0.0.1
-ns.example.com.						10800 IN A		127.0.0.1
-
-_caldavs._tcp.example.com.			10800 IN SRV	0	0	8443	example.com.
-_ischedules._tcp.example.com.		10800 IN SRV	0	0	8443	example.com.
-
-_ischedule._domainkey.example.com.	10800 IN TXT	"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjUfDqd8ICAL0dyq2KdjKN6LS8O/Y4yMxOxgATqtSIMi7baKXEs1w5Wj9efOC2nU+aqyhP2/J6AzfFJfSB+GV5gcIT+LAC4btJKPGjPUyXcQFJV4a73y0jIgCTBzWxdaP6qD9P9rzYlvMPcdrrKiKoAOtI3JZqAAdZudOmGlc4QQIDAQAB"
-_revoked._domainkey.example.com.	10800 IN TXT	"v=DKIM1; p="
-
-_domainkey._tcp.example.com.		10800 IN SRV	0	0	8443	key.example.com.
-_domainkey._tcp.www.example.com.	10800 IN SRV	0	0	80		key.example.com.

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.example.com (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.example.com	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.example.com	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,19 @@
+example.com.		  10800 IN SOA	ns.example.com. 	admin.example.com. (
+                                                        2012090810 ; serial
+                                                        3600       ; refresh (1 hour)
+                                                        900        ; retry (15 minutes)
+                                                        1209600    ; expire (2 weeks)
+                                                        86400      ; minimum (1 day)
+							)
+									10800 IN NS		ns.example.com.
+									10800 IN A		127.0.0.1
+ns.example.com.						10800 IN A		127.0.0.1
+
+_caldavs._tcp.example.com.			10800 IN SRV	0	0	8443	example.com.
+_ischedules._tcp.example.com.		10800 IN SRV	0	0	8443	example.com.
+
+_ischedule._domainkey.example.com.	10800 IN TXT	"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjUfDqd8ICAL0dyq2KdjKN6LS8O/Y4yMxOxgATqtSIMi7baKXEs1w5Wj9efOC2nU+aqyhP2/J6AzfFJfSB+GV5gcIT+LAC4btJKPGjPUyXcQFJV4a73y0jIgCTBzWxdaP6qD9P9rzYlvMPcdrrKiKoAOtI3JZqAAdZudOmGlc4QQIDAQAB"
+_revoked._domainkey.example.com.	10800 IN TXT	"v=DKIM1; p="
+
+_domainkey._tcp.example.com.		10800 IN SRV	0	0	8443	key.example.com.
+_domainkey._tcp.www.example.com.	10800 IN SRV	0	0	80		key.example.com.

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.two.zones
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.two.zones	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.two.zones	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,31 +0,0 @@
-example.com.		  10800 IN SOA	ns.example.com. 	admin.example.com. (
-                                                        2012090810 ; serial
-                                                        3600       ; refresh (1 hour)
-                                                        900        ; retry (15 minutes)
-                                                        1209600    ; expire (2 weeks)
-                                                        86400      ; minimum (1 day)
-							)
-									10800 IN NS		ns.example.com.
-									10800 IN A		127.0.0.1
-ns.example.com.						10800 IN A		127.0.0.1
-
-_caldavs._tcp.example.com.			10800 IN SRV	0	0	8443	example.com.
-_ischedules._tcp.example.com.		10800 IN SRV	0	0	8443	example.com.
-_ischedule._domainkey.example.com.	10800 IN TXT	"v=DKIM1; p="
-
-
-
-example.org.		  10800 IN SOA	ns.example.org. 	admin.example.org. (
-                                                        2012090810 ; serial
-                                                        3600       ; refresh (1 hour)
-                                                        900        ; retry (15 minutes)
-                                                        1209600    ; expire (2 weeks)
-                                                        86400      ; minimum (1 day)
-							)
-									10800 IN NS		ns.example.org.
-									10800 IN A		127.0.0.1
-ns.example.org.						10800 IN A		127.0.0.1
-
-_caldavs._tcp.example.org.			10800 IN SRV	0	0	8543	example.org.
-_ischedules._tcp.example.org.		10800 IN SRV	0	0	8543	example.org.
-_ischedule2._domainkey.example.org.	10800 IN TXT	"v=DKIM1; s=ischedule; p="

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.two.zones (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.two.zones)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.two.zones	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/data/db.two.zones	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,31 @@
+example.com.		  10800 IN SOA	ns.example.com. 	admin.example.com. (
+                                                        2012090810 ; serial
+                                                        3600       ; refresh (1 hour)
+                                                        900        ; retry (15 minutes)
+                                                        1209600    ; expire (2 weeks)
+                                                        86400      ; minimum (1 day)
+							)
+									10800 IN NS		ns.example.com.
+									10800 IN A		127.0.0.1
+ns.example.com.						10800 IN A		127.0.0.1
+
+_caldavs._tcp.example.com.			10800 IN SRV	0	0	8443	example.com.
+_ischedules._tcp.example.com.		10800 IN SRV	0	0	8443	example.com.
+_ischedule._domainkey.example.com.	10800 IN TXT	"v=DKIM1; p="
+
+
+
+example.org.		  10800 IN SOA	ns.example.org. 	admin.example.org. (
+                                                        2012090810 ; serial
+                                                        3600       ; refresh (1 hour)
+                                                        900        ; retry (15 minutes)
+                                                        1209600    ; expire (2 weeks)
+                                                        86400      ; minimum (1 day)
+							)
+									10800 IN NS		ns.example.org.
+									10800 IN A		127.0.0.1
+ns.example.org.						10800 IN A		127.0.0.1
+
+_caldavs._tcp.example.org.			10800 IN SRV	0	0	8543	example.org.
+_ischedules._tcp.example.org.		10800 IN SRV	0	0	8543	example.org.
+_ischedule2._domainkey.example.org.	10800 IN TXT	"v=DKIM1; s=ischedule; p="

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_delivery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,62 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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 twistedcaldav.test.util
-from twisted.internet.defer import inlineCallbacks
-from twisted.python.modules import getModule
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule import utils
-from twisted.names import client
-from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
-
-class CalDAV (twistedcaldav.test.util.TestCase):
-    """
-    twistedcaldav.scheduling.caldav tests
-    """
-
-    def tearDown(self):
-        """
-        By setting the resolver to None, it will be recreated next time a name
-        lookup is done.
-        """
-        client.theResolver = None
-        utils.DebugResolver = None
-
-
-    @inlineCallbacks
-    def test_matchCalendarUserAddress(self):
-        """
-        Make sure we do an exact comparison on EmailDomain
-        """
-
-        self.patch(config.Scheduling.iSchedule, "RemoteServers", "")
-
-        # Only mailtos:
-        result = yield ScheduleViaISchedule.matchCalendarUserAddress("http://example.com/principal/user")
-        self.assertFalse(result)
-
-        # Need to setup a fake resolver
-        module = getModule(__name__)
-        dataPath = module.filePath.sibling("data")
-        bindPath = dataPath.child("db.example.com")
-        self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
-        utils.DebugResolver = None
-        utils._initResolver()
-
-        result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.com")
-        self.assertTrue(result)
-        result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.org")
-        self.assertFalse(result)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_delivery.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_delivery.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_delivery.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,62 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All 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 twistedcaldav.test.util
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.modules import getModule
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule import utils
+from twisted.names import client
+from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
+
+class CalDAV (twistedcaldav.test.util.TestCase):
+    """
+    twistedcaldav.scheduling.caldav tests
+    """
+
+    def tearDown(self):
+        """
+        By setting the resolver to None, it will be recreated next time a name
+        lookup is done.
+        """
+        client.theResolver = None
+        utils.DebugResolver = None
+
+
+    @inlineCallbacks
+    def test_matchCalendarUserAddress(self):
+        """
+        Make sure we do an exact comparison on EmailDomain
+        """
+
+        self.patch(config.Scheduling.iSchedule, "RemoteServers", "")
+
+        # Only mailtos:
+        result = yield ScheduleViaISchedule.matchCalendarUserAddress("http://example.com/principal/user")
+        self.assertFalse(result)
+
+        # Need to setup a fake resolver
+        module = getModule(__name__)
+        dataPath = module.filePath.sibling("data")
+        bindPath = dataPath.child("db.example.com")
+        self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+        utils.DebugResolver = None
+        utils._initResolver()
+
+        result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.com")
+        self.assertTrue(result)
+        result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.org")
+        self.assertFalse(result)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_dkim.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_dkim.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_dkim.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,862 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All 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 Crypto.PublicKey import RSA
-
-from twext.web2.http_headers import Headers, MimeType
-from twext.web2.stream import MemoryStream
-
-from twisted.internet.defer import inlineCallbacks, succeed
-from twisted.names import client
-from twisted.python.modules import getModule
-
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule import utils
-from twistedcaldav.scheduling.ischedule.dkim import DKIMRequest, DKIMVerifier, \
-    DKIMVerificationError, DKIMUtils, PublicKeyLookup_DNSTXT, \
-    PublicKeyLookup_HTTP_WellKnown, PublicKeyLookup_PrivateExchange
-
-import base64
-import hashlib
-import os
-import time
-import twistedcaldav.test.util
-
-class TestDKIMBase (twistedcaldav.test.util.TestCase):
-    """
-    DKIM support tests
-    """
-
-    class PublicKeyLookup_Testing(PublicKeyLookup_HTTP_WellKnown):
-
-        keys = []
-
-        def _lookupKeys(self):
-            """
-            Do the key lookup using the actual lookup method.
-            """
-            return succeed(self.keys)
-
-
-    def setUp(self):
-        super(TestDKIMBase, self).setUp()
-
-        self.private_keyfile = self.mktemp()
-        f = open(self.private_keyfile, "w")
-        f.write("""-----BEGIN RSA PRIVATE KEY-----
-MIIEogIBAAKCAQEAw7bJxD1k5VSA5AqdfmJ7vj99oKQ4qYtSeJ5HiK6W40dzC++k
-LweUWLzeUErgXwcJlyOC6rqVVPBfSJq4l7yPdVqpWUo6s2jnUsSWOfhpre22yc4B
-K0QY2Euc3R+gT59eM0mtJPtWaQw5BmQ2GrV6f0OUiKi17jEPasKcxf1qZrWU0+Ik
-D2DhUCuRrNb/baUkuIkxoit6M7k7s5X9swT1hE/Eso0gS79FSti1fkDeoPZ296Gu
-5uYWdpaLl03Nr0w65Gbw+2v79AcwOyvbZD6y9xYGLWubic0dUeWuhUipZdmQf8Bd
-t7cZVgjQX/giQQqqLDFhfNFwapUZDhS7TCtujQIDAQABAoIBADfFuzHFHR+NOT3D
-GKaPghvxE+fXZJ5MKbBdypzUxAL4tXxNSkhsrIWtLN1MuSvbYYxEfmZNzYhrB3w1
-Oy1ieq9CqsfbM2c1GdaoVvcmJ1d9Sn2vyv19ZmcdBRKulIycKcgL0t+bEEDXTtjX
-beOmm8XwiD95dH7wVChkVTDGyq+BxtSY6wav9y15zWnBH7+BAeq3OnKaNIQB0iTI
-UA41jWocKYI18/6D5gQTDSoYvKB7saFVGw9IgmmHA/3rYztcHCxUoE15x7wWuwtF
-vzQanEt/QwEEFMibNTjvfIUPoeIeQH7MzcD56AL9u/cs8LNeSbappWE7BneQ0ll3
-CfTsAQECgYEA/eoDkpPMWxuoSrZ1oXLxeImEAB2EHNs4UV9dmcUkhNeYZP0rv7pL
-4jpkNHTRvFeeovy5khXhykb9BUYDuZy6rcELlFxpCKYRw3d+PPWM+wfqmJp+fIN7
-Z4F1Kpznt0F2e+9LXF1Qi5bM2dHy1maxEjaBUIOIoczbjJJDmNN8zR0CgYEAxVJg
-2VCpjaRoJtaZYeserkVgB8SFffBnm/8XQv8uTbKrz104t9nFyezbINmKrQs3cxT3
-1+PiVbLJpPRcik129x4xIlz3zapsMqwXL97Lz92vXm/nELRnV8d+F9SxVzlijRDL
-rvl3X3Vayq2zKb6euBOwOu8UnQO3xJkTtLPtHDECgYAptxuVJkEJqtaQR7+1oZu4
-UOdl2XOOBhoPjFplW/Uu+fiohst8OVAkP7GcyKB4j/CZGGoobP3mbJk/F4yfHvew
-eim72x7Kc/YxJd2QiEr8JwXMwn0LWdKZY7RrJtIO0mtz2xGHgDEubb0EADEkNkTb
-GCdQoft9kZl0U8dVQVGcpQKBgHsvjIre0ps8slDc1HDO6h597Q+sXnJbLTO0Mv9+
-c5fKHXydhBUy/UmsdrixVuPlBr7vrjK3b8t0jHJQo50r80MfNClxxLo+1MFlsiwO
-eUrR6POaBLTnC0U/o7aY8AW2K5JJk/8uepm7l+zEN/+to0Tj9bc1HrdPZOB1eFnt
-oe9hAoGAEwwDhNrmSlZjmZMT8WehCdyS7zQgI8OCLAlu9KTiwFzoSDcnhVbS4nd4
-iblllHCLZ2Q/rHSH3cQor94kxePm+b3KH9ZwAgInMModuSPcScrR5/vsORZCtJEO
-CAXnxZHhrExMGIIa7KV33W5v7Hstl7SnPWKFgCvlBH2QoMTjoUE=
------END RSA PRIVATE KEY-----
-""")
-        f.close()
-
-        pkey_data = """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw7bJxD1k5VSA5AqdfmJ7
-vj99oKQ4qYtSeJ5HiK6W40dzC++kLweUWLzeUErgXwcJlyOC6rqVVPBfSJq4l7yP
-dVqpWUo6s2jnUsSWOfhpre22yc4BK0QY2Euc3R+gT59eM0mtJPtWaQw5BmQ2GrV6
-f0OUiKi17jEPasKcxf1qZrWU0+IkD2DhUCuRrNb/baUkuIkxoit6M7k7s5X9swT1
-hE/Eso0gS79FSti1fkDeoPZ296Gu5uYWdpaLl03Nr0w65Gbw+2v79AcwOyvbZD6y
-9xYGLWubic0dUeWuhUipZdmQf8Bdt7cZVgjQX/giQQqqLDFhfNFwapUZDhS7TCtu
-jQIDAQAB
-"""
-        self.public_keyfile = self.mktemp()
-        f = open(self.public_keyfile, "w")
-        f.write("""-----BEGIN PUBLIC KEY-----
-%s-----END PUBLIC KEY-----
-""" % (pkey_data,))
-        f.close()
-        self.public_key_data = pkey_data.replace("\n", "")
-
-
-
-class TestDKIMRequest (TestDKIMBase):
-    """
-    L{DKIMRequest} support tests.
-    """
-
-    @inlineCallbacks
-    def test_body_hash(self):
-
-        data = "Hello World!"
-        for algorithm, hash_method in (
-            ("rsa-sha1", hashlib.sha1,),
-            ("rsa-sha256", hashlib.sha256,),
-        ):
-            stream = str(data)
-            headers = Headers()
-            headers.addRawHeader("Originator", "mailto:user01 at example.com")
-            headers.addRawHeader("Recipient", "mailto:user02 at example.com")
-            headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
-            request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", "/tmp/key", algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
-            hash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
-            result = (yield request.bodyHash())
-            self.assertEqual(result, hash)
-
-
-    def test_generateSignature(self):
-
-        data = "Hello World!"
-
-        for algorithm, hash_method in (
-            ("rsa-sha1", hashlib.sha1,),
-            ("rsa-sha256", hashlib.sha256,),
-        ):
-            stream = MemoryStream(data)
-            headers = Headers()
-            headers.addRawHeader("Originator", "mailto:user01 at example.com")
-            headers.addRawHeader("Recipient", "mailto:user02 at example.com")
-            headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
-            request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
-
-            # Manually create what should be the correct thing to sign
-            bodyhash = base64.b64encode(hash_method(data).digest())
-            sign_this = """originator:mailto:user01 at example.com
-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)
-
-            result = request.generateSignature(sign_this)
-
-            key = RSA.importKey(open(self.private_keyfile).read())
-            signature = DKIMUtils.sign(sign_this, key, DKIMUtils.hash_func(algorithm))
-
-            self.assertEqual(result, signature)
-
-
-    @inlineCallbacks
-    def test_signatureHeaders(self):
-
-        data = "Hello World!"
-
-        for algorithm, hash_method in (
-            ("rsa-sha1", hashlib.sha1,),
-            ("rsa-sha256", hashlib.sha256,),
-        ):
-            stream = MemoryStream(data)
-            headers = Headers()
-            headers.addRawHeader("Originator", "mailto:user01 at example.com")
-            headers.addRawHeader("Recipient", "mailto:user02 at example.com")
-            headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
-            request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
-            result, _ignore_tags = (yield request.signatureHeaders())
-
-            # Manually create what should be the correct thing to sign
-            bodyhash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
-            sign_this = """originator:mailto:user01 at example.com
-recipient:mailto:user02 at example.com
-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)
-
-            self.assertEqual(result, sign_this)
-
-
-    @inlineCallbacks
-    def test_sign(self):
-
-        data = "Hello World!"
-        for algorithm, hash_method in (
-            ("rsa-sha1", hashlib.sha1,),
-            ("rsa-sha256", hashlib.sha256,),
-        ):
-            stream = MemoryStream(data)
-            headers = Headers()
-            headers.addRawHeader("Originator", "mailto:user01 at example.com")
-            headers.addRawHeader("Recipient", "mailto:user02 at example.com")
-            headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
-            request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
-            result = (yield request.sign())
-
-            # Manually create what should be the correct thing to sign and make sure signatures match
-            bodyhash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
-            sign_this = """originator:mailto:user01 at example.com
-recipient:mailto:user02 at example.com
-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)
-            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,)
-            self.assertEqual(request.headers.getRawHeaders("DKIM-Signature")[0], updated_header)
-
-            # Try to verify result using public key
-            pubkey = RSA.importKey(open(self.public_keyfile).read())
-            self.assertEqual(DKIMUtils.verify(sign_this, result, pubkey, DKIMUtils.hash_func(algorithm)), None)
-
-
-
-class TestDKIMVerifier (TestDKIMBase):
-    """
-    L{DKIMVerifier} support tests.
-    """
-
-    class StubRequest(object):
-
-        def __init__(self, method, uri, headers, body):
-            self.method = method
-            self.uri = uri
-            self.headers = Headers()
-            for name, value in headers:
-                self.headers.addRawHeader(name, value)
-            self.stream = MemoryStream(body)
-
-
-    def test_valid_dkim_headers(self):
-        """
-        L{DKIMVerifier.processDKIMHeader} correctly validates DKIM-Signature headers.
-        """
-
-        data = (
-            # Bogus
-            ((("DKIM-Signature", "v=1"),), False,),
-
-            # 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"),
-            ), 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,),
-
-            # 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,),
-        )
-
-        for headers, result in data:
-            request = self.StubRequest("POST", "/", headers, "")
-            verifier = DKIMVerifier(request)
-            if result:
-                verifier.processDKIMHeader()
-            else:
-                self.assertRaises(DKIMVerificationError, verifier.processDKIMHeader)
-
-
-    def test_canonicalize_header(self):
-        """
-        L{DKIMVerifier.canonicalizeHeader} correctly canonicalizes headers.
-        """
-
-        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"),
-            ("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=",
-            ),
-            (
-                "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",
-            ),
-        )
-
-        for name, value, result in data:
-            request = self.StubRequest("POST", "/", ((name, value,),), "")
-            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)
-            self.assertEqual(canonicalized, result)
-
-
-    def test_extract_headers(self):
-        """
-        L{DKIMVerifier.extractSignedHeaders} correctly extracts canonicalizes headers.
-        """
-
-        data = (
-            # Over 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
-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
-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="""
-            ),
-            # Exact 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
-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
-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
-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="""
-            ),
-            # 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
-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
-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="""
-            ),
-        )
-
-        for hdrs, result in data:
-            headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
-            request = self.StubRequest("POST", "/", headers, "")
-            verifier = DKIMVerifier(request)
-            verifier.processDKIMHeader()
-            extracted = verifier.extractSignedHeaders()
-            self.assertEqual(extracted, result.replace("\n", "\r\n"))
-
-
-    def test_locate_public_key(self):
-        """
-        L{DKIMVerifier.locatePublicKey} correctly finds key matching headers.
-        """
-
-        data = (
-            # Valid
-            ("""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
-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
-""",
-            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            True,
-            ),
-            # Invalid - no method
-            ("""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
-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
-Cache-Control:no-cache
-Connection:close
-""",
-            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            False,
-            ),
-            # Invalid - wrong algorithm
-            ("""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
-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
-""",
-            [DKIMUtils.extractTags("v=DKIM1; h=sha-1; p=%s" % (self.public_key_data,))],
-            False,
-            ),
-        )
-
-        for hdrs, keys, result in data:
-            headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
-            request = self.StubRequest("POST", "/", headers, "")
-            TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
-            verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
-            verifier.processDKIMHeader()
-            pkey = (yield verifier.locatePublicKey())
-            if result:
-                self.assertNotEqual(pkey, None)
-            else:
-                self.assertEqual(pkey, None)
-
-
-    @inlineCallbacks
-    def test_verify(self):
-        """
-        L{DKIMVerifier.verify} correctly finds key matching headers.
-        """
-
-        @inlineCallbacks
-        def _verify(hdrs, body, keys, result, sign_headers=("Originator", "Recipient", "Content-Type",), manipulate_request=None):
-            for algorithm in ("rsa-sha1", "rsa-sha256",):
-                # Create signature
-                stream = MemoryStream(body)
-                headers = Headers()
-                for name, value in [hdr.split(":", 1) for hdr in hdrs.splitlines()]:
-                    headers.addRawHeader(name, value)
-                request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, sign_headers, True, True, True, 3600)
-                yield request.sign()
-
-                # Possibly munge the request after the signature is done
-                if manipulate_request is not None:
-                    manipulate_request(request)
-
-                # Verify signature
-                TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
-                verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
-                TestPublicKeyLookup.PublicKeyLookup_Testing({}).flushCache()
-                try:
-                    yield verifier.verify()
-                except Exception, e:
-                    if result:
-                        self.fail("DKIMVerifier:verify failed: %s" % (e,))
-                else:
-                    if not result:
-                        self.fail("DKIMVerifier:verify did not fail")
-
-        # Valid
-        yield _verify(
-            """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
-Cache-Control:no-cache
-Connection:close
-""",
-            """BEGIN:DATA
-END:DATA
-""",
-            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            True,
-        )
-
-        # Invalid - key revoked
-        yield _verify(
-            """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
-Cache-Control:no-cache
-Connection:close
-""",
-            """BEGIN:DATA
-END:DATA
-""",
-            [DKIMUtils.extractTags("v=DKIM1; p=")],
-            False,
-        )
-
-        # Invalid - missing header
-        yield _verify(
-            """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
-Cache-Control:no-cache
-Connection:close
-""",
-            """BEGIN:DATA
-END:DATA
-""",
-            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            False,
-            manipulate_request=lambda request: request.headers.removeHeader("Originator")
-        )
-
-        # Invalid - changed header
-        yield _verify(
-            """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
-Cache-Control:no-cache
-Connection:close
-""",
-            """BEGIN:DATA
-END:DATA
-""",
-            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            False,
-            manipulate_request=lambda request: request.headers.setRawHeaders("Originator", ("mailto:user04 at example.com",))
-        )
-
-        # Invalid - changed body
-        yield _verify(
-            """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
-Cache-Control:no-cache
-Connection:close
-""",
-            """BEGIN:DATA
-END:DATA
-""",
-            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            False,
-            manipulate_request=lambda request: setattr(request, "stream", MemoryStream("BEGIN:DATA\n")),
-        )
-
-        # Valid - extra header no over sign
-        yield _verify(
-            """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
-Cache-Control:no-cache
-Connection:close
-""",
-            """BEGIN:DATA
-END:DATA
-""",
-            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            True,
-            manipulate_request=lambda request: request.headers.getRawHeaders("Recipient").insert(0, "mailto:user04 at example.com"),
-        )
-
-        # Valid - over sign header
-        yield _verify(
-            """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
-Cache-Control:no-cache
-Connection:close
-""",
-            """BEGIN:DATA
-END:DATA
-""",
-            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            False,
-            sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
-        )
-
-        # Invalid - over sign header extra header
-        yield _verify(
-            """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
-Cache-Control:no-cache
-Connection:close
-""",
-            """BEGIN:DATA
-END:DATA
-""",
-            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
-            False,
-            sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
-            manipulate_request=lambda request: request.headers.addRawHeader("Recipient", ("mailto:user04 at example.com",))
-        )
-
-
-
-class TestPublicKeyLookup (TestDKIMBase):
-    """
-    L{PublicKeyLookup} support tests.
-    """
-
-    def tearDown(self):
-        """
-        By setting the resolver to None, it will be recreated next time a name
-        lookup is done.
-        """
-        client.theResolver = None
-        utils.DebugResolver = None
-
-
-    def test_selector_key(self):
-
-        for lookup, d, result in (
-            (PublicKeyLookup_DNSTXT, "example.com", "dkim._domainkey.example.com"),
-            (PublicKeyLookup_DNSTXT, "calendar.example.com", "dkim._domainkey.calendar.example.com"),
-            (PublicKeyLookup_HTTP_WellKnown, "example.com", "https://example.com/.well-known/domainkey/example.com/dkim"),
-            (PublicKeyLookup_HTTP_WellKnown, "calendar.example.com", "https://example.com/.well-known/domainkey/calendar.example.com/dkim"),
-            (PublicKeyLookup_PrivateExchange, "example.com", "example.com#dkim"),
-            (PublicKeyLookup_PrivateExchange, "calendar.example.com", "calendar.example.com#dkim"),
-        ):
-            dkim = "v=1; d=%s; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d,)
-            tester = lookup(DKIMUtils.extractTags(dkim))
-            self.assertEqual(tester._getSelectorKey(), result)
-
-
-    @inlineCallbacks
-    def test_get_key(self):
-
-        # Valid
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))]
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is not None)
-
-        # Valid with more tags
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k = rsa ; h=  sha1 : sha256  ; s=ischedule ; p=%s" % (self.public_key_data,))]
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is not None)
-
-        # Invalid - key type
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=dsa ; p=%s" % (self.public_key_data,))]
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is None)
-
-        # Invalid - hash
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; h=sha512 ; p=%s" % (self.public_key_data,))]
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is None)
-
-        # Invalid - service
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p=%s" % (self.public_key_data,))]
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is None)
-
-        # Invalid - revoked
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p=")]
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is None)
-
-        # Multiple valid
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = [
-            DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,)),
-            DKIMUtils.extractTags("v=DKIM1; k = rsa ; h=  sha1 : sha256  ; s=ischedule ; p=%s" % (self.public_key_data,)),
-            DKIMUtils.extractTags("v=DKIM1; k = rsa ; h=  sha1 : sha256  ; s=* ; p=%s" % (self.public_key_data,)),
-        ]
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is not None)
-
-        # Multiple - some valid, some invalid
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = [
-            DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
-            DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,)),
-            DKIMUtils.extractTags("v=DKIM1; k = rsa ; h=  sha1 : sha256  ; s=ischedule ; p=%s" % (self.public_key_data,)),
-            DKIMUtils.extractTags("v=DKIM1; k = rsa ; h=  sha1 : sha256  ; s=* ; p=%s" % (self.public_key_data,)),
-        ]
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is not None)
-
-        # Multiple - invalid
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = [
-            DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
-            DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
-        ]
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is None)
-
-
-    @inlineCallbacks
-    def test_cached_key(self):
-
-        # Create cache entry
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))]
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is not None)
-
-        # Cache valid
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.keys = []
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is not None)
-
-        # Cache invalid
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        lookup.keys = []
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is None)
-
-
-    @inlineCallbacks
-    def test_TXT_key(self):
-
-        # Need to setup a fake resolver
-        module = getModule(__name__)
-        dataPath = module.filePath.sibling("data")
-        bindPath = dataPath.child("db.example.com")
-        self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
-        utils.DebugResolver = None
-        utils._initResolver()
-
-        for d, s, result in (
-            ("example.com", "_ischedule", True),
-            ("example.com", "_revoked", False),
-            ("example.com", "dkim", False),
-            ("calendar.example.com", "_ischedule", False),
-            ("example.org", "_ischedule", False),
-        ):
-            dkim = "v=1; d=%s; s = %s; t = 1234; a=rsa-sha1; q=dns/txt ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d, s,)
-            tester = PublicKeyLookup_DNSTXT(DKIMUtils.extractTags(dkim))
-            pkey = yield tester.getPublicKey(False)
-            self.assertEqual(pkey is not None, result)
-
-
-    @inlineCallbacks
-    def test_HTTP_URI_key(self):
-
-        # Need to setup a fake resolver
-        module = getModule(__name__)
-        dataPath = module.filePath.sibling("data")
-        bindPath = dataPath.child("db.example.com")
-        self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
-        utils.DebugResolver = None
-        utils._initResolver()
-
-        for d, s, result in (
-            ("example.com", "_ischedule", "https://key.example.com:8443/.well-known/domainkey/example.com/_ischedule"),
-            ("www.example.com", "_ischedule", "http://key.example.com/.well-known/domainkey/www.example.com/_ischedule"),
-            ("example.org", "_ischedule", "https://example.org/.well-known/domainkey/example.org/_ischedule"),
-        ):
-            dkim = "v=1; d=%s; s = %s; t = 1234; a=rsa-sha1; q=http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d, s,)
-            tester = PublicKeyLookup_HTTP_WellKnown(DKIMUtils.extractTags(dkim))
-            uri = (yield tester._getURI())
-            self.assertEqual(uri, result)
-
-
-    @inlineCallbacks
-    def test_private_exchange(self):
-
-        keydir = self.mktemp()
-        PublicKeyLookup_PrivateExchange.directory = keydir
-        os.mkdir(keydir)
-        keyfile = os.path.join(keydir, "example.com#dkim")
-        with open(keyfile, "w") as f:
-            f.write("""v=DKIM1; p=%s
-""" % (self.public_key_data,))
-
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is not None)
-
-        dkim = "v=1; d=example.com; s = dkim2; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is None)
-
-        with open(keyfile, "w") as f:
-            f.write("""v=DKIM1; s=email; p=%s
-""" % (self.public_key_data,))
-
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is None)
-
-        with open(keyfile, "w") as f:
-            f.write("""v=DKIM1; s=email; p=%s
-v=DKIM1; s=ischedule; p=%s
-""" % (self.public_key_data, self.public_key_data,))
-
-        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
-        lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
-        lookup.flushCache()
-        pubkey = (yield lookup.getPublicKey())
-        self.assertTrue(pubkey is not None)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_dkim.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_dkim.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_dkim.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_dkim.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,862 @@
+##
+# Copyright (c) 2012 Apple Inc. All 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 Crypto.PublicKey import RSA
+
+from twext.web2.http_headers import Headers, MimeType
+from twext.web2.stream import MemoryStream
+
+from twisted.internet.defer import inlineCallbacks, succeed
+from twisted.names import client
+from twisted.python.modules import getModule
+
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule import utils
+from twistedcaldav.scheduling.ischedule.dkim import DKIMRequest, DKIMVerifier, \
+    DKIMVerificationError, DKIMUtils, PublicKeyLookup_DNSTXT, \
+    PublicKeyLookup_HTTP_WellKnown, PublicKeyLookup_PrivateExchange
+
+import base64
+import hashlib
+import os
+import time
+import twistedcaldav.test.util
+
+class TestDKIMBase (twistedcaldav.test.util.TestCase):
+    """
+    DKIM support tests
+    """
+
+    class PublicKeyLookup_Testing(PublicKeyLookup_HTTP_WellKnown):
+
+        keys = []
+
+        def _lookupKeys(self):
+            """
+            Do the key lookup using the actual lookup method.
+            """
+            return succeed(self.keys)
+
+
+    def setUp(self):
+        super(TestDKIMBase, self).setUp()
+
+        self.private_keyfile = self.mktemp()
+        f = open(self.private_keyfile, "w")
+        f.write("""-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAw7bJxD1k5VSA5AqdfmJ7vj99oKQ4qYtSeJ5HiK6W40dzC++k
+LweUWLzeUErgXwcJlyOC6rqVVPBfSJq4l7yPdVqpWUo6s2jnUsSWOfhpre22yc4B
+K0QY2Euc3R+gT59eM0mtJPtWaQw5BmQ2GrV6f0OUiKi17jEPasKcxf1qZrWU0+Ik
+D2DhUCuRrNb/baUkuIkxoit6M7k7s5X9swT1hE/Eso0gS79FSti1fkDeoPZ296Gu
+5uYWdpaLl03Nr0w65Gbw+2v79AcwOyvbZD6y9xYGLWubic0dUeWuhUipZdmQf8Bd
+t7cZVgjQX/giQQqqLDFhfNFwapUZDhS7TCtujQIDAQABAoIBADfFuzHFHR+NOT3D
+GKaPghvxE+fXZJ5MKbBdypzUxAL4tXxNSkhsrIWtLN1MuSvbYYxEfmZNzYhrB3w1
+Oy1ieq9CqsfbM2c1GdaoVvcmJ1d9Sn2vyv19ZmcdBRKulIycKcgL0t+bEEDXTtjX
+beOmm8XwiD95dH7wVChkVTDGyq+BxtSY6wav9y15zWnBH7+BAeq3OnKaNIQB0iTI
+UA41jWocKYI18/6D5gQTDSoYvKB7saFVGw9IgmmHA/3rYztcHCxUoE15x7wWuwtF
+vzQanEt/QwEEFMibNTjvfIUPoeIeQH7MzcD56AL9u/cs8LNeSbappWE7BneQ0ll3
+CfTsAQECgYEA/eoDkpPMWxuoSrZ1oXLxeImEAB2EHNs4UV9dmcUkhNeYZP0rv7pL
+4jpkNHTRvFeeovy5khXhykb9BUYDuZy6rcELlFxpCKYRw3d+PPWM+wfqmJp+fIN7
+Z4F1Kpznt0F2e+9LXF1Qi5bM2dHy1maxEjaBUIOIoczbjJJDmNN8zR0CgYEAxVJg
+2VCpjaRoJtaZYeserkVgB8SFffBnm/8XQv8uTbKrz104t9nFyezbINmKrQs3cxT3
+1+PiVbLJpPRcik129x4xIlz3zapsMqwXL97Lz92vXm/nELRnV8d+F9SxVzlijRDL
+rvl3X3Vayq2zKb6euBOwOu8UnQO3xJkTtLPtHDECgYAptxuVJkEJqtaQR7+1oZu4
+UOdl2XOOBhoPjFplW/Uu+fiohst8OVAkP7GcyKB4j/CZGGoobP3mbJk/F4yfHvew
+eim72x7Kc/YxJd2QiEr8JwXMwn0LWdKZY7RrJtIO0mtz2xGHgDEubb0EADEkNkTb
+GCdQoft9kZl0U8dVQVGcpQKBgHsvjIre0ps8slDc1HDO6h597Q+sXnJbLTO0Mv9+
+c5fKHXydhBUy/UmsdrixVuPlBr7vrjK3b8t0jHJQo50r80MfNClxxLo+1MFlsiwO
+eUrR6POaBLTnC0U/o7aY8AW2K5JJk/8uepm7l+zEN/+to0Tj9bc1HrdPZOB1eFnt
+oe9hAoGAEwwDhNrmSlZjmZMT8WehCdyS7zQgI8OCLAlu9KTiwFzoSDcnhVbS4nd4
+iblllHCLZ2Q/rHSH3cQor94kxePm+b3KH9ZwAgInMModuSPcScrR5/vsORZCtJEO
+CAXnxZHhrExMGIIa7KV33W5v7Hstl7SnPWKFgCvlBH2QoMTjoUE=
+-----END RSA PRIVATE KEY-----
+""")
+        f.close()
+
+        pkey_data = """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw7bJxD1k5VSA5AqdfmJ7
+vj99oKQ4qYtSeJ5HiK6W40dzC++kLweUWLzeUErgXwcJlyOC6rqVVPBfSJq4l7yP
+dVqpWUo6s2jnUsSWOfhpre22yc4BK0QY2Euc3R+gT59eM0mtJPtWaQw5BmQ2GrV6
+f0OUiKi17jEPasKcxf1qZrWU0+IkD2DhUCuRrNb/baUkuIkxoit6M7k7s5X9swT1
+hE/Eso0gS79FSti1fkDeoPZ296Gu5uYWdpaLl03Nr0w65Gbw+2v79AcwOyvbZD6y
+9xYGLWubic0dUeWuhUipZdmQf8Bdt7cZVgjQX/giQQqqLDFhfNFwapUZDhS7TCtu
+jQIDAQAB
+"""
+        self.public_keyfile = self.mktemp()
+        f = open(self.public_keyfile, "w")
+        f.write("""-----BEGIN PUBLIC KEY-----
+%s-----END PUBLIC KEY-----
+""" % (pkey_data,))
+        f.close()
+        self.public_key_data = pkey_data.replace("\n", "")
+
+
+
+class TestDKIMRequest (TestDKIMBase):
+    """
+    L{DKIMRequest} support tests.
+    """
+
+    @inlineCallbacks
+    def test_body_hash(self):
+
+        data = "Hello World!"
+        for algorithm, hash_method in (
+            ("rsa-sha1", hashlib.sha1,),
+            ("rsa-sha256", hashlib.sha256,),
+        ):
+            stream = str(data)
+            headers = Headers()
+            headers.addRawHeader("Originator", "mailto:user01 at example.com")
+            headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+            headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
+            request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", "/tmp/key", algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
+            hash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
+            result = (yield request.bodyHash())
+            self.assertEqual(result, hash)
+
+
+    def test_generateSignature(self):
+
+        data = "Hello World!"
+
+        for algorithm, hash_method in (
+            ("rsa-sha1", hashlib.sha1,),
+            ("rsa-sha256", hashlib.sha256,),
+        ):
+            stream = MemoryStream(data)
+            headers = Headers()
+            headers.addRawHeader("Originator", "mailto:user01 at example.com")
+            headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+            headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
+            request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
+
+            # Manually create what should be the correct thing to sign
+            bodyhash = base64.b64encode(hash_method(data).digest())
+            sign_this = """originator:mailto:user01 at example.com
+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)
+
+            result = request.generateSignature(sign_this)
+
+            key = RSA.importKey(open(self.private_keyfile).read())
+            signature = DKIMUtils.sign(sign_this, key, DKIMUtils.hash_func(algorithm))
+
+            self.assertEqual(result, signature)
+
+
+    @inlineCallbacks
+    def test_signatureHeaders(self):
+
+        data = "Hello World!"
+
+        for algorithm, hash_method in (
+            ("rsa-sha1", hashlib.sha1,),
+            ("rsa-sha256", hashlib.sha256,),
+        ):
+            stream = MemoryStream(data)
+            headers = Headers()
+            headers.addRawHeader("Originator", "mailto:user01 at example.com")
+            headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+            headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
+            request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
+            result, _ignore_tags = (yield request.signatureHeaders())
+
+            # Manually create what should be the correct thing to sign
+            bodyhash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
+            sign_this = """originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com
+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)
+
+            self.assertEqual(result, sign_this)
+
+
+    @inlineCallbacks
+    def test_sign(self):
+
+        data = "Hello World!"
+        for algorithm, hash_method in (
+            ("rsa-sha1", hashlib.sha1,),
+            ("rsa-sha256", hashlib.sha256,),
+        ):
+            stream = MemoryStream(data)
+            headers = Headers()
+            headers.addRawHeader("Originator", "mailto:user01 at example.com")
+            headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+            headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
+            request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
+            result = (yield request.sign())
+
+            # Manually create what should be the correct thing to sign and make sure signatures match
+            bodyhash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
+            sign_this = """originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com
+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)
+            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,)
+            self.assertEqual(request.headers.getRawHeaders("DKIM-Signature")[0], updated_header)
+
+            # Try to verify result using public key
+            pubkey = RSA.importKey(open(self.public_keyfile).read())
+            self.assertEqual(DKIMUtils.verify(sign_this, result, pubkey, DKIMUtils.hash_func(algorithm)), None)
+
+
+
+class TestDKIMVerifier (TestDKIMBase):
+    """
+    L{DKIMVerifier} support tests.
+    """
+
+    class StubRequest(object):
+
+        def __init__(self, method, uri, headers, body):
+            self.method = method
+            self.uri = uri
+            self.headers = Headers()
+            for name, value in headers:
+                self.headers.addRawHeader(name, value)
+            self.stream = MemoryStream(body)
+
+
+    def test_valid_dkim_headers(self):
+        """
+        L{DKIMVerifier.processDKIMHeader} correctly validates DKIM-Signature headers.
+        """
+
+        data = (
+            # Bogus
+            ((("DKIM-Signature", "v=1"),), False,),
+
+            # 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"),
+            ), 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,),
+
+            # 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,),
+        )
+
+        for headers, result in data:
+            request = self.StubRequest("POST", "/", headers, "")
+            verifier = DKIMVerifier(request)
+            if result:
+                verifier.processDKIMHeader()
+            else:
+                self.assertRaises(DKIMVerificationError, verifier.processDKIMHeader)
+
+
+    def test_canonicalize_header(self):
+        """
+        L{DKIMVerifier.canonicalizeHeader} correctly canonicalizes headers.
+        """
+
+        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"),
+            ("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=",
+            ),
+            (
+                "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",
+            ),
+        )
+
+        for name, value, result in data:
+            request = self.StubRequest("POST", "/", ((name, value,),), "")
+            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)
+            self.assertEqual(canonicalized, result)
+
+
+    def test_extract_headers(self):
+        """
+        L{DKIMVerifier.extractSignedHeaders} correctly extracts canonicalizes headers.
+        """
+
+        data = (
+            # Over 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
+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
+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="""
+            ),
+            # Exact 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
+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
+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
+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="""
+            ),
+            # 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
+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
+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="""
+            ),
+        )
+
+        for hdrs, result in data:
+            headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
+            request = self.StubRequest("POST", "/", headers, "")
+            verifier = DKIMVerifier(request)
+            verifier.processDKIMHeader()
+            extracted = verifier.extractSignedHeaders()
+            self.assertEqual(extracted, result.replace("\n", "\r\n"))
+
+
+    def test_locate_public_key(self):
+        """
+        L{DKIMVerifier.locatePublicKey} correctly finds key matching headers.
+        """
+
+        data = (
+            # Valid
+            ("""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
+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
+""",
+            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+            True,
+            ),
+            # Invalid - no method
+            ("""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
+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
+Cache-Control:no-cache
+Connection:close
+""",
+            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+            False,
+            ),
+            # Invalid - wrong algorithm
+            ("""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
+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
+""",
+            [DKIMUtils.extractTags("v=DKIM1; h=sha-1; p=%s" % (self.public_key_data,))],
+            False,
+            ),
+        )
+
+        for hdrs, keys, result in data:
+            headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
+            request = self.StubRequest("POST", "/", headers, "")
+            TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
+            verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
+            verifier.processDKIMHeader()
+            pkey = (yield verifier.locatePublicKey())
+            if result:
+                self.assertNotEqual(pkey, None)
+            else:
+                self.assertEqual(pkey, None)
+
+
+    @inlineCallbacks
+    def test_verify(self):
+        """
+        L{DKIMVerifier.verify} correctly finds key matching headers.
+        """
+
+        @inlineCallbacks
+        def _verify(hdrs, body, keys, result, sign_headers=("Originator", "Recipient", "Content-Type",), manipulate_request=None):
+            for algorithm in ("rsa-sha1", "rsa-sha256",):
+                # Create signature
+                stream = MemoryStream(body)
+                headers = Headers()
+                for name, value in [hdr.split(":", 1) for hdr in hdrs.splitlines()]:
+                    headers.addRawHeader(name, value)
+                request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, sign_headers, True, True, True, 3600)
+                yield request.sign()
+
+                # Possibly munge the request after the signature is done
+                if manipulate_request is not None:
+                    manipulate_request(request)
+
+                # Verify signature
+                TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
+                verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
+                TestPublicKeyLookup.PublicKeyLookup_Testing({}).flushCache()
+                try:
+                    yield verifier.verify()
+                except Exception, e:
+                    if result:
+                        self.fail("DKIMVerifier:verify failed: %s" % (e,))
+                else:
+                    if not result:
+                        self.fail("DKIMVerifier:verify did not fail")
+
+        # Valid
+        yield _verify(
+            """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
+Cache-Control:no-cache
+Connection:close
+""",
+            """BEGIN:DATA
+END:DATA
+""",
+            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+            True,
+        )
+
+        # Invalid - key revoked
+        yield _verify(
+            """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
+Cache-Control:no-cache
+Connection:close
+""",
+            """BEGIN:DATA
+END:DATA
+""",
+            [DKIMUtils.extractTags("v=DKIM1; p=")],
+            False,
+        )
+
+        # Invalid - missing header
+        yield _verify(
+            """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
+Cache-Control:no-cache
+Connection:close
+""",
+            """BEGIN:DATA
+END:DATA
+""",
+            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+            False,
+            manipulate_request=lambda request: request.headers.removeHeader("Originator")
+        )
+
+        # Invalid - changed header
+        yield _verify(
+            """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
+Cache-Control:no-cache
+Connection:close
+""",
+            """BEGIN:DATA
+END:DATA
+""",
+            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+            False,
+            manipulate_request=lambda request: request.headers.setRawHeaders("Originator", ("mailto:user04 at example.com",))
+        )
+
+        # Invalid - changed body
+        yield _verify(
+            """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
+Cache-Control:no-cache
+Connection:close
+""",
+            """BEGIN:DATA
+END:DATA
+""",
+            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+            False,
+            manipulate_request=lambda request: setattr(request, "stream", MemoryStream("BEGIN:DATA\n")),
+        )
+
+        # Valid - extra header no over sign
+        yield _verify(
+            """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
+Cache-Control:no-cache
+Connection:close
+""",
+            """BEGIN:DATA
+END:DATA
+""",
+            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+            True,
+            manipulate_request=lambda request: request.headers.getRawHeaders("Recipient").insert(0, "mailto:user04 at example.com"),
+        )
+
+        # Valid - over sign header
+        yield _verify(
+            """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
+Cache-Control:no-cache
+Connection:close
+""",
+            """BEGIN:DATA
+END:DATA
+""",
+            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+            False,
+            sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
+        )
+
+        # Invalid - over sign header extra header
+        yield _verify(
+            """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
+Cache-Control:no-cache
+Connection:close
+""",
+            """BEGIN:DATA
+END:DATA
+""",
+            [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+            False,
+            sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
+            manipulate_request=lambda request: request.headers.addRawHeader("Recipient", ("mailto:user04 at example.com",))
+        )
+
+
+
+class TestPublicKeyLookup (TestDKIMBase):
+    """
+    L{PublicKeyLookup} support tests.
+    """
+
+    def tearDown(self):
+        """
+        By setting the resolver to None, it will be recreated next time a name
+        lookup is done.
+        """
+        client.theResolver = None
+        utils.DebugResolver = None
+
+
+    def test_selector_key(self):
+
+        for lookup, d, result in (
+            (PublicKeyLookup_DNSTXT, "example.com", "dkim._domainkey.example.com"),
+            (PublicKeyLookup_DNSTXT, "calendar.example.com", "dkim._domainkey.calendar.example.com"),
+            (PublicKeyLookup_HTTP_WellKnown, "example.com", "https://example.com/.well-known/domainkey/example.com/dkim"),
+            (PublicKeyLookup_HTTP_WellKnown, "calendar.example.com", "https://example.com/.well-known/domainkey/calendar.example.com/dkim"),
+            (PublicKeyLookup_PrivateExchange, "example.com", "example.com#dkim"),
+            (PublicKeyLookup_PrivateExchange, "calendar.example.com", "calendar.example.com#dkim"),
+        ):
+            dkim = "v=1; d=%s; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d,)
+            tester = lookup(DKIMUtils.extractTags(dkim))
+            self.assertEqual(tester._getSelectorKey(), result)
+
+
+    @inlineCallbacks
+    def test_get_key(self):
+
+        # Valid
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))]
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is not None)
+
+        # Valid with more tags
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k = rsa ; h=  sha1 : sha256  ; s=ischedule ; p=%s" % (self.public_key_data,))]
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is not None)
+
+        # Invalid - key type
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=dsa ; p=%s" % (self.public_key_data,))]
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is None)
+
+        # Invalid - hash
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; h=sha512 ; p=%s" % (self.public_key_data,))]
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is None)
+
+        # Invalid - service
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p=%s" % (self.public_key_data,))]
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is None)
+
+        # Invalid - revoked
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p=")]
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is None)
+
+        # Multiple valid
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = [
+            DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,)),
+            DKIMUtils.extractTags("v=DKIM1; k = rsa ; h=  sha1 : sha256  ; s=ischedule ; p=%s" % (self.public_key_data,)),
+            DKIMUtils.extractTags("v=DKIM1; k = rsa ; h=  sha1 : sha256  ; s=* ; p=%s" % (self.public_key_data,)),
+        ]
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is not None)
+
+        # Multiple - some valid, some invalid
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = [
+            DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
+            DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,)),
+            DKIMUtils.extractTags("v=DKIM1; k = rsa ; h=  sha1 : sha256  ; s=ischedule ; p=%s" % (self.public_key_data,)),
+            DKIMUtils.extractTags("v=DKIM1; k = rsa ; h=  sha1 : sha256  ; s=* ; p=%s" % (self.public_key_data,)),
+        ]
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is not None)
+
+        # Multiple - invalid
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = [
+            DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
+            DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
+        ]
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is None)
+
+
+    @inlineCallbacks
+    def test_cached_key(self):
+
+        # Create cache entry
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))]
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is not None)
+
+        # Cache valid
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.keys = []
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is not None)
+
+        # Cache invalid
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = []
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is None)
+
+
+    @inlineCallbacks
+    def test_TXT_key(self):
+
+        # Need to setup a fake resolver
+        module = getModule(__name__)
+        dataPath = module.filePath.sibling("data")
+        bindPath = dataPath.child("db.example.com")
+        self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+        utils.DebugResolver = None
+        utils._initResolver()
+
+        for d, s, result in (
+            ("example.com", "_ischedule", True),
+            ("example.com", "_revoked", False),
+            ("example.com", "dkim", False),
+            ("calendar.example.com", "_ischedule", False),
+            ("example.org", "_ischedule", False),
+        ):
+            dkim = "v=1; d=%s; s = %s; t = 1234; a=rsa-sha1; q=dns/txt ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d, s,)
+            tester = PublicKeyLookup_DNSTXT(DKIMUtils.extractTags(dkim))
+            pkey = yield tester.getPublicKey(False)
+            self.assertEqual(pkey is not None, result)
+
+
+    @inlineCallbacks
+    def test_HTTP_URI_key(self):
+
+        # Need to setup a fake resolver
+        module = getModule(__name__)
+        dataPath = module.filePath.sibling("data")
+        bindPath = dataPath.child("db.example.com")
+        self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+        utils.DebugResolver = None
+        utils._initResolver()
+
+        for d, s, result in (
+            ("example.com", "_ischedule", "https://key.example.com:8443/.well-known/domainkey/example.com/_ischedule"),
+            ("www.example.com", "_ischedule", "http://key.example.com/.well-known/domainkey/www.example.com/_ischedule"),
+            ("example.org", "_ischedule", "https://example.org/.well-known/domainkey/example.org/_ischedule"),
+        ):
+            dkim = "v=1; d=%s; s = %s; t = 1234; a=rsa-sha1; q=http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d, s,)
+            tester = PublicKeyLookup_HTTP_WellKnown(DKIMUtils.extractTags(dkim))
+            uri = (yield tester._getURI())
+            self.assertEqual(uri, result)
+
+
+    @inlineCallbacks
+    def test_private_exchange(self):
+
+        keydir = self.mktemp()
+        PublicKeyLookup_PrivateExchange.directory = keydir
+        os.mkdir(keydir)
+        keyfile = os.path.join(keydir, "example.com#dkim")
+        with open(keyfile, "w") as f:
+            f.write("""v=DKIM1; p=%s
+""" % (self.public_key_data,))
+
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is not None)
+
+        dkim = "v=1; d=example.com; s = dkim2; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is None)
+
+        with open(keyfile, "w") as f:
+            f.write("""v=DKIM1; s=email; p=%s
+""" % (self.public_key_data,))
+
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is None)
+
+        with open(keyfile, "w") as f:
+            f.write("""v=DKIM1; s=email; p=%s
+v=DKIM1; s=ischedule; p=%s
+""" % (self.public_key_data, self.public_key_data,))
+
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+        lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        pubkey = (yield lookup.getPublicKey())
+        self.assertTrue(pubkey is not None)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_localservers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_localservers.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_localservers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,184 +0,0 @@
-##
-# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.web2.test.test_server import SimpleRequest
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule.localservers import Servers, SERVER_SECRET_HEADER
-from twistedcaldav.test.util import TestCase
-import StringIO as StringIO
-
-class ServerTests(TestCase):
-
-    data1 = """<?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>
-"""
-
-    data2 = """<?xml version="1.0" encoding="utf-8"?>
-<servers>
-  <server>
-    <id>00001</id>
-    <uri>http://caldav1.example.com:8008</uri>
-    <allowed-from>localhost</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>
-"""
-
-    def _setupServers(self, data=data1):
-        self.patch(config, "ServerHostName", "caldav1.example.com")
-        self.patch(config, "HTTPPort", 8008)
-
-        xmlFile = StringIO.StringIO(data)
-        servers = Servers
-        servers.load(xmlFile, ignoreIPLookupFailures=True)
-
-        return servers
-
-
-    def test_read_ok(self):
-
-        servers = self._setupServers()
-
-        self.assertTrue(servers.getServerById("00001") is not None)
-        self.assertTrue(servers.getServerById("00002") is not None)
-
-        self.assertEqual(servers.getServerById("00001").uri, "http://caldav1.example.com:8008")
-        self.assertEqual(servers.getServerById("00002").uri, "https://caldav2.example.com:8843")
-
-        self.assertEqual(servers.getServerById("00001").allowed_from_ips, set(("127.0.0.1",)))
-        self.assertEqual(servers.getServerById("00002").allowed_from_ips, set())
-
-        self.assertEqual(servers.getServerById("00001").shared_secret, "foobar")
-        self.assertEqual(servers.getServerById("00002").shared_secret, None)
-
-        self.assertEqual(len(servers.getServerById("00001").partitions), 0)
-        self.assertEqual(len(servers.getServerById("00002").partitions), 2)
-
-        self.assertEqual(servers.getServerById("00002").getPartitionURIForId("A"), "https://machine1.example.com:8443")
-        self.assertEqual(servers.getServerById("00002").getPartitionURIForId("B"), "https://machine2.example.com:8443")
-
-
-    def test_this_server(self):
-
-        servers = self._setupServers()
-
-        self.assertTrue(servers.getServerById("00001").thisServer)
-        self.assertFalse(servers.getServerById("00002").thisServer)
-
-        self.patch(config, "ServerHostName", "caldav2.example.com")
-        self.patch(config, "SSLPort", 8443)
-        self.patch(config, "BindSSLPorts", [8843])
-
-        xmlFile = StringIO.StringIO(ServerTests.data1)
-        servers = Servers
-        servers.load(xmlFile, ignoreIPLookupFailures=True)
-
-        self.assertFalse(servers.getServerById("00001").thisServer)
-        self.assertTrue(servers.getServerById("00002").thisServer)
-
-
-    def test_check_is_partitioned(self):
-
-        servers = self._setupServers()
-
-        self.assertFalse(servers.getServerById("00001").isPartitioned())
-        self.assertTrue(servers.getServerById("00002").isPartitioned())
-
-
-    def test_check_this_ip(self):
-
-        servers = self._setupServers()
-        servers.getServerById("00001").ips = set(("127.0.0.2",))
-        servers.getServerById("00002").ips = set(("127.0.0.3",))
-
-        self.assertTrue(servers.getServerById("00001").checkThisIP("127.0.0.2"))
-        self.assertFalse(servers.getServerById("00001").checkThisIP("127.0.0.3"))
-
-
-    def test_check_allowed_from(self):
-
-        for servers in (self._setupServers(), self._setupServers(data=self.data2),):
-            self.assertTrue(servers.getServerById("00001").hasAllowedFromIP())
-            self.assertFalse(servers.getServerById("00002").hasAllowedFromIP())
-
-            self.assertTrue(servers.getServerById("00001").checkAllowedFromIP("127.0.0.1"))
-            self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.2"))
-            self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.3"))
-            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.1"))
-            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.2"))
-            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.3"))
-
-
-    def test_check_shared_secret(self):
-
-        servers = self._setupServers()
-
-        request = SimpleRequest(None, "POST", "/ischedule")
-        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
-        self.assertTrue(servers.getServerById("00001").checkSharedSecret(request))
-
-        request = SimpleRequest(None, "POST", "/ischedule")
-        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
-        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
-
-        request = SimpleRequest(None, "POST", "/ischedule")
-        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
-
-        request = SimpleRequest(None, "POST", "/ischedule")
-        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
-        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
-
-        request = SimpleRequest(None, "POST", "/ischedule")
-        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
-        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
-
-        request = SimpleRequest(None, "POST", "/ischedule")
-        self.assertTrue(servers.getServerById("00002").checkSharedSecret(request))

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_localservers.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_localservers.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_localservers.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_localservers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,184 @@
+##
+# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.web2.test.test_server import SimpleRequest
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule.localservers import Servers, SERVER_SECRET_HEADER
+from twistedcaldav.test.util import TestCase
+import StringIO as StringIO
+
+class ServerTests(TestCase):
+
+    data1 = """<?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>
+"""
+
+    data2 = """<?xml version="1.0" encoding="utf-8"?>
+<servers>
+  <server>
+    <id>00001</id>
+    <uri>http://caldav1.example.com:8008</uri>
+    <allowed-from>localhost</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>
+"""
+
+    def _setupServers(self, data=data1):
+        self.patch(config, "ServerHostName", "caldav1.example.com")
+        self.patch(config, "HTTPPort", 8008)
+
+        xmlFile = StringIO.StringIO(data)
+        servers = Servers
+        servers.load(xmlFile, ignoreIPLookupFailures=True)
+
+        return servers
+
+
+    def test_read_ok(self):
+
+        servers = self._setupServers()
+
+        self.assertTrue(servers.getServerById("00001") is not None)
+        self.assertTrue(servers.getServerById("00002") is not None)
+
+        self.assertEqual(servers.getServerById("00001").uri, "http://caldav1.example.com:8008")
+        self.assertEqual(servers.getServerById("00002").uri, "https://caldav2.example.com:8843")
+
+        self.assertEqual(servers.getServerById("00001").allowed_from_ips, set(("127.0.0.1",)))
+        self.assertEqual(servers.getServerById("00002").allowed_from_ips, set())
+
+        self.assertEqual(servers.getServerById("00001").shared_secret, "foobar")
+        self.assertEqual(servers.getServerById("00002").shared_secret, None)
+
+        self.assertEqual(len(servers.getServerById("00001").partitions), 0)
+        self.assertEqual(len(servers.getServerById("00002").partitions), 2)
+
+        self.assertEqual(servers.getServerById("00002").getPartitionURIForId("A"), "https://machine1.example.com:8443")
+        self.assertEqual(servers.getServerById("00002").getPartitionURIForId("B"), "https://machine2.example.com:8443")
+
+
+    def test_this_server(self):
+
+        servers = self._setupServers()
+
+        self.assertTrue(servers.getServerById("00001").thisServer)
+        self.assertFalse(servers.getServerById("00002").thisServer)
+
+        self.patch(config, "ServerHostName", "caldav2.example.com")
+        self.patch(config, "SSLPort", 8443)
+        self.patch(config, "BindSSLPorts", [8843])
+
+        xmlFile = StringIO.StringIO(ServerTests.data1)
+        servers = Servers
+        servers.load(xmlFile, ignoreIPLookupFailures=True)
+
+        self.assertFalse(servers.getServerById("00001").thisServer)
+        self.assertTrue(servers.getServerById("00002").thisServer)
+
+
+    def test_check_is_partitioned(self):
+
+        servers = self._setupServers()
+
+        self.assertFalse(servers.getServerById("00001").isPartitioned())
+        self.assertTrue(servers.getServerById("00002").isPartitioned())
+
+
+    def test_check_this_ip(self):
+
+        servers = self._setupServers()
+        servers.getServerById("00001").ips = set(("127.0.0.2",))
+        servers.getServerById("00002").ips = set(("127.0.0.3",))
+
+        self.assertTrue(servers.getServerById("00001").checkThisIP("127.0.0.2"))
+        self.assertFalse(servers.getServerById("00001").checkThisIP("127.0.0.3"))
+
+
+    def test_check_allowed_from(self):
+
+        for servers in (self._setupServers(), self._setupServers(data=self.data2),):
+            self.assertTrue(servers.getServerById("00001").hasAllowedFromIP())
+            self.assertFalse(servers.getServerById("00002").hasAllowedFromIP())
+
+            self.assertTrue(servers.getServerById("00001").checkAllowedFromIP("127.0.0.1"))
+            self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.2"))
+            self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.3"))
+            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.1"))
+            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.2"))
+            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.3"))
+
+
+    def test_check_shared_secret(self):
+
+        servers = self._setupServers()
+
+        request = SimpleRequest(None, "POST", "/ischedule")
+        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
+        self.assertTrue(servers.getServerById("00001").checkSharedSecret(request))
+
+        request = SimpleRequest(None, "POST", "/ischedule")
+        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
+        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
+
+        request = SimpleRequest(None, "POST", "/ischedule")
+        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
+
+        request = SimpleRequest(None, "POST", "/ischedule")
+        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
+        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
+
+        request = SimpleRequest(None, "POST", "/ischedule")
+        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
+        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
+
+        request = SimpleRequest(None, "POST", "/ischedule")
+        self.assertTrue(servers.getServerById("00002").checkSharedSecret(request))

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,49 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.python.filepath import FilePath
-from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServersParser
-import twistedcaldav.test.util
-
-class Test_IScheduleServersParser(twistedcaldav.test.util.TestCase):
-    """
-    Test L{IScheduleServersParser} implementation.
-    """
-
-    def test_readXML(self):
-
-        fp = FilePath(self.mktemp())
-        fp.open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE servers SYSTEM "servertoserver.dtd">
-<servers>
-  <server>
-    <uri>https://localhost:8543/inbox</uri>
-    <allow-requests-from/>
-    <allow-requests-to/>
-    <domains>
-        <domain>example.org</domain>
-    </domains>
-    <hosts>
-        <host>127.0.0.1</host>
-    </hosts>
-  </server>
-</servers>
-"""
-)
-
-        parser = IScheduleServersParser(fp)
-        self.assertEqual(len(parser.servers), 1)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,49 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.python.filepath import FilePath
+from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServersParser
+import twistedcaldav.test.util
+
+class Test_IScheduleServersParser(twistedcaldav.test.util.TestCase):
+    """
+    Test L{IScheduleServersParser} implementation.
+    """
+
+    def test_readXML(self):
+
+        fp = FilePath(self.mktemp())
+        fp.open("w").write(
+"""<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE servers SYSTEM "servertoserver.dtd">
+<servers>
+  <server>
+    <uri>https://localhost:8543/inbox</uri>
+    <allow-requests-from/>
+    <allow-requests-to/>
+    <domains>
+        <domain>example.org</domain>
+    </domains>
+    <hosts>
+        <host>127.0.0.1</host>
+    </hosts>
+  </server>
+</servers>
+"""
+)
+
+        parser = IScheduleServersParser(fp)
+        self.assertEqual(len(parser.servers), 1)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,73 +0,0 @@
-##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.web2 import http_headers, responsecode
-from twext.web2.test.test_server import SimpleRequest
-from twisted.internet.defer import inlineCallbacks
-from twistedcaldav.config import config
-from twistedcaldav.memcachelock import MemcacheLock
-from twistedcaldav.scheduling.ischedule.resource import IScheduleInboxResource
-from twistedcaldav.test.util import TestCase
-
-class iSchedulePOST (TestCase):
-
-    def setUp(self):
-        super(iSchedulePOST, self).setUp()
-        self.createStockDirectoryService()
-        self.setupCalendars()
-        self.site.resource.putChild("ischedule", IScheduleInboxResource(self.site.resource, self._newStore))
-
-
-    @inlineCallbacks
-    def test_deadlock(self):
-        """
-        Make calendar
-        """
-
-        request = SimpleRequest(
-            self.site,
-            "POST",
-            "/ischedule",
-            headers=http_headers.Headers(rawHeaders={
-                "Originator": ("mailto:wsanchez at example.com",),
-                "Recipient": ("mailto:cdaboo at example.com",),
-            }),
-            content="""BEGIN:VCALENDAR
-CALSCALE:GREGORIAN
-PRODID:-//Example Inc.//Example Calendar//EN
-VERSION:2.0
-BEGIN:VEVENT
-DTSTAMP:20051222T205953Z
-CREATED:20060101T150000Z
-DTSTART:20060101T100000Z
-DURATION:PT1H
-SUMMARY:event 1
-UID:deadlocked
-ORGANIZER:mailto:wsanchez at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:wsanchez at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:cdaboo at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        )
-
-        # Lock the UID here to force a deadlock - but adjust the timeout so the test does not wait too long
-        self.patch(config.Scheduling.Options, "UIDLockTimeoutSeconds", 1)
-        lock = MemcacheLock("ImplicitUIDLock", "deadlocked", timeout=60, expire_time=60)
-        yield lock.acquire()
-
-        response = (yield self.send(request))
-        self.assertEqual(response.code, responsecode.CONFLICT)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_resource.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_resource.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_resource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,73 @@
+##
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.web2 import http_headers, responsecode
+from twext.web2.test.test_server import SimpleRequest
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.config import config
+from twistedcaldav.memcachelock import MemcacheLock
+from twistedcaldav.scheduling.ischedule.resource import IScheduleInboxResource
+from twistedcaldav.test.util import TestCase
+
+class iSchedulePOST (TestCase):
+
+    def setUp(self):
+        super(iSchedulePOST, self).setUp()
+        self.createStockDirectoryService()
+        self.setupCalendars()
+        self.site.resource.putChild("ischedule", IScheduleInboxResource(self.site.resource, self._newStore))
+
+
+    @inlineCallbacks
+    def test_deadlock(self):
+        """
+        Make calendar
+        """
+
+        request = SimpleRequest(
+            self.site,
+            "POST",
+            "/ischedule",
+            headers=http_headers.Headers(rawHeaders={
+                "Originator": ("mailto:wsanchez at example.com",),
+                "Recipient": ("mailto:cdaboo at example.com",),
+            }),
+            content="""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20060101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:deadlocked
+ORGANIZER:mailto:wsanchez at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:wsanchez at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:cdaboo at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        )
+
+        # Lock the UID here to force a deadlock - but adjust the timeout so the test does not wait too long
+        self.patch(config.Scheduling.Options, "UIDLockTimeoutSeconds", 1)
+        lock = MemcacheLock("ImplicitUIDLock", "deadlocked", timeout=60, expire_time=60)
+        yield lock.acquire()
+
+        response = (yield self.send(request))
+        self.assertEqual(response.code, responsecode.CONFLICT)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_utils.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_utils.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_utils.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,116 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule import utils
-from twistedcaldav.test.util import TestCase
-from twisted.python.modules import getModule
-from twisted.names.authority import BindAuthority
-from twisted.names import client
-from twisted.names.test.test_client import FakeResolver
-
-class LookupService (TestCase):
-
-
-    def setUp(self):
-        """
-        Replace the resolver with a FakeResolver
-        """
-        client.theResolver = FakeResolver()
-
-
-    def tearDown(self):
-        """
-        By setting the resolver to None, it will be recreated next time a name
-        lookup is done.
-        """
-        client.theResolver = None
-        utils.DebugResolver = None
-
-
-    def test_initResolver(self):
-        """
-        Test L{lookupServerViaSRV} with a local Bind find
-        """
-
-        # Default resolver
-        utils.DebugResolver = None
-        utils._initResolver()
-        self.assertNotEqual(utils.DebugResolver, None)
-        self.assertFalse(isinstance(utils.DebugResolver, BindAuthority))
-
-        # Patch config for Bind resolver
-        for zonefile in ("db.example.com", "db.two.zones",):
-            module = getModule(__name__)
-            dataPath = module.filePath.sibling("data")
-            bindPath = dataPath.child(zonefile)
-            self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
-            utils.DebugResolver = None
-            utils._initResolver()
-            self.assertNotEqual(utils.DebugResolver, None)
-            self.assertTrue(isinstance(utils.DebugResolver, BindAuthority))
-
-
-    @inlineCallbacks
-    def test_lookupServerViaSRV(self):
-        """
-        Test L{lookupServerViaSRV} with a local Bind find
-        """
-
-        # Patch config
-        for zonefile, checks in (
-            ("db.example.com", (("example.com", "example.com", 8443,),),),
-            ("db.two.zones", (
-                ("example.com", "example.com", 8443,),
-                ("example.org", "example.org", 8543,),
-            ),),
-        ):
-            module = getModule(__name__)
-            dataPath = module.filePath.sibling("data")
-            bindPath = dataPath.child(zonefile)
-            self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
-            utils.DebugResolver = None
-
-            for domain, result_host, result_port in checks:
-                host, port = (yield utils.lookupServerViaSRV(domain))
-                self.assertEqual(host, result_host)
-                self.assertEqual(port, result_port)
-
-
-    @inlineCallbacks
-    def test_lookupDataViaTXT(self):
-        """
-        Test L{lookupDataViaTXT} with a local Bind find
-        """
-
-        # Patch config
-        for zonefile, checks in (
-            ("db.example.com", (("example.com", "_ischedule._domainkey", "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjUfDqd8ICAL0dyq2KdjKN6LS8O/Y4yMxOxgATqtSIMi7baKXEs1w5Wj9efOC2nU+aqyhP2/J6AzfFJfSB+GV5gcIT+LAC4btJKPGjPUyXcQFJV4a73y0jIgCTBzWxdaP6qD9P9rzYlvMPcdrrKiKoAOtI3JZqAAdZudOmGlc4QQIDAQAB"),),),
-            ("db.two.zones", (
-                ("example.com", "_ischedule._domainkey", "v=DKIM1; p="),
-                ("example.org", "_ischedule2._domainkey", "v=DKIM1; s=ischedule; p="),
-            )),
-        ):
-            module = getModule(__name__)
-            dataPath = module.filePath.sibling("data")
-            bindPath = dataPath.child(zonefile)
-            self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
-            utils.DebugResolver = None
-
-            for domain, prefix, result in checks:
-                texts = (yield utils.lookupDataViaTXT(domain, prefix))
-                self.assertEqual(texts, [result])

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_utils.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_utils.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_utils.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/test/test_utils.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,116 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule import utils
+from twistedcaldav.test.util import TestCase
+from twisted.python.modules import getModule
+from twisted.names.authority import BindAuthority
+from twisted.names import client
+from twisted.names.test.test_client import FakeResolver
+
+class LookupService (TestCase):
+
+
+    def setUp(self):
+        """
+        Replace the resolver with a FakeResolver
+        """
+        client.theResolver = FakeResolver()
+
+
+    def tearDown(self):
+        """
+        By setting the resolver to None, it will be recreated next time a name
+        lookup is done.
+        """
+        client.theResolver = None
+        utils.DebugResolver = None
+
+
+    def test_initResolver(self):
+        """
+        Test L{lookupServerViaSRV} with a local Bind find
+        """
+
+        # Default resolver
+        utils.DebugResolver = None
+        utils._initResolver()
+        self.assertNotEqual(utils.DebugResolver, None)
+        self.assertFalse(isinstance(utils.DebugResolver, BindAuthority))
+
+        # Patch config for Bind resolver
+        for zonefile in ("db.example.com", "db.two.zones",):
+            module = getModule(__name__)
+            dataPath = module.filePath.sibling("data")
+            bindPath = dataPath.child(zonefile)
+            self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+            utils.DebugResolver = None
+            utils._initResolver()
+            self.assertNotEqual(utils.DebugResolver, None)
+            self.assertTrue(isinstance(utils.DebugResolver, BindAuthority))
+
+
+    @inlineCallbacks
+    def test_lookupServerViaSRV(self):
+        """
+        Test L{lookupServerViaSRV} with a local Bind find
+        """
+
+        # Patch config
+        for zonefile, checks in (
+            ("db.example.com", (("example.com", "example.com", 8443,),),),
+            ("db.two.zones", (
+                ("example.com", "example.com", 8443,),
+                ("example.org", "example.org", 8543,),
+            ),),
+        ):
+            module = getModule(__name__)
+            dataPath = module.filePath.sibling("data")
+            bindPath = dataPath.child(zonefile)
+            self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+            utils.DebugResolver = None
+
+            for domain, result_host, result_port in checks:
+                host, port = (yield utils.lookupServerViaSRV(domain))
+                self.assertEqual(host, result_host)
+                self.assertEqual(port, result_port)
+
+
+    @inlineCallbacks
+    def test_lookupDataViaTXT(self):
+        """
+        Test L{lookupDataViaTXT} with a local Bind find
+        """
+
+        # Patch config
+        for zonefile, checks in (
+            ("db.example.com", (("example.com", "_ischedule._domainkey", "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjUfDqd8ICAL0dyq2KdjKN6LS8O/Y4yMxOxgATqtSIMi7baKXEs1w5Wj9efOC2nU+aqyhP2/J6AzfFJfSB+GV5gcIT+LAC4btJKPGjPUyXcQFJV4a73y0jIgCTBzWxdaP6qD9P9rzYlvMPcdrrKiKoAOtI3JZqAAdZudOmGlc4QQIDAQAB"),),),
+            ("db.two.zones", (
+                ("example.com", "_ischedule._domainkey", "v=DKIM1; p="),
+                ("example.org", "_ischedule2._domainkey", "v=DKIM1; s=ischedule; p="),
+            )),
+        ):
+            module = getModule(__name__)
+            dataPath = module.filePath.sibling("data")
+            bindPath = dataPath.child(zonefile)
+            self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+            utils.DebugResolver = None
+
+            for domain, prefix, result in checks:
+                texts = (yield utils.lookupDataViaTXT(domain, prefix))
+                self.assertEqual(texts, [result])

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/utils.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/utils.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,182 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All 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.python.log import Logger
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.names import dns
-from twisted.names.authority import BindAuthority
-from twisted.names.client import getResolver
-from twisted.names.error import DomainError, AuthoritativeDomainError
-
-from twistedcaldav.config import config
-
-import socket
-
-log = Logger()
-
-DebugResolver = None
-
-
-def getIPsFromHost(host):
-    """
-    Map a hostname to an IPv4 or IPv6 address.
-
-    @param host: the hostname
-    @type host: C{str}
-
-    @return: a C{set} of IPs
-    """
-    ips = set()
-    for family in (socket.AF_INET, socket.AF_INET6):
-        results = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
-        for _ignore_family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in results:
-            ips.add(sockaddr[0])
-
-    return ips
-
-
-
- at inlineCallbacks
-def lookupServerViaSRV(domain, service="_ischedules"):
-
-    _initResolver()
-
-    lookup = "%s._tcp.%s" % (service, domain,)
-    log.debug("DNS SRV: lookup: %s" % (lookup,))
-    try:
-        answers = (yield DebugResolver.lookupService(lookup))
-    except (DomainError, AuthoritativeDomainError), e:
-        log.debug("DNS SRV: lookup failed: %s" % (e,))
-        returnValue(None)
-
-    if len(answers) == 1 and answers[0].type == dns.SRV \
-                         and answers[0].payload \
-                         and answers[0].payload.target == dns.Name('.'):
-        # decidedly not available
-        log.debug("DNS SRV: disabled: %s" % (lookup,))
-        returnValue(None)
-
-    servers = []
-    for a in answers:
-
-        if a.type != dns.SRV or not a.payload:
-            continue
-
-        servers.append((a.payload.priority, a.payload.weight, str(a.payload.target), a.payload.port))
-
-    log.debug("DNS SRV: lookup results: %s\n%s" % (lookup, servers,))
-
-
-    def _serverCmp(a, b):
-        if a[0] != b[0]:
-            return cmp(a[0], b[0])
-        else:
-            return cmp(a[1], b[1])
-
-    servers.sort(_serverCmp)
-    minPriority = servers[0][0]
-
-    weightIndex = zip(xrange(len(servers)), [x[1] for x in servers if x[0] == minPriority])
-    weightSum = reduce(lambda x, y: (None, x[1] + y[1]), weightIndex, (None, 0))[1]
-
-    for index, weight in weightIndex:
-        weightSum -= weight
-        if weightSum <= 0:
-            chosen = servers[index]
-            _ignore_p, _ignore_w, host, port = chosen
-            host = host.rstrip(".")
-            break
-    else:
-        log.debug("DNS SRV: unable to determine best record to use: %s" % (lookup,))
-        returnValue(None)
-
-    log.debug("DNS SRV: lookup chosen service: %s %s %s" % (lookup, host, port,))
-    returnValue((host, port,))
-
-
-
- at inlineCallbacks
-def lookupDataViaTXT(domain, prefix=""):
-
-    _initResolver()
-
-    lookup = "%s.%s" % (prefix, domain,) if prefix else domain
-    log.debug("DNS TXT: lookup: %s" % (lookup,))
-    try:
-        answers = (yield DebugResolver.lookupText(lookup))
-    except (DomainError, AuthoritativeDomainError), e:
-        log.debug("DNS TXT: lookup failed: %s" % (e,))
-        answers = ()
-
-    results = []
-    for a in answers:
-
-        if a.type != dns.TXT or not a.payload:
-            continue
-
-        results.append("".join(a.payload.data))
-
-    log.debug("DNS TXT: lookup results: %s\n%s" % (lookup, "\n".join(results),))
-    returnValue(results)
-
-
-
-class FakeBindAuthority(BindAuthority):
-
-    @inlineCallbacks
-    def _lookup(self, name, cls, type, timeout=None):
-        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])
-
-
-    def stripComments(self, lines):
-        """
-        Work around a bug in the base implementation that causes parsing of TXT RRs with
-        a ; in the RDATA to fail because the ; is treated as the start of a comment. Here
-        we simply ignore all comments.
-        """
-        return [
-            (a.find(';') == -1 or "TXT" in a) and a or a[:a.find(';')] for a in [
-                b.strip() for b in lines
-            ]
-        ]
-
-
-    def parseLines(self, lines):
-        """
-        Work around a bug in the base implementation that causes parsing of TXT RRs with
-        spaces in the RDATA to be broken into multiple fragments and for quotes around the
-        data to not be removed.
-        """
-        for line in lines:
-            if line[3] == "TXT":
-                line[4] = " ".join(line[4:])[1:-1]
-                del line[5:]
-
-        BindAuthority.parseLines(self, lines)
-
-
-
-def _initResolver():
-    global DebugResolver
-    if DebugResolver is None:
-        if config.Scheduling.iSchedule.DNSDebug:
-            DebugResolver = FakeBindAuthority(config.Scheduling.iSchedule.DNSDebug)
-        else:
-            DebugResolver = getResolver()

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/utils.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/utils.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/utils.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,182 @@
+##
+# Copyright (c) 2012 Apple Inc. All 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.python.log import Logger
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.names import dns
+from twisted.names.authority import BindAuthority
+from twisted.names.client import getResolver
+from twisted.names.error import DomainError, AuthoritativeDomainError
+
+from twistedcaldav.config import config
+
+import socket
+
+log = Logger()
+
+DebugResolver = None
+
+
+def getIPsFromHost(host):
+    """
+    Map a hostname to an IPv4 or IPv6 address.
+
+    @param host: the hostname
+    @type host: C{str}
+
+    @return: a C{set} of IPs
+    """
+    ips = set()
+    for family in (socket.AF_INET, socket.AF_INET6):
+        results = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
+        for _ignore_family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in results:
+            ips.add(sockaddr[0])
+
+    return ips
+
+
+
+ at inlineCallbacks
+def lookupServerViaSRV(domain, service="_ischedules"):
+
+    _initResolver()
+
+    lookup = "%s._tcp.%s" % (service, domain,)
+    log.debug("DNS SRV: lookup: %s" % (lookup,))
+    try:
+        answers = (yield DebugResolver.lookupService(lookup))
+    except (DomainError, AuthoritativeDomainError), e:
+        log.debug("DNS SRV: lookup failed: %s" % (e,))
+        returnValue(None)
+
+    if len(answers) == 1 and answers[0].type == dns.SRV \
+                         and answers[0].payload \
+                         and answers[0].payload.target == dns.Name('.'):
+        # decidedly not available
+        log.debug("DNS SRV: disabled: %s" % (lookup,))
+        returnValue(None)
+
+    servers = []
+    for a in answers:
+
+        if a.type != dns.SRV or not a.payload:
+            continue
+
+        servers.append((a.payload.priority, a.payload.weight, str(a.payload.target), a.payload.port))
+
+    log.debug("DNS SRV: lookup results: %s\n%s" % (lookup, servers,))
+
+
+    def _serverCmp(a, b):
+        if a[0] != b[0]:
+            return cmp(a[0], b[0])
+        else:
+            return cmp(a[1], b[1])
+
+    servers.sort(_serverCmp)
+    minPriority = servers[0][0]
+
+    weightIndex = zip(xrange(len(servers)), [x[1] for x in servers if x[0] == minPriority])
+    weightSum = reduce(lambda x, y: (None, x[1] + y[1]), weightIndex, (None, 0))[1]
+
+    for index, weight in weightIndex:
+        weightSum -= weight
+        if weightSum <= 0:
+            chosen = servers[index]
+            _ignore_p, _ignore_w, host, port = chosen
+            host = host.rstrip(".")
+            break
+    else:
+        log.debug("DNS SRV: unable to determine best record to use: %s" % (lookup,))
+        returnValue(None)
+
+    log.debug("DNS SRV: lookup chosen service: %s %s %s" % (lookup, host, port,))
+    returnValue((host, port,))
+
+
+
+ at inlineCallbacks
+def lookupDataViaTXT(domain, prefix=""):
+
+    _initResolver()
+
+    lookup = "%s.%s" % (prefix, domain,) if prefix else domain
+    log.debug("DNS TXT: lookup: %s" % (lookup,))
+    try:
+        answers = (yield DebugResolver.lookupText(lookup))
+    except (DomainError, AuthoritativeDomainError), e:
+        log.debug("DNS TXT: lookup failed: %s" % (e,))
+        answers = ()
+
+    results = []
+    for a in answers:
+
+        if a.type != dns.TXT or not a.payload:
+            continue
+
+        results.append("".join(a.payload.data))
+
+    log.debug("DNS TXT: lookup results: %s\n%s" % (lookup, "\n".join(results),))
+    returnValue(results)
+
+
+
+class FakeBindAuthority(BindAuthority):
+
+    @inlineCallbacks
+    def _lookup(self, name, cls, type, timeout=None):
+        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])
+
+
+    def stripComments(self, lines):
+        """
+        Work around a bug in the base implementation that causes parsing of TXT RRs with
+        a ; in the RDATA to fail because the ; is treated as the start of a comment. Here
+        we simply ignore all comments.
+        """
+        return [
+            (a.find(';') == -1 or "TXT" in a) and a or a[:a.find(';')] for a in [
+                b.strip() for b in lines
+            ]
+        ]
+
+
+    def parseLines(self, lines):
+        """
+        Work around a bug in the base implementation that causes parsing of TXT RRs with
+        spaces in the RDATA to be broken into multiple fragments and for quotes around the
+        data to not be removed.
+        """
+        for line in lines:
+            if line[3] == "TXT":
+                line[4] = " ".join(line[4:])[1:-1]
+                del line[5:]
+
+        BindAuthority.parseLines(self, lines)
+
+
+
+def _initResolver():
+    global DebugResolver
+    if DebugResolver is None:
+        if config.Scheduling.iSchedule.DNSDebug:
+            DebugResolver = FakeBindAuthority(config.Scheduling.iSchedule.DNSDebug)
+        else:
+            DebugResolver = getResolver()

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/xml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py	2012-10-25 22:50:21 UTC (rev 9988)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/xml.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,281 +0,0 @@
-##
-# Copyright (c) 2011-2012 Apple Inc. All 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.
-##
-
-"""
-This module provides XML definitions for use with Timezone Standard Service.
-"""
-
-from twistedcaldav.config import config
-from twistedcaldav.ical import Component as iComponent
-from txdav.xml.element import PCDATAElement, WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
-from txdav.xml.element import registerElement
-
-
-##
-# iSchedule XML Definitions
-##
-
-ischedule_namespace = "urn:ietf:params:xml:ns:ischedule"
-
-
- at registerElement
-class QueryResult (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "query-result"
-
-    allowed_children = {
-        (ischedule_namespace, "capability-set"): (0, None),
-    }
-
-
-
- at registerElement
-class Capabilities (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "capabilities"
-
-    allowed_children = {
-        (ischedule_namespace, "versions"): (1, 1),
-        (ischedule_namespace, "scheduling-messages"): (1, 1),
-        (ischedule_namespace, "calendar-data-types"): (1, 1),
-        (ischedule_namespace, "attachments"): (1, 1),
-        (ischedule_namespace, "supported-recipient-uri-scheme-set"): (1, 1),
-        (ischedule_namespace, "max-content-length"): (1, 1),
-        (ischedule_namespace, "min-date-time"): (1, 1),
-        (ischedule_namespace, "max-date-time"): (1, 1),
-        (ischedule_namespace, "max-instances"): (1, 1),
-        (ischedule_namespace, "max-recipients"): (1, 1),
-        (ischedule_namespace, "administrator"): (1, 1),
-    }
-
-
-
- at registerElement
-class Versions (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "versions"
-
-    allowed_children = {
-        (ischedule_namespace, "version"): (1, None),
-    }
-
-
-
- at registerElement
-class Version (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "version"
-
-
-
- at registerElement
-class SchedulingMessages (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "scheduling-messages"
-
-    allowed_children = {
-        (ischedule_namespace, "component"): (1, None),
-    }
-
-
-
- at registerElement
-class Component (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "component"
-
-    allowed_children = {
-        (ischedule_namespace, "method"): (0, None),
-    }
-    allowed_attributes = {"name": True}
-
-
-
- at registerElement
-class Method (WebDAVEmptyElement):
-    namespace = ischedule_namespace
-    name = "method"
-
-    allowed_attributes = {"name": True}
-
-
-
- at registerElement
-class CalendarDataTypes (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "calendar-data-types"
-
-    allowed_children = {
-        (ischedule_namespace, "calendar-data-type"): (1, None),
-    }
-
-
-
- at registerElement
-class CalendarDataType (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "calendar-data-type"
-
-    allowed_attributes = {
-        "content-type": True,
-        "version": True,
-    }
-
-
-
- at registerElement
-class Attachments (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "attachments"
-
-    allowed_children = {
-        (ischedule_namespace, "inline"): (0, 1),
-        (ischedule_namespace, "external"): (0, 1),
-    }
-
-
-
- at registerElement
-class Inline (WebDAVEmptyElement):
-    namespace = ischedule_namespace
-    name = "inline"
-
-
-
- at registerElement
-class External (WebDAVEmptyElement):
-    namespace = ischedule_namespace
-    name = "external"
-
-
-
- at registerElement
-class MaxContentLength (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "max-content-length"
-
-
-
- at registerElement
-class MinDateTime (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "min-date-time"
-
-
-
- at registerElement
-class MaxDateTime (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "max-date-time"
-
-
-
- at registerElement
-class MaxInstances (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "max-instances"
-
-
-
- at registerElement
-class MaxRecipients (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "max-recipients"
-
-
-
- at registerElement
-class Administrator (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "administrator"
-
-
-
- at registerElement
-class ScheduleResponse (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "schedule-response"
-
-    allowed_children = {
-        (ischedule_namespace, "response"): (0, None),
-    }
-
-
-
- at registerElement
-class Response (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "response"
-
-    allowed_children = {
-        (ischedule_namespace, "recipient"): (1, 1),
-        (ischedule_namespace, "request-status"): (1, 1),
-        (ischedule_namespace, "calendar-data"): (0, 1),
-        (ischedule_namespace, "error"): (0, 1),
-        (ischedule_namespace, "response-description"): (0, 1),
-    }
-
-
-
- at registerElement
-class Recipient (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "recipient"
-
-
-
- at registerElement
-class RequestStatus (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "request-status"
-
-
-
- at registerElement
-class CalendarData (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "calendar-data"
-
-
-    @classmethod
-    def fromCalendar(clazz, calendar):
-        if isinstance(calendar, str):
-            if not calendar:
-                raise ValueError("Missing calendar data")
-            return clazz(PCDATAElement(calendar))
-        elif isinstance(calendar, iComponent):
-            assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
-            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
-        else:
-            raise ValueError("Not a calendar: %s" % (calendar,))
-
-    fromTextData = fromCalendar
-
-
-
- at registerElement
-class Error (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "error"
-
-    allowed_children = {WebDAVElement: (0, None)}
-
-
-
- at registerElement
-class ResponseDescription (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "response-description"

Copied: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/xml.py (from rev 9988, CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/xml.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule/xml.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,281 @@
+##
+# Copyright (c) 2011-2012 Apple Inc. All 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.
+##
+
+"""
+This module provides XML definitions for use with Timezone Standard Service.
+"""
+
+from twistedcaldav.config import config
+from twistedcaldav.ical import Component as iComponent
+from txdav.xml.element import PCDATAElement, WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
+from txdav.xml.element import registerElement
+
+
+##
+# iSchedule XML Definitions
+##
+
+ischedule_namespace = "urn:ietf:params:xml:ns:ischedule"
+
+
+ at registerElement
+class QueryResult (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "query-result"
+
+    allowed_children = {
+        (ischedule_namespace, "capability-set"): (0, None),
+    }
+
+
+
+ at registerElement
+class Capabilities (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "capabilities"
+
+    allowed_children = {
+        (ischedule_namespace, "versions"): (1, 1),
+        (ischedule_namespace, "scheduling-messages"): (1, 1),
+        (ischedule_namespace, "calendar-data-types"): (1, 1),
+        (ischedule_namespace, "attachments"): (1, 1),
+        (ischedule_namespace, "supported-recipient-uri-scheme-set"): (1, 1),
+        (ischedule_namespace, "max-content-length"): (1, 1),
+        (ischedule_namespace, "min-date-time"): (1, 1),
+        (ischedule_namespace, "max-date-time"): (1, 1),
+        (ischedule_namespace, "max-instances"): (1, 1),
+        (ischedule_namespace, "max-recipients"): (1, 1),
+        (ischedule_namespace, "administrator"): (1, 1),
+    }
+
+
+
+ at registerElement
+class Versions (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "versions"
+
+    allowed_children = {
+        (ischedule_namespace, "version"): (1, None),
+    }
+
+
+
+ at registerElement
+class Version (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "version"
+
+
+
+ at registerElement
+class SchedulingMessages (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "scheduling-messages"
+
+    allowed_children = {
+        (ischedule_namespace, "component"): (1, None),
+    }
+
+
+
+ at registerElement
+class Component (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "component"
+
+    allowed_children = {
+        (ischedule_namespace, "method"): (0, None),
+    }
+    allowed_attributes = {"name": True}
+
+
+
+ at registerElement
+class Method (WebDAVEmptyElement):
+    namespace = ischedule_namespace
+    name = "method"
+
+    allowed_attributes = {"name": True}
+
+
+
+ at registerElement
+class CalendarDataTypes (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "calendar-data-types"
+
+    allowed_children = {
+        (ischedule_namespace, "calendar-data-type"): (1, None),
+    }
+
+
+
+ at registerElement
+class CalendarDataType (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "calendar-data-type"
+
+    allowed_attributes = {
+        "content-type": True,
+        "version": True,
+    }
+
+
+
+ at registerElement
+class Attachments (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "attachments"
+
+    allowed_children = {
+        (ischedule_namespace, "inline"): (0, 1),
+        (ischedule_namespace, "external"): (0, 1),
+    }
+
+
+
+ at registerElement
+class Inline (WebDAVEmptyElement):
+    namespace = ischedule_namespace
+    name = "inline"
+
+
+
+ at registerElement
+class External (WebDAVEmptyElement):
+    namespace = ischedule_namespace
+    name = "external"
+
+
+
+ at registerElement
+class MaxContentLength (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "max-content-length"
+
+
+
+ at registerElement
+class MinDateTime (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "min-date-time"
+
+
+
+ at registerElement
+class MaxDateTime (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "max-date-time"
+
+
+
+ at registerElement
+class MaxInstances (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "max-instances"
+
+
+
+ at registerElement
+class MaxRecipients (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "max-recipients"
+
+
+
+ at registerElement
+class Administrator (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "administrator"
+
+
+
+ at registerElement
+class ScheduleResponse (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "schedule-response"
+
+    allowed_children = {
+        (ischedule_namespace, "response"): (0, None),
+    }
+
+
+
+ at registerElement
+class Response (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "response"
+
+    allowed_children = {
+        (ischedule_namespace, "recipient"): (1, 1),
+        (ischedule_namespace, "request-status"): (1, 1),
+        (ischedule_namespace, "calendar-data"): (0, 1),
+        (ischedule_namespace, "error"): (0, 1),
+        (ischedule_namespace, "response-description"): (0, 1),
+    }
+
+
+
+ at registerElement
+class Recipient (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "recipient"
+
+
+
+ at registerElement
+class RequestStatus (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "request-status"
+
+
+
+ at registerElement
+class CalendarData (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "calendar-data"
+
+
+    @classmethod
+    def fromCalendar(clazz, calendar):
+        if isinstance(calendar, str):
+            if not calendar:
+                raise ValueError("Missing calendar data")
+            return clazz(PCDATAElement(calendar))
+        elif isinstance(calendar, iComponent):
+            assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
+            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
+        else:
+            raise ValueError("Not a calendar: %s" % (calendar,))
+
+    fromTextData = fromCalendar
+
+
+
+ at registerElement
+class Error (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "error"
+
+    allowed_children = {WebDAVElement: (0, None)}
+
+
+
+ at registerElement
+class ResponseDescription (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "response-description"

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischedule.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,343 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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 StringIO import StringIO
-
-from twisted.internet.defer import inlineCallbacks, DeferredList, succeed
-from twisted.internet.protocol import Factory
-
-from twisted.python.failure import Failure
-
-from twext.web2 import responsecode
-from twext.web2.client.http import ClientRequest
-from twext.web2.client.http import HTTPClientProtocol
-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.python.log import Logger, logLevels
-from twext.web2.dav.http import ErrorResponse
-
-from twistedcaldav.client.pool import _configuredClientContextFactory
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServerRecord
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.util import utf8String, normalizationLookup
-from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser, RemoteCalendarUser,\
-    OtherServerCalendarUser
-from twext.internet.gaiendpoint import GAIEndpoint
-
-"""
-Handles the sending of iSchedule scheduling messages. Used for both cross-domain scheduling,
-as well as internal partitioning or podding.
-"""
-
-__all__ = [
-    "ScheduleViaISchedule",
-]
-
-log = Logger()
-
-class ScheduleViaISchedule(DeliveryService):
-    
-    @classmethod
-    def serviceType(cls):
-        return DeliveryService.serviceType_ischedule
-
-    @classmethod
-    def matchCalendarUserAddress(cls, cuaddr):
-
-        # TODO: here is where we would attempt service discovery based on the cuaddr.
-        
-        # Do default match
-        return super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr)
-
-    def generateSchedulingResponses(self, refreshOnly=False):
-        """
-        Generate scheduling responses for remote recipients.
-        """
-        
-        # Group recipients by server so that we can do a single request with multiple recipients
-        # to each different server.
-        groups = {}
-        servermgr = IScheduleServers()
-        for recipient in self.recipients:
-            if isinstance(recipient, RemoteCalendarUser):
-                # Map the recipient's domain to a server
-                server = servermgr.mapDomain(recipient.domain)
-            elif isinstance(recipient, PartitionedCalendarUser):
-                server = self._getServerForPartitionedUser(recipient)
-            elif isinstance(recipient, OtherServerCalendarUser):
-                server = self._getServerForOtherServerUser(recipient)
-            else:
-                assert False, "Incorrect calendar user address class"
-            if not server:
-                # Cannot do server-to-server for this recipient.
-                err = HTTPError(ErrorResponse(
-                    responsecode.NOT_FOUND,
-                    (caldav_namespace, "recipient-allowed"),
-                    "No server for recipient",
-                ))
-                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
-            
-                # Process next recipient
-                continue
-            
-            if not server.allow_to:
-                # Cannot do server-to-server outgoing requests for this server.
-                err = HTTPError(ErrorResponse(
-                    responsecode.NOT_FOUND,
-                    (caldav_namespace, "recipient-allowed"),
-                    "Cannot send to recipient's server",
-                ))
-                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-            
-                # Process next recipient
-                continue
-            
-            groups.setdefault(server, []).append(recipient)
-
-        if len(groups) == 0:
-            return
-
-        # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
-        # we will generate for each request. That way we can have parallel requests in progress
-        # rather than serialize them.
-        deferreds = []
-        for server, recipients in groups.iteritems():
-            requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses, refreshOnly)
-            deferreds.append(requestor.doRequest())
-
-        return DeferredList(deferreds)
-
-    def _getServerForPartitionedUser(self, recipient):
-        
-        if not hasattr(self, "partitionedServers"):
-            self.partitionedServers = {}
-            
-        partition = recipient.principal.partitionURI()
-        if partition not in self.partitionedServers:
-            self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
-            self.partitionedServers[partition].unNormalizeAddresses = False
-            self.partitionedServers[partition].moreHeaders.append(recipient.principal.server().secretHeader())
-        
-        return self.partitionedServers[partition]
-
-    def _getServerForOtherServerUser(self, recipient):
-        
-        if not hasattr(self, "otherServers"):
-            self.otherServers = {}
-            
-        serverURI = recipient.principal.serverURI()
-        if serverURI not in self.otherServers:
-            self.otherServers[serverURI] = IScheduleServerRecord(uri=joinURL(serverURI, "/ischedule"))
-            self.otherServers[serverURI].unNormalizeAddresses = not recipient.principal.server().isImplicit
-            self.otherServers[serverURI].moreHeaders.append(recipient.principal.server().secretHeader())
-        
-        return self.otherServers[serverURI]
-
-class IScheduleRequest(object):
-    
-    def __init__(self, scheduler, server, recipients, responses, refreshOnly=False):
-
-        self.scheduler = scheduler
-        self.server = server
-        self.recipients = recipients
-        self.responses = responses
-        self.refreshOnly = refreshOnly
-        
-        self._generateHeaders()
-        self._prepareData()
-        
-    @inlineCallbacks
-    def doRequest(self):
-        
-        # Generate an HTTP client request
-        try:
-            if not hasattr(self.scheduler.request, "extendedLogItems"):
-                self.scheduler.request.extendedLogItems = {}
-            if "itip.ischedule" not in self.scheduler.request.extendedLogItems:
-                self.scheduler.request.extendedLogItems["itip.ischedule"] = 0
-            self.scheduler.request.extendedLogItems["itip.ischedule"] += 1
-
-            from twisted.internet import reactor
-            f = Factory()
-            f.protocol = HTTPClientProtocol
-            if self.server.ssl:
-                ep = GAIEndpoint(reactor, self.server.host, self.server.port,
-                                 _configuredClientContextFactory())
-            else:
-                ep = GAIEndpoint(reactor, self.server.host, self.server.port)
-            proto = (yield ep.connect(f))
-            
-            request = ClientRequest("POST", self.server.path, self.headers, self.data)
-            yield self.logRequest("debug", "Sending server-to-server POST request:", request)
-            response = (yield proto.submitRequest(request))
-    
-            yield self.logResponse("debug", "Received server-to-server POST response:", response)
-            xml = (yield davXMLFromStream(response.stream))
-    
-            self._parseResponse(xml)
-
-        except Exception, e:
-            # Generated failed responses for each recipient
-            log.err("Could not do server-to-server request : %s %s" % (self, e))
-            for recipient in self.recipients:
-                err = HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (caldav_namespace, "recipient-failed"),
-                    "Server-to-server request failed",
-                ))
-                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
-    def logRequest(self, level, message, request, **kwargs):
-        """
-        Log an HTTP request.
-        """
-
-        assert level in logLevels
-
-        if log.willLogAtLevel(level):
-            iostr = StringIO()
-            iostr.write("%s\n" % (message,))
-            if hasattr(request, "clientproto"):
-                protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
-            else:
-                protocol = "HTTP/1.1"
-            iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
-            for name, valuelist in request.headers.getAllRawHeaders():
-                for value in valuelist:
-                    # Do not log authorization details
-                    if name not in ("Authorization",):
-                        iostr.write("%s: %s\n" % (name, value))
-                    else:
-                        iostr.write("%s: xxxxxxxxx\n" % (name,))
-            iostr.write("\n")
-            
-            # We need to play a trick with the request stream as we can only read it once. So we
-            # read it, store the value in a MemoryStream, and replace the request's stream with that,
-            # so the data can be read again.
-            def _gotData(data):
-                iostr.write(data)
-                
-                request.stream = MemoryStream(data if data is not None else "")
-                request.stream.doStartReading = None
-            
-                log.emit(level, iostr.getvalue(), **kwargs)
-
-            d = allDataFromStream(request.stream)
-            d.addCallback(_gotData)
-            return d
-        
-        else:
-            return succeed(None)
-    
-    def logResponse(self, level, message, response, **kwargs):
-        """
-        Log an HTTP request.
-        """
-        assert level in logLevels
-
-        if log.willLogAtLevel(level):
-            iostr = StringIO()
-            iostr.write("%s\n" % (message,))
-            code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
-            iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
-            for name, valuelist in response.headers.getAllRawHeaders():
-                for value in valuelist:
-                    # Do not log authorization details
-                    if name not in ("WWW-Authenticate",):
-                        iostr.write("%s: %s\n" % (name, value))
-                    else:
-                        iostr.write("%s: xxxxxxxxx\n" % (name,))
-            iostr.write("\n")
-            
-            # We need to play a trick with the response stream to ensure we don't mess it up. So we
-            # read it, store the value in a MemoryStream, and replace the response's stream with that,
-            # so the data can be read again.
-            def _gotData(data):
-                iostr.write(data)
-                
-                response.stream = MemoryStream(data if data is not None else "")
-                response.stream.doStartReading = None
-            
-                log.emit(level, iostr.getvalue(), **kwargs)
-                
-            d = allDataFromStream(response.stream)
-            d.addCallback(_gotData)
-            return d
-
-    def _generateHeaders(self):
-        self.headers = Headers()
-        self.headers.setHeader('Host', utf8String(self.server.host + ":%s" % (self.server.port,)))
-        
-        # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
-        self.headers.addRawHeader('Originator', utf8String(self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee))
-        self._doAuthentication()
-        for recipient in self.recipients:
-            self.headers.addRawHeader('Recipient', utf8String(recipient.cuaddr))
-        self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
-
-        # Add any additional headers
-        for name, value in self.server.moreHeaders:
-            self.headers.addRawHeader(name, value)
-            
-        if self.refreshOnly:
-            self.headers.addRawHeader("X-CALENDARSERVER-ITIP-REFRESHONLY", "T")
-
-    def _doAuthentication(self):
-        if self.server.authentication and self.server.authentication[0] == "basic":
-            self.headers.setHeader(
-                'Authorization',
-                ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
-            )
-
-    def _prepareData(self):
-        if self.server.unNormalizeAddresses and self.scheduler.method == "PUT": 
-            normalizedCalendar = self.scheduler.calendar.duplicate()
-            normalizedCalendar.normalizeCalendarUserAddresses(
-                normalizationLookup,
-                self.scheduler.resource.principalForCalendarUserAddress,
-                toUUID=False)
-        else:
-            normalizedCalendar = self.scheduler.calendar
-        self.data = str(normalizedCalendar)
-
-    def _parseResponse(self, xml):
-
-        # Check for correct root element
-        schedule_response = xml.root_element
-        if not isinstance(schedule_response, caldavxml.ScheduleResponse) or not schedule_response.children:
-            raise HTTPError(responsecode.BAD_REQUEST)
-        
-        # Parse each response - do this twice: once looking for errors that will
-        # result in all recipients shown as failures; the second loop adds all the
-        # valid responses to the actual result.
-        for response in schedule_response.children:
-            if not isinstance(response, caldavxml.Response) or not response.children:
-                raise HTTPError(responsecode.BAD_REQUEST)
-            recipient = response.childOfType(caldavxml.Recipient)
-            request_status = response.childOfType(caldavxml.RequestStatus)
-            if not recipient or not request_status:
-                raise HTTPError(responsecode.BAD_REQUEST)
-        for response in schedule_response.children:
-            self.responses.clone(response)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischeduleservers.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/ischeduleservers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,195 +0,0 @@
-##
-# Copyright (c) 2006-2012 Apple Inc. All 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.python.filepath import CachingFilePath as FilePath
-
-from twext.python.log import Logger
-
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.xmlutil import readXML
-
-"""
-XML based iSchedule configuration file handling.
-"""
-
-__all__ = [
-    "IScheduleServers",
-]
-
-log = Logger()
-
-class IScheduleServers(object):
-    
-    _fileInfo = None
-    _xmlFile = None
-    _servers = None
-    _domainMap = None
-    
-    def __init__(self):
-        
-        self._loadConfig()
-
-    def _loadConfig(self):
-        if IScheduleServers._servers is None:
-            IScheduleServers._xmlFile = FilePath(
-                fullServerPath(
-                    config.ConfigRoot,
-                    config.Scheduling[DeliveryService.serviceType_ischedule]["Servers"]
-                )
-            )
-        IScheduleServers._xmlFile.restat()
-        fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
-        if fileInfo != IScheduleServers._fileInfo:
-            parser = IScheduleServersParser(IScheduleServers._xmlFile)
-            IScheduleServers._servers = parser.servers
-            self._mapDomains()
-            IScheduleServers._fileInfo = fileInfo
-        
-    def _mapDomains(self):
-        IScheduleServers._domainMap = {}
-        for server in IScheduleServers._servers:
-            for domain in server.domains:
-                IScheduleServers._domainMap[domain] = server
-    
-    def mapDomain(self, domain):
-        """
-        Map a calendar user address domain to a suitable server that can
-        handle server-to-server requests for that user.
-        """
-        return IScheduleServers._domainMap.get(domain)
-
-ELEMENT_SERVERS                 = "servers"
-ELEMENT_SERVER                  = "server"
-ELEMENT_URI                     = "uri"
-ELEMENT_AUTHENTICATION          = "authentication"
-ATTRIBUTE_TYPE                  = "type"
-ATTRIBUTE_BASICAUTH             = "basic"
-ELEMENT_USER                    = "user"
-ELEMENT_PASSWORD                = "password"
-ELEMENT_ALLOW_REQUESTS_FROM     = "allow-requests-from"
-ELEMENT_ALLOW_REQUESTS_TO       = "allow-requests-to"
-ELEMENT_DOMAINS                 = "domains"
-ELEMENT_DOMAIN                  = "domain"
-ELEMENT_CLIENT_HOSTS            = "hosts"
-ELEMENT_HOST                    = "host"
-
-class IScheduleServersParser(object):
-    """
-    Server-to-server configuration file parser.
-    """
-    def __repr__(self):
-        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
-
-    def __init__(self, xmlFile):
-
-        self.servers = []
-        
-        # Read in XML
-        try:
-            _ignore_tree, servers_node = readXML(xmlFile.path, ELEMENT_SERVERS)
-        except ValueError:
-            log.error("Ignoring file %r because it is not a server-to-server config file" % (xmlFile,))
-
-        self._parseXML(servers_node)
-        
-    def _parseXML(self, node):
-        """
-        Parse the XML root node from the server-to-server configuration document.
-        @param node: the L{Node} to parse.
-        """
-        for child in node.getchildren():
-            if child.tag == ELEMENT_SERVER:
-                self.servers.append(IScheduleServerRecord())
-                self.servers[-1].parseXML(child)
-                
-class IScheduleServerRecord (object):
-    """
-    Contains server-to-server details.
-    """
-    def __init__(self, uri=None):
-        """
-        @param recordType: record type for directory entry.
-        """
-        self.uri = ""
-        self.authentication = None
-        self.allow_from = False
-        self.allow_to = True
-        self.domains = []
-        self.client_hosts = []
-        self.unNormalizeAddresses = True
-        self.moreHeaders = []
-        
-        if uri:
-            self.uri = uri
-            self._parseDetails()
-
-    def parseXML(self, node):
-        for child in node.getchildren():
-            if child.tag == ELEMENT_URI:
-                self.uri = child.text
-            elif child.tag == ELEMENT_AUTHENTICATION:
-                self._parseAuthentication(child)
-            elif child.tag == ELEMENT_ALLOW_REQUESTS_FROM:
-                self.allow_from = True
-            elif child.tag == ELEMENT_ALLOW_REQUESTS_TO:
-                self.allow_to = True
-            elif child.tag == ELEMENT_DOMAINS:
-                self._parseList(child, ELEMENT_DOMAIN, self.domains)
-            elif child.tag == ELEMENT_CLIENT_HOSTS:
-                self._parseList(child, ELEMENT_HOST, self.client_hosts)
-            else:
-                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child.tag,))
-        
-        self._parseDetails()
-
-    def _parseList(self, node, element_name, appendto):
-        for child in node.getchildren():
-            if child.tag == element_name:
-                appendto.append(child.text)
-
-    def _parseAuthentication(self, node):
-        atype = node.getAttribute(ATTRIBUTE_TYPE, "")
-        if atype != ATTRIBUTE_BASICAUTH:
-            return
-
-        for child in node.getchildren():
-            if child.tag == ELEMENT_USER:
-                user = child.text
-            elif child.tag == ELEMENT_PASSWORD:
-                password = child.text
-        
-        self.authentication = ("basic", user, password,)
-
-    def _parseDetails(self):
-        # Extract scheme, host, port and path
-        if self.uri.startswith("http://"):
-            self.ssl = False
-            rest = self.uri[7:]
-        elif self.uri.startswith("https://"):
-            self.ssl = True
-            rest = self.uri[8:]
-        
-        splits = rest.split("/", 1)
-        hostport = splits[0].split(":")
-        self.host = hostport[0]
-        if len(hostport) > 1:
-            self.port = int(hostport[1])
-        else:
-            self.port = {False:80, True:443}[self.ssl]
-        self.path = "/"
-        if len(splits) > 1:
-            self.path += splits[1]

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/itip.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/itip.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -30,15 +30,13 @@
 from twext.python.log import Logger
 
 from twistedcaldav.config import config
-from twistedcaldav.ical import Property, iCalendarProductID, Component,\
+from twistedcaldav.ical import Property, iCalendarProductID, Component, \
     ignoredComponents
 
 from pycalendar.datetime import PyCalendarDateTime
 
 log = Logger()
 
-__version__ = "0.0"
-
 __all__ = [
     "iTipProcessing",
     "iTipGenerator",
@@ -50,10 +48,10 @@
     def processNewRequest(itip_message, recipient=None, creating=False):
         """
         Process a METHOD=REQUEST for a brand new calendar object.
-        
+
         @param itip_message: the iTIP message calendar object to process.
         @type itip_message:
-        
+
         @return: calendar object ready to save
         """
         assert itip_message.propertyValue("METHOD") == "REQUEST", "iTIP message must have METHOD:REQUEST"
@@ -65,7 +63,7 @@
 
         if recipient:
             iTipProcessing.addTranspForNeedsAction(calendar.subcomponents(), recipient)
-                
+
             # Check for incoming DECLINED
             if creating:
                 master = calendar.masterComponent()
@@ -81,23 +79,24 @@
                             calendar.removeComponent(component)
 
         return calendar
-        
+
+
     @staticmethod
     def processRequest(itip_message, calendar, recipient):
         """
         Process a METHOD=REQUEST. We need to merge per-attendee properties such as TRANPS, COMPLETED etc
         with the data coming from the organizer.
-        
+
         @param itip_message: the iTIP message calendar object to process.
         @type itip_message:
         @param calendar: the calendar object to apply the REQUEST to
         @type calendar:
-        
+
         @return: a C{tuple} of:
             calendar object ready to save, or C{None} (request should be ignored)
             a C{set} of recurrences that changed, or C{None}
         """
-        
+
         # Check sequencing
         if not iTipProcessing.sequenceComparison(itip_message, calendar):
             # Ignore out of sequence message
@@ -126,10 +125,10 @@
             organizer_schedule_status = None
 
         if itip_message.masterComponent() is not None:
-            
+
             # Get a new calendar object first
             new_calendar = iTipProcessing.processNewRequest(itip_message, recipient)
-            
+
             # Copy over master alarms, comments
             master_component = new_calendar.masterComponent()
             for alarm in master_valarms:
@@ -140,16 +139,16 @@
                 master_component.replaceProperty(transp)
             for completed in completeds:
                 master_component.replaceProperty(completed)
-            if organizer_schedule_status: 
+            if organizer_schedule_status:
                 organizer = master_component.getProperty("ORGANIZER")
                 if organizer:
                     organizer.setParameter("SCHEDULE-STATUS", organizer_schedule_status)
-                
+
             # 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, component, recipient)
-            
+
             # Now try to match recurrences from the old calendar
             for component in calendar.subcomponents():
                 if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
@@ -160,7 +159,7 @@
                         if new_component:
                             new_calendar.addComponent(new_component)
                             iTipProcessing.transferItems(calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, new_component, recipient)
-            
+
             # Replace the entire object
             return new_calendar, rids
 
@@ -185,24 +184,25 @@
             # Write back the modified object
             return calendar, rids
 
+
     @staticmethod
     def processCancel(itip_message, calendar, autoprocessing=False):
         """
         Process a METHOD=CANCEL.
-        
+
         TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
-        
+
         @param itip_message: the iTIP message calendar object to process.
         @type itip_message:
         @param calendar: the calendar object to apply the CANCEL to
         @type calendar:
-        
+
         @return: C{tuple} of:
             C{bool} : C{True} if processed, C{False} if scheduling message should be ignored
             C{bool} : C{True} if calendar object should be deleted, C{False} otherwise
             C{set}  : set of Recurrence-IDs for cancelled instances, or C{None} if all cancelled
         """
-        
+
         assert itip_message.propertyValue("METHOD") == "CANCEL", "iTIP message must have METHOD:CANCEL"
         assert itip_message.resourceUID() == calendar.resourceUID(), "UIDs must be the same to process iTIP message"
 
@@ -235,32 +235,32 @@
         for component in itip_message.subcomponents():
             if component.name() == "VTIMEZONE":
                 continue
-        
+
             # Extract RECURRENCE-ID value from component
             rid = component.getRecurrenceIDUTC()
             rids.add(rid)
-            
+
             # Get the one that matches in the calendar
             overridden = calendar.overriddenComponent(rid)
-            
+
             if overridden:
                 # We are cancelling an overridden component.
 
                 if autoprocessing:
                     # Exclude the cancelled instance
                     exdates.append(component.getRecurrenceIDUTC())
-                
+
                     # Remove the existing component.
                     calendar.removeComponent(overridden)
                 else:
                     # Existing component is cancelled.
                     overridden.replaceProperty(Property("STATUS", "CANCELLED"))
                     newseq = component.propertyValue("SEQUENCE")
-                    overridden.replacePropertyInAllComponents(Property("SEQUENCE", newseq))
+                    overridden.replaceProperty(Property("SEQUENCE", newseq))
 
             elif calendar_master:
                 # We are trying to CANCEL a non-overridden instance.
-                
+
                 if autoprocessing:
                     # Exclude the cancelled instance
                     exdates.append(component.getRecurrenceIDUTC())
@@ -284,26 +284,27 @@
             return True, True, None
         else:
             return True, False, rids
-    
+
+
     @staticmethod
     def processReply(itip_message, calendar):
         """
         Process a METHOD=REPLY.
-        
+
         TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
         TODO: We have no way to track SEQUENCE/DTSTAMP on a per-attendee basis to correctly serialize out-of-order
               replies.
-        
+
         @param itip_message: the iTIP message calendar object to process.
         @type itip_message:
         @param calendar: the calendar object to apply the REPLY to
         @type calendar:
-        
+
         @return: a C{tuple} of:
             C{True} if processed, C{False} if scheduling message should be ignored
             C{tuple} of change info
         """
-        
+
         assert itip_message.propertyValue("METHOD") == "REPLY", "iTIP message must have METHOD:REPLY"
         assert itip_message.resourceUID() == calendar.resourceUID(), "UIDs must be the same to process iTIP message"
 
@@ -326,7 +327,7 @@
         # Now do all overridden ones (sort by RECURRENCE-ID)
         sortedComponents = []
         for itip_component in itip_message.subcomponents():
-            
+
             # Make sure we have an appropriate component
             if itip_component.name() == "VTIMEZONE":
                 continue
@@ -334,9 +335,9 @@
             if rid is None:
                 continue
             sortedComponents.append((rid, itip_component,))
-            
-        sortedComponents.sort(key=lambda x:x[0])
-        
+
+        sortedComponents.sort(key=lambda x: x[0])
+
         for rid, itip_component in sortedComponents:
             # Find matching component in organizer's copy
             match_component = calendar.overriddenComponent(rid)
@@ -366,6 +367,7 @@
             log.error("ATTENDEE property in a REPLY must be the same in all components\n%s" % (str(itip_message),))
             return False, None
 
+
     @staticmethod
     def updateAttendeeData(from_component, to_component):
         """
@@ -377,7 +379,7 @@
         @param to_component:
         @type to_component:
         """
-        
+
         # Track what changed
         partstat_changed = False
         private_comment_changed = False
@@ -397,7 +399,7 @@
 
         attendee = attendees[0]
         partstat = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
-        
+
         # Now find matching ATTENDEE in to_component
         existing_attendee = to_component.getAttendeeProperty((attendee.value(),))
         if existing_attendee:
@@ -405,7 +407,7 @@
             existing_attendee.setParameter("PARTSTAT", partstat)
             existing_attendee.setParameter("SCHEDULE-STATUS", reqstatus)
             partstat_changed = (oldpartstat != partstat)
-            
+
             # Always delete RSVP on PARTSTAT change
             if partstat_changed:
                 try:
@@ -418,7 +420,7 @@
                 # Look for X-CALENDARSERVER-PRIVATE-COMMENT property in iTIP component (State 1 in spec)
                 attendee_comment = tuple(from_component.properties("X-CALENDARSERVER-PRIVATE-COMMENT"))
                 attendee_comment = attendee_comment[0] if len(attendee_comment) else None
-                
+
                 # Look for matching X-CALENDARSERVER-ATTENDEE-COMMENT property in existing data (State 2 in spec)
                 private_comments = tuple(to_component.properties("X-CALENDARSERVER-ATTENDEE-COMMENT"))
                 for comment in private_comments:
@@ -431,64 +433,65 @@
             else:
                 attendee_comment = None
                 private_comment = None
-                
+
             # Now do update logic
             if attendee_comment is None and private_comment is None:
                 # Nothing to do
                 pass
- 
+
             elif attendee_comment is None and private_comment is not None:
                 # Remove all property parameters
                 private_comment.removeAllParameters()
-                
+
                 # Add default parameters
                 private_comment.setParameter("X-CALENDARSERVER-ATTENDEE-REF", attendee.value())
                 private_comment.setParameter("X-CALENDARSERVER-DTSTAMP", PyCalendarDateTime.getNowUTC().getText())
-                
+
                 # Set value empty
                 private_comment.setValue("")
-                
+
                 private_comment_changed = True
-                
+
             elif attendee_comment is not None and private_comment is None:
-                
+
                 # Add new property
                 private_comment = Property(
                     "X-CALENDARSERVER-ATTENDEE-COMMENT",
                     attendee_comment.value(),
-                    params = {
+                    params={
                         "X-CALENDARSERVER-ATTENDEE-REF": attendee.value(),
-                        "X-CALENDARSERVER-DTSTAMP":      PyCalendarDateTime.getNowUTC().getText(),
+                        "X-CALENDARSERVER-DTSTAMP": PyCalendarDateTime.getNowUTC().getText(),
                     }
                 )
                 to_component.addProperty(private_comment)
-                
+
                 private_comment_changed = True
-            
+
             else:
                 # Only change if different
                 if private_comment.value() != attendee_comment.value():
                     # Remove all property parameters
                     private_comment.removeAllParameters()
-                    
+
                     # Add default parameters
                     private_comment.setParameter("X-CALENDARSERVER-ATTENDEE-REF", attendee.value())
                     private_comment.setParameter("X-CALENDARSERVER-DTSTAMP", PyCalendarDateTime.getNowUTC().getText())
-                    
+
                     # Set new value
                     private_comment.setValue(attendee_comment.value())
-    
+
                     private_comment_changed = True
 
         return attendee.value(), partstat_changed, private_comment_changed
 
+
     @staticmethod
     def transferItems(from_calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, 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
         master component).
-        
+
         @return: C{True} if an EXDATE match occurred requiring the incoming component to be removed.
         """
 
@@ -505,7 +508,7 @@
 
             organizer = matched.getProperty("ORGANIZER")
             organizer_schedule_status = organizer.parameterValue("SCHEDULE-STATUS", None) if organizer else None
-            if organizer_schedule_status: 
+            if organizer_schedule_status:
                 organizer = to_component.getProperty("ORGANIZER")
                 if organizer:
                     organizer.setParameter("SCHEDULE-STATUS", organizer_schedule_status)
@@ -513,7 +516,7 @@
             # Remove the old one
             if remove_matched:
                 from_calendar.removeComponent(matched)
-                
+
             # Check for incoming DECLINED
             attendee = to_component.getAttendeeProperty((recipient,))
             if attendee and attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED":
@@ -526,20 +529,21 @@
             attendee = to_component.getAttendeeProperty((recipient,))
             if attendee and attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED":
                 return True
-                    
+
             # It is a new override - copy any valarms on the existing master component
             # into the new one.
             [to_component.addComponent(alarm) for alarm in master_valarms]
             [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: 
+            if organizer_schedule_status:
                 organizer = to_component.getProperty("ORGANIZER")
                 if organizer:
                     organizer.setParameter("SCHEDULE-STATUS", organizer_schedule_status)
-                
+
         return False
-    
+
+
     @staticmethod
     def addTranspForNeedsAction(components, recipient):
         # For each component where the ATTENDEE property of the recipient has PARTSTAT
@@ -551,20 +555,21 @@
             if attendee and attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "NEEDS-ACTION":
                 component.replaceProperty(Property("TRANSP", "TRANSPARENT"))
 
+
     @staticmethod
     def sequenceComparison(itip, calendar):
         """
         Do appropriate itip message sequencing based by comparison with existing calendar data.
-        
+
         @return: C{True} if the itip message is new and should be processed, C{False}
             if no processing is needed
         @rtype: C{bool}
         """
-        
+
         # Master component comparison trumps all else
         itip_master = itip.masterComponent()
         cal_master = calendar.masterComponent()
-        
+
         # If master component exists, compare all in iTIP and update if any are new
         if cal_master:
             for itip_component in itip.subcomponents():
@@ -573,7 +578,7 @@
                 cal_component = calendar.overriddenComponent(itip_component.getRecurrenceIDUTC())
                 if cal_component is None:
                     cal_component = cal_master
-                    
+
                 # TODO: No DTSTAMP comparison because we do not track DTSTAMPs
                 # Treat components the same as meaning so an update - in theory no harm in doing that
                 if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0:
@@ -582,7 +587,7 @@
             return False
 
         elif itip_master:
-            
+
             # Do comparison of each appropriate component if any one is new, process the itip
             for cal_component in calendar.subcomponents():
                 if cal_component.name() in ignoredComponents:
@@ -595,14 +600,14 @@
                 # Treat components the same as meaning so an update - in theory no harm in doing that
                 if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0:
                     return True
-            
+
             return False
-    
+
         else:
             # Do comparison of each matching component if any one is new, process the entire itip.
             # There is a race condition here, similar to REPLY, where we could reinstate an instance
             # that has been removed. Not much we can do about it without additional tracking.
-            
+
             cal_rids = set()
             for cal_component in calendar.subcomponents():
                 if cal_component.name() in ignoredComponents:
@@ -613,7 +618,7 @@
                 if itip_component.name() in ignoredComponents:
                     continue
                 itip_rids.add(itip_component.getRecurrenceIDUTC())
-            
+
             # Compare ones that match
             for rid in cal_rids & itip_rids:
                 cal_component = calendar.overriddenComponent(rid)
@@ -623,24 +628,26 @@
                 # Treat components the same as meaning so an update - in theory no harm in doing that
                 if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0:
                     return True
-            
+
             # If there are others in one set and not the other - always process, else no process
             return len(cal_rids ^ itip_rids) > 0
-            
+
+
+
 class iTipGenerator(object):
     """
     This assumes that DTSTAMP and SEQUENCE are already at their new values in the original calendar
     data passed in to each generateXXX() call.
     """
-    
+
     @staticmethod
     def generateCancel(original, attendees, instances=None, full_cancel=False):
         """
         This assumes that SEQUENCE is not already at its new value in the original calendar data. This
         is because the component passed in is the one that originally contained the attendee that is
-        being removed. 
+        being removed.
         """
-        
+
         itip = Component("VCALENDAR")
         itip.addProperty(Property("VERSION", "2.0"))
         itip.addProperty(Property("PRODID", iCalendarProductID))
@@ -652,7 +659,7 @@
         tzids = set()
         added = False
         for instance_rid in instances:
-            
+
             # Create a new component matching the type of the original
             comp = Component(original.mainType())
 
@@ -664,7 +671,7 @@
                 instance = original.overriddenComponent(instance_rid)
                 if instance is None:
                     instance = original.deriveInstance(instance_rid)
-                    
+
                 # If the instance to be cancelled did not exist in the original, then
                 # do nothing
                 if instance is None:
@@ -679,11 +686,11 @@
             comp.addProperty(instance.getOrganizerProperty())
             if instance_rid:
                 comp.addProperty(Property("RECURRENCE-ID", instance_rid.duplicate().adjustToUTC()))
-            
+
             def addProperties(propname):
                 for icalproperty in instance.properties(propname):
                     comp.addProperty(icalproperty)
-                    
+
             addProperties("SUMMARY")
             addProperties("DTSTART")
             addProperties("DTEND")
@@ -706,7 +713,7 @@
 
             itip.addComponent(comp)
             added = True
-        
+
         if added:
             # Now include any referenced tzids
             for comp in original.subcomponents():
@@ -714,37 +721,39 @@
                     tzid = comp.propertyValue("TZID")
                     if tzid in tzids:
                         itip.addComponent(comp)
-    
+
             # Strip out unwanted bits
             iTipGenerator.prepareSchedulingMessage(itip)
-    
+
             return itip
         else:
             return None
 
+
     @staticmethod
     def generateAttendeeRequest(original, attendees, filter_rids):
         """
         This assumes that SEQUENCE is already at its new value in the original calendar data.
         """
-        
+
         # Start with a copy of the original as we may have to modify bits of it
         itip = original.duplicate()
         itip.replaceProperty(Property("PRODID", iCalendarProductID))
         itip.addProperty(Property("METHOD", "REQUEST"))
-        
+
         # Now filter out components that do not contain every attendee
         itip.attendeesView(attendees, onlyScheduleAgentServer=True)
-        
+
         # Now filter out components except the ones specified
         if itip.filterComponents(filter_rids):
             # Strip out unwanted bits
             iTipGenerator.prepareSchedulingMessage(itip)
             return itip
-        
+
         else:
             return None
 
+
     @staticmethod
     def generateAttendeeReply(original, attendee, changedRids=None, force_decline=False):
 
@@ -792,22 +801,23 @@
             "LOCATION",
             "DESCRIPTION",
         ))
-        
+
         # Now set each ATTENDEE's PARTSTAT to DECLINED
         if force_decline:
             attendeeProps = itip.getAttendeeProperties((attendee,))
             assert attendeeProps, "Must have some matching ATTENDEEs"
             for attendeeProp in attendeeProps:
                 attendeeProp.setParameter("PARTSTAT", "DECLINED")
-        
+
         # Add REQUEST-STATUS to each top-level component
-        itip.addPropertyToAllComponents(Property("REQUEST-STATUS", ["2.0", "Success",]))
+        itip.addPropertyToAllComponents(Property("REQUEST-STATUS", ["2.0", "Success", ]))
 
         # Strip out unwanted bits
         iTipGenerator.prepareSchedulingMessage(itip, reply=True)
 
         return itip
 
+
     @staticmethod
     def prepareSchedulingMessage(itip, reply=False):
         """
@@ -831,40 +841,42 @@
             # Attendee properties that need to go to the Organizer
             keep_properties = ("X-CALENDARSERVER-PRIVATE-COMMENT",)
         itip.removeXProperties(keep_properties=keep_properties)
-        
+
         # Property Parameters
         itip.removePropertyParameters("ATTENDEE", ("SCHEDULE-AGENT", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
         itip.removePropertyParameters("ORGANIZER", ("SCHEDULE-AGENT", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
 
+
+
 class iTIPRequestStatus(object):
     """
     String constants for various iTIP status codes we use.
     """
-    
-    MESSAGE_PENDING_CODE        = "1.0"
-    MESSAGE_SENT_CODE           = "1.1"
-    MESSAGE_DELIVERED_CODE      = "1.2"
 
-    SUCCESS_CODE                = "2.0"
+    MESSAGE_PENDING_CODE = "1.0"
+    MESSAGE_SENT_CODE = "1.1"
+    MESSAGE_DELIVERED_CODE = "1.2"
 
-    INVALID_CALENDAR_USER_CODE  = "3.7"
-    NO_AUTHORITY_CODE           = "3.8"
+    SUCCESS_CODE = "2.0"
 
-    BAD_REQUEST_CODE            = "5.0"
-    SERVICE_UNAVAILABLE_CODE    = "5.1"
-    INVALID_SERVICE_CODE        = "5.2"
-    NO_USER_SUPPORT_CODE        = "5.3"
+    INVALID_CALENDAR_USER_CODE = "3.7"
+    NO_AUTHORITY_CODE = "3.8"
 
-    MESSAGE_PENDING         = MESSAGE_PENDING_CODE + ";Scheduling message send is pending"
-    MESSAGE_SENT            = MESSAGE_SENT_CODE + ";Scheduling message has been sent"
-    MESSAGE_DELIVERED       = MESSAGE_DELIVERED_CODE + ";Scheduling message has been delivered"
-    
-    SUCCESS                 = SUCCESS_CODE + ";Success"
+    BAD_REQUEST_CODE = "5.0"
+    SERVICE_UNAVAILABLE_CODE = "5.1"
+    INVALID_SERVICE_CODE = "5.2"
+    NO_USER_SUPPORT_CODE = "5.3"
 
-    INVALID_CALENDAR_USER   = INVALID_CALENDAR_USER_CODE + ";Invalid Calendar User"
-    NO_AUTHORITY            = NO_AUTHORITY_CODE + ";No authority"
+    MESSAGE_PENDING = MESSAGE_PENDING_CODE + ";Scheduling message send is pending"
+    MESSAGE_SENT = MESSAGE_SENT_CODE + ";Scheduling message has been sent"
+    MESSAGE_DELIVERED = MESSAGE_DELIVERED_CODE + ";Scheduling message has been delivered"
 
-    BAD_REQUEST             = BAD_REQUEST_CODE + ";Service cannot handle request"
-    SERVICE_UNAVAILABLE     = SERVICE_UNAVAILABLE_CODE + ";Service unavailable"
-    INVALID_SERVICE         = INVALID_SERVICE_CODE + ";Invalid calendar service"
-    NO_USER_SUPPORT         = NO_USER_SUPPORT_CODE + ";No scheduling support for user"
+    SUCCESS = SUCCESS_CODE + ";Success"
+
+    INVALID_CALENDAR_USER = INVALID_CALENDAR_USER_CODE + ";Invalid Calendar User"
+    NO_AUTHORITY = NO_AUTHORITY_CODE + ";No authority"
+
+    BAD_REQUEST = BAD_REQUEST_CODE + ";Service cannot handle request"
+    SERVICE_UNAVAILABLE = SERVICE_UNAVAILABLE_CODE + ";Service unavailable"
+    INVALID_SERVICE = INVALID_SERVICE_CODE + ";Invalid calendar service"
+    NO_USER_SUPPORT = NO_USER_SUPPORT_CODE + ";No scheduling support for user"

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/processing.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/processing.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -57,15 +57,18 @@
 log = Logger()
 
 class ImplicitProcessorException(Exception):
-    
+
     def __init__(self, msg):
         self.msg = msg
 
+
+
 class ImplicitProcessor(object):
-    
+
     def __init__(self):
         pass
 
+
     @inlineCallbacks
     def doImplicitProcessing(self, request, message, originator, recipient):
         """
@@ -78,7 +81,7 @@
         @type originator:
         @param recipient:
         @type recipient:
-        
+
         @return: a C{tuple} of (C{bool}, C{bool}) indicating whether the message was processed, and if it was whether
             auto-processing has taken place.
         """
@@ -87,10 +90,10 @@
         self.message = message
         self.originator = originator
         self.recipient = recipient
-        
+
         # TODO: for now going to assume that the originator is local - i.e. the scheduling message sent
         # represents the actual organizer's view.
-        
+
         # First see whether this is the organizer or attendee sending the message
         self.extractCalendarData()
 
@@ -126,24 +129,28 @@
 
         returnValue(result)
 
+
     def extractCalendarData(self):
-        
+
         # Some other useful things
         self.method = self.message.propertyValue("METHOD")
         self.uid = self.message.resourceUID()
-    
+
+
     def isOrganizerReceivingMessage(self):
         return self.method in ("REPLY", "REFRESH")
 
+
     def isAttendeeReceivingMessage(self):
         return self.method in ("REQUEST", "ADD", "CANCEL")
 
+
     @inlineCallbacks
     def getRecipientsCopy(self):
         """
         Get the Recipient's copy of the event being processed.
         """
-        
+
         self.recipient_calendar = None
         self.recipient_calendar_collection = None
         self.recipient_calendar_collection_uri = None
@@ -154,7 +161,8 @@
             self.recipient_calendar_collection = calendar_collection
             self.recipient_calendar_collection_uri = calendar_collection_uri
             self.recipient_calendar_name = resource_name
-    
+
+
     @inlineCallbacks
     def doImplicitOrganizer(self):
 
@@ -169,30 +177,31 @@
             result = (yield self.doImplicitOrganizerUpdate())
         elif self.method == "REFRESH":
             # With implicit we ignore refreshes.
-            # TODO: for iMIP etc we do need to handle them 
+            # TODO: for iMIP etc we do need to handle them
             result = (True, True, False, None,)
 
         returnValue(result)
 
+
     @inlineCallbacks
     def doImplicitOrganizerUpdate(self):
-        
+
         # Check to see if this is a valid reply
         result, processed = iTipProcessing.processReply(self.message, self.recipient_calendar)
         if result:
- 
+
             # Let the store know that no time-range info has changed
             self.recipient_calendar.noInstanceIndexing = True
 
             # Update the organizer's copy of the event
             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))
-            
+
             # Build the schedule-changes XML element
             attendeeReplying, rids = processed
             partstatChanged = False
             reply_details = (customxml.Attendee.fromString(attendeeReplying),)
-            
+
             for rid, partstatChanged, privateCommentChanged in sorted(rids):
                 recurrence = []
                 if rid == "":
@@ -201,7 +210,7 @@
                     recurrence.append(customxml.RecurrenceID.fromString(rid))
                 changes = []
                 if partstatChanged:
-                    changes.append(customxml.ChangedProperty(customxml.ChangedParameter(name="PARTSTAT"), name="ATTENDEE" ))
+                    changes.append(customxml.ChangedProperty(customxml.ChangedParameter(name="PARTSTAT"), name="ATTENDEE"))
                     partstatChanged = True
                 if privateCommentChanged:
                     changes.append(customxml.ChangedProperty(name="X-CALENDARSERVER-PRIVATE-COMMENT"))
@@ -229,15 +238,16 @@
 
         returnValue(result)
 
+
     @inlineCallbacks
     def queueAttendeeUpdate(self, exclude_attendees):
         """
         Queue up an update to attendees and use a memcache lock to ensure we don't update too frequently.
-        
+
         @param exclude_attendees: list of attendees who should not be refreshed (e.g., the one that triggeed the refresh)
         @type exclude_attendees: C{list}
         """
-        
+
         # When doing auto-processing of replies, only refresh attendees when the last auto-accept is done.
         # Note that when we do this we also need to refresh the attendee that is generating the reply because they
         # are no longer up to date with changes of other auto-accept attendees.
@@ -251,7 +261,7 @@
 
         # Check for batched refreshes
         if config.Scheduling.Options.AttendeeRefreshBatch:
-            
+
             # Need to lock whilst manipulating the batch list
             lock = MemcacheLock(
                 "BatchRefreshUIDLock",
@@ -264,14 +274,14 @@
             except MemcacheLockTimeoutError:
                 # If we could not lock then just fail the refresh - not sure what else to do
                 returnValue(None)
-            
+
             try:
                 # Get all attendees to refresh
                 allAttendees = sorted(list(self.recipient_calendar.getAllUniqueAttendees()))
-    
+
                 # Always need to refresh every attendee
                 exclude_attendees = ()
-                
+
                 # 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)
@@ -285,16 +295,17 @@
                     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)
             finally:
                 yield lock.clean()
-        
+
         else:
             yield self._doRefresh(self.organizer_calendar_resource, exclude_attendees)
 
+
     @inlineCallbacks
     def _doRefresh(self, organizer_resource, exclude_attendees=(), only_attendees=None):
         """
@@ -304,7 +315,7 @@
         @type organizer_resource: L{DAVResource}
         @param exclude_attendees: list of attendees to not refresh
         @type exclude_attendees: C{tuple}
-        @param only_attendees: list of attendees to refresh (C{None} - refresh all) 
+        @param only_attendees: list of attendees to refresh (C{None} - refresh all)
         @type only_attendees: C{tuple}
         """
         log.debug("ImplicitProcessing - refreshing UID: '%s', Attendees: %s" % (self.uid, ", ".join(only_attendees) if only_attendees else "all"))
@@ -316,7 +327,8 @@
             exclude_attendees,
             only_attendees=only_attendees,
         )
-        
+
+
     @inlineCallbacks
     def _doDelayedRefresh(self, attendeesToProcess):
         """
@@ -366,6 +378,7 @@
         finally:
             yield uidlock.clean()
 
+
     @inlineCallbacks
     def _doBatchRefresh(self):
         """
@@ -391,7 +404,7 @@
             cache = Memcacher("BatchRefreshAttendees", pickle=True)
             pendingAttendees = yield cache.get(self.uid)
             if pendingAttendees:
-                
+
                 # Get the next batch of attendees to process and update the cache value or remove it if
                 # no more processing is needed
                 attendeesToProcess = pendingAttendees[:config.Scheduling.Options.AttendeeRefreshBatch]
@@ -400,13 +413,13 @@
                     yield cache.set(self.uid, pendingAttendees)
                 else:
                     yield cache.delete(self.uid)
-                    
+
                 # Make sure we release this here to avoid potential deadlock when grabbing the ImplicitUIDLock in the next call
                 yield lock.release()
-                
+
                 # Now do the batch refresh
                 yield self._doDelayedRefresh(attendeesToProcess)
-                
+
                 # Queue the next refresh if needed
                 if pendingAttendees:
                     reactor.callLater(config.Scheduling.Options.AttendeeRefreshBatchIntervalSeconds, self._doBatchRefresh)
@@ -415,7 +428,8 @@
                 yield lock.release()
         finally:
             yield lock.clean()
-            
+
+
     @inlineCallbacks
     def doImplicitAttendee(self):
 
@@ -428,12 +442,13 @@
             result = (True, True, False, None)
         else:
             result = (yield self.doImplicitAttendeeUpdate())
-        
+
         returnValue(result)
 
+
     @inlineCallbacks
     def doImplicitAttendeeUpdate(self):
-        
+
         # Do security check: ORGANZIER in iTIP MUST match existing resource value
         if self.recipient_calendar:
             existing_organizer = self.recipient_calendar.getOrganizer()
@@ -455,9 +470,10 @@
         else:
             # NB We should never get here as we will have rejected unsupported METHODs earlier.
             result = (True, True, False, None,)
-            
+
         returnValue(result)
 
+
     @inlineCallbacks
     def doImplicitAttendeeRequest(self):
         """
@@ -466,14 +482,14 @@
 
         # If there is no existing copy, then look for default calendar and copy it here
         if self.new_resource:
-            
+
             # Check if the incoming data has the recipient declined in all instances. In that case we will not create
             # a new resource as chances are the recipient previously deleted the resource and we want to keep it deleted.
             attendees = self.message.getAttendeeProperties((self.recipient.cuaddr,))
             if all([attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED" for attendee in attendees]):
                 log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - ignoring all declined" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                 returnValue((True, False, False, None,))
-            
+
             # Check for default calendar
             default = (yield self.recipient.inbox.defaultCalendar(self.request, self.message.mainType()))
             if default is None:
@@ -482,11 +498,11 @@
 
             log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - new processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
             new_calendar = iTipProcessing.processNewRequest(self.message, self.recipient.cuaddr, creating=True)
-            
+
             # Handle auto-reply behavior
             if self.recipient.principal.canAutoSchedule():
                 send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar, self.recipient.principal.getAutoScheduleMode()))
-                
+
                 # Only store inbox item when reply is not sent or always for users
                 store_inbox = store_inbox or self.recipient.principal.getCUType() == "INDIVIDUAL"
             else:
@@ -494,7 +510,7 @@
                 store_inbox = True
 
             new_resource = (yield self.writeCalendarResource(default.url(), default, None, new_calendar))
-            
+
             if send_reply:
                 # Track outstanding auto-reply processing
                 if not hasattr(self.request, "auto_reply_processing_count"):
@@ -515,11 +531,11 @@
             # Processing update to existing event
             new_calendar, rids = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr)
             if new_calendar:
-     
+
                 # Handle auto-reply behavior
                 if self.recipient.principal.canAutoSchedule():
                     send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar, self.recipient.principal.getAutoScheduleMode()))
-                    
+
                     # Only store inbox item when reply is not sent or always for users
                     store_inbox = store_inbox or self.recipient.principal.getCUType() == "INDIVIDUAL"
                 else:
@@ -529,11 +545,11 @@
                 # Let the store know that no time-range info has changed for a refresh
                 if hasattr(self.request, "doing_attendee_refresh"):
                     new_calendar.noInstanceIndexing = True
-    
+
                 # Update the attendee's copy of the event
                 log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                 new_resource = (yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, new_calendar))
-                
+
                 if send_reply:
                     # Track outstanding auto-reply processing
                     if not hasattr(self.request, "auto_reply_processing_count"):
@@ -544,16 +560,16 @@
 
                 # Build the schedule-changes XML element
                 update_details = []
-                for rid, props_changed in sorted(rids.iteritems(), key=lambda x:x[0]):
+                for rid, props_changed in sorted(rids.iteritems(), key=lambda x: x[0]):
                     recurrence = []
                     if rid == "":
                         recurrence.append(customxml.Master())
                     else:
                         recurrence.append(customxml.RecurrenceID.fromString(rid))
                     changes = []
-                    for propName, paramNames in sorted(props_changed.iteritems(), key=lambda x:x[0]):
+                    for propName, paramNames in sorted(props_changed.iteritems(), key=lambda x: x[0]):
                         params = tuple([customxml.ChangedParameter(name=param) for param in paramNames])
-                        changes.append(customxml.ChangedProperty(*params, **{"name":propName}))
+                        changes.append(customxml.ChangedProperty(*params, **{"name": propName}))
                     recurrence.append(customxml.Changes(*changes))
                     update_details += (customxml.Recurrence(*recurrence),)
 
@@ -563,13 +579,13 @@
                         customxml.Update(*update_details),
                     ),
                 )
-                
+
                 # Refresh from another Attendee should not have Inbox item
                 if hasattr(self.request, "doing_attendee_refresh"):
                     store_inbox = False
 
                 result = (True, send_reply, store_inbox, changes,)
-                
+
             else:
                 # Request needs to be ignored
                 log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
@@ -597,7 +613,7 @@
             processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar, autoprocessing=autoprocessed)
             if processed_message:
                 if delete_original:
-                    
+
                     # Delete the attendee's copy of the event
                     log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - deleting entire event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                     yield self.deleteCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name)
@@ -610,9 +626,9 @@
                         ),
                     )
                     result = (True, autoprocessed, store_inbox, changes,)
-                    
+
                 else:
-         
+
                     # Update the attendee's copy of the event
                     log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                     yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar)
@@ -635,6 +651,7 @@
 
         returnValue(result)
 
+
     @inlineCallbacks
     def sendAttendeeAutoReply(self, calendar, resource, partstat):
         """
@@ -646,7 +663,7 @@
 
         @return: L{Component} for the new calendar data to write
         """
-        
+
         # We need to get the UID lock for implicit processing whilst we send the auto-reply
         # as the Organizer processing will attempt to write out data to other attendees to
         # refresh them. To prevent a race we need a lock.
@@ -695,6 +712,7 @@
             if hasattr(self.request, "auto_reply_processing_count"):
                 self.request.auto_reply_processing_count -= 1
 
+
     @inlineCallbacks
     def checkAttendeeAutoReply(self, calendar, automode):
         """
@@ -702,7 +720,7 @@
         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).
-        
+
         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.
 
@@ -714,7 +732,7 @@
         @return: C{tuple} of C{bool}, C{bool}, C{str} indicating whether changes were made, whether the inbox item
             should be added, and the new PARTSTAT.
         """
-        
+
         # First ignore the none mode
         if automode == "none":
             returnValue((False, True, "",))
@@ -727,26 +745,26 @@
         if automode in ("accept-always", "decline-always",):
             all_accepted = automode == "accept-always"
             all_declined = automode == "decline-always"
-        
+
         # 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)
+            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()])
-            
+
             # Extract UID from primary component as we want to ignore this one if we match it
             # in any calendars.
             comp = calendar.mainComponent(allow_multiple=True)
             uid = comp.propertyValue("UID")
-        
+
             # Now compare each instance time-range with the index and see if there is an overlap
             calendars = (yield self._getCalendarsToMatch())
-        
+
             for calURL in calendars:
                 testcal = (yield self.request.locateResource(calURL))
-    
+
                 # 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))
@@ -755,14 +773,14 @@
                     tzinfo = tz.calendar().gettimezone()
                 else:
                     tzinfo = PyCalendarTimezone(utc=True)
-    
+
                 # 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 = ([], [], [])
-                            
+
                             def makeTimedUTC(dt):
                                 dt = dt.duplicate()
                                 if dt.isDateOnly():
@@ -772,28 +790,28 @@
                                     dt.setTimezone(tzinfo)
                                     dt.adjustToUTC()
                                 return dt
-                            
+
                             tr = caldavxml.TimeRange(
                                 start=str(makeTimedUTC(instance.start)),
                                 end=str(makeTimedUTC(instance.end)),
                             )
-    
+
                             yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid, servertoserver=True)
-                            
+
                             # 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.")
-                
+
                 # If everything is declined we can exit now
                 if not any(instance_states.itervalues()):
                     break
-            
+
             # TODO: here we should do per-instance ACCEPT/DECLINE behavior
             # For now we will assume overall ACCEPT/DECLINE
-    
+
             # Collect all the accepted and declined states
             all_accepted = all(instance_states.itervalues())
             all_declined = not any(instance_states.itervalues())
@@ -805,7 +823,7 @@
             attendeeProps = calendar.getAttendeeProperties(cuas)
             if not attendeeProps:
                 returnValue((False, True, "",))
-        
+
             if automode == "accept-always":
                 freePartstat = busyPartstat = "ACCEPTED"
             elif automode == "decline-always":
@@ -817,26 +835,26 @@
 
             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)
             store_inbox = partstat == "NEEDS-ACTION"
-        
+
         else:
             # Hard case: some accepted some declined
             # What we will do is mark any master instance as accepted, then mark each existing
             # overridden instance as accepted or declined, and generate new overridden instances for
             # any other declines.
-            
+
             made_changes = False
             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
+            defaultStateFree = len(filter(lambda x: x, instance_states.values())) >= len(instance_states.keys()) / 2
 
             # See if there is a master component first
             hadMasterRsvp = False
@@ -853,12 +871,12 @@
 
             # Look at expanded instances and change partstat accordingly
             for instance, free in sorted(instance_states.iteritems(), key=lambda x: x[0].rid):
-                
+
                 overridden = calendar.overriddenComponent(instance.rid)
                 if not overridden and free == defaultStateFree:
                     # Nothing to do as state matches the master
-                    continue 
-                
+                    continue
+
                 if overridden:
                     # Change ATTENDEE property to match new state
                     attendee = overridden.getAttendeeProperty(cuas)
@@ -870,7 +888,7 @@
                         overridden.replaceProperty(Property("TRANSP", "OPAQUE" if free and freeStateOpaque else "TRANSPARENT"))
                 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. 
+                    # value that may have been overwritten by any change to the master itself.
                     derived = calendar.deriveInstance(instance.rid)
                     if derived:
                         attendee = derived.getAttendeeProperty(cuas)
@@ -882,56 +900,58 @@
                             derived.replaceProperty(Property("TRANSP", "OPAQUE" if free and freeStateOpaque else "TRANSPARENT"))
                             calendar.addComponent(derived)
                             made_changes = True
-            
+
         # Fake a SCHEDULE-STATUS on the ORGANIZER property
         if made_changes:
             calendar.setParameterToValueForPropertyWithValue("SCHEDULE-STATUS", iTIPRequestStatus.MESSAGE_DELIVERED_CODE, "ORGANIZER", None)
-        
+
         returnValue((made_changes, store_inbox, partstat,))
 
+
     def _getCalendarsToMatch(self):
         # Determine the set of calendar URIs for a principal need to be searched.
-        
+
         # Find the current recipients calendar-free-busy-set
         return self.recipient.principal.calendarFreeBusyURIs(self.request)
 
+
     @inlineCallbacks
     def writeCalendarResource(self, collURL, collection, name, calendar):
         """
         Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
         resource or by creating a new one.
-        
+
         @param collURL: the C{str} containing the URL of the calendar collection.
         @param collection: the L{CalDAVResource} for the calendar collection to store the resource in.
         @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
         @param calendar: the L{Component} calendar to write.
         @return: L{Deferred} -> L{CalDAVResource}
         """
-        
+
         # Create a new name if one was not provided
         if name is None:
-            name =  "%s-%s.ics" % (hashlib.md5(calendar.resourceUID()).hexdigest(), str(uuid.uuid4())[:8],)
-    
+            name = "%s-%s.ics" % (hashlib.md5(calendar.resourceUID()).hexdigest(), str(uuid.uuid4())[:8],)
+
         # Get a resource for the new item
         newchildURL = joinURL(collURL, name)
         newchild = yield self.request.locateResource(newchildURL)
         newchild._url = newchildURL
-        
+
         # Now write it to the resource
         from twistedcaldav.method.put_common import StoreCalendarObjectResource
         yield StoreCalendarObjectResource(
                      request=self.request,
-                     destination = newchild,
-                     destination_uri = newchildURL,
-                     destinationparent = collection,
-                     destinationcal = True,
-                     calendar = calendar,
-                     isiTIP = False,
-                     allowImplicitSchedule = False,
-                     internal_request = True,
-                     processing_organizer = self.isOrganizerReceivingMessage(),
+                     destination=newchild,
+                     destination_uri=newchildURL,
+                     destinationparent=collection,
+                     destinationcal=True,
+                     calendar=calendar,
+                     isiTIP=False,
+                     allowImplicitSchedule=False,
+                     internal_request=True,
+                     processing_organizer=self.isOrganizerReceivingMessage(),
                  ).run()
-    
+
         returnValue(newchild)
 
 
@@ -939,7 +959,7 @@
     def deleteCalendarResource(self, collURL, collection, name):
         """
         Delete the calendar resource in the specified calendar.
-        
+
         @param collURL: the URL of the calendar collection.
         @type name: C{str}
         @param collection: the calendar collection to delete the resource from.
@@ -963,7 +983,7 @@
         @type partstat: C{str}
         @param hadRSVP: indicates whether RSVP should be added when changing to NEEDS-ACTION
         @type hadRSVP: C{bool}
-        
+
         @return: C{True} if any change was made, C{False} otherwise
         """
 
@@ -986,15 +1006,15 @@
             except KeyError:
                 pass
 
-        
         return madeChanges
 
+
     @inlineCallbacks
     def doImplicitAttendeeEventFix(self, ex):
 
         # Only certain types of exception should be handled - ones related to calendar data errors.
         # All others should result in the scheduling response coming back as a 5.x code
-        
+
         if type(ex) not in (InvalidOverriddenInstanceError, HTTPError):
             raise ImplicitProcessorException("5.1;Service unavailable")
 
@@ -1013,12 +1033,12 @@
 
         # Locate the attendee's copy of the event if it exists.
         recipient_resource, recipient_resource_name, recipient_collection, recipient_collection_uri = (yield getCalendarObjectForPrincipals(self.request, self.recipient.principal, self.uid))
-        
+
         # We only need to fix data that already exists
         if recipient_resource:
             if originator_calendar.mainType() != None:
                 yield self.writeCalendarResource(recipient_collection_uri, recipient_collection, recipient_resource_name, originator_calendar)
             else:
                 yield self.deleteCalendarResource(recipient_collection_uri, recipient_collection, recipient_resource_name)
-        
+
         returnValue(True)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/scheduler.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/scheduler.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -14,12 +14,6 @@
 # limitations under the License.
 ##
 
-import itertools
-import re
-import socket
-import urlparse
-
-from twisted.internet.abstract import isIPAddress
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python.failure import Failure
 
@@ -28,30 +22,26 @@
 from twext.web2.http import HTTPError, Response, StatusResponse
 from twext.web2.http_headers import MimeType
 from txdav.xml import element as davxml
-from twext.web2.dav.http import errorForFailure, messageForFailure, statusForFailure
-from twext.web2.dav.http import ErrorResponse
+from twext.web2.dav.http import messageForFailure, statusForFailure, \
+    ErrorResponse
 
 from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.accounting import accountingEnabled, emitAccounting
 from twistedcaldav.config import config
 from twistedcaldav.ical import Component
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 from twistedcaldav.scheduling import addressmapping
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
-from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
+from twistedcaldav.scheduling.caldav.delivery import ScheduleViaCalDAV
+from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser, \
     calendarUserFromPrincipal, OtherServerCalendarUser
 from twistedcaldav.scheduling.cuaddress import LocalCalendarUser
 from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
 from twistedcaldav.scheduling.cuaddress import EmailCalendarUser
 from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser
-from twistedcaldav.scheduling.imip import ScheduleViaIMip
-from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
+from twistedcaldav.scheduling.imip.delivery import ScheduleViaIMip
+from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.servers import Servers
-from twistedcaldav.util import normalizationLookup
 
 """
 CalDAV/Server-to-Server scheduling behavior.
@@ -88,7 +78,7 @@
           \_L{ImplicitProcessor}  - dispatches iTIP message (also auto-accept)
             \
              \_L{iTipProcessing}  - processes iTIP message
-             
+
 Here is a typical flow of activity for a iTIP between an organizer on the server and an iMIP attendee:
 
 iTIP PUT request
@@ -115,9 +105,7 @@
 
 __all__ = [
     "Scheduler",
-    "CalDAVScheduler",
-    "IScheduleScheduler",
-    "IMIPScheduler",
+    "RemoteScheduler",
     "DirectScheduler",
 ]
 
@@ -125,7 +113,25 @@
 log = Logger()
 
 class Scheduler(object):
-    
+
+    scheduleResponse = None
+
+    errorResponse = None # The class used for generating an HTTP XML error response
+
+    errorElements = {
+        "originator-missing": (),
+        "originator-invalid": (),
+        "originator-denied": (),
+        "recipient-missing": (),
+        "recipient-invalid": (),
+        "organizer-denied": (),
+        "attendee-denied": (),
+        "invalid-calendar-data-type": (),
+        "invalid-calendar-data": (),
+        "invalid-scheduling-message": (),
+        "max-recipients": (),
+    }
+
     def __init__(self, request, resource):
         self.request = request
         self.resource = resource
@@ -140,18 +146,19 @@
         self.fakeTheResult = False
         self.method = "Unknown"
         self.internal_request = False
-    
+
+
     @inlineCallbacks
     def doSchedulingViaPOST(self, transaction, use_request_headers=False):
         """
         The Scheduling POST operation on an Outbox.
         """
-    
+
         self.method = "POST"
 
         # Load various useful bits doing some basic checks on those
         yield self.loadCalendarFromRequest()
-        
+
         if use_request_headers:
             self.loadFromRequestHeaders()
         else:
@@ -161,7 +168,7 @@
             self.request.extendedLogItems = {}
         self.request.extendedLogItems["recipients"] = len(self.recipients)
         self.request.extendedLogItems["cl"] = str(len(str(self.calendar)))
-    
+
         # Do some extra authorization checks
         self.checkAuthorization()
 
@@ -184,15 +191,16 @@
                 # Release lock after commit or abort
                 transaction.postCommit(lock.clean)
                 transaction.postAbort(lock.clean)
-                
+
         result = (yield self.doScheduling())
         returnValue(result)
 
+
     def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
         """
         The implicit scheduling PUT operation.
         """
-    
+
         self.method = "PUT"
 
         # Load various useful bits doing some basic checks on those
@@ -206,23 +214,24 @@
 
         return self.doScheduling()
 
+
     @inlineCallbacks
     def doScheduling(self):
         # Check validity of Originator header.
         yield self.checkOriginator()
-    
+
         # Get recipient details.
         yield self.checkRecipients()
-    
+
         # Check calendar data.
         self.checkCalendarData()
-    
+
         # Check validity of ORGANIZER
         yield self.checkOrganizer()
-    
+
         # Do security checks (e.g. spoofing)
         yield self.securityChecks()
-    
+
         # Generate accounting information
         self.doAccounting()
 
@@ -234,11 +243,13 @@
 
         returnValue(result)
 
+
     @inlineCallbacks
     def loadFromRequestData(self):
         yield self.loadOriginatorFromRequestDetails()
         self.loadRecipientsFromCalendarData()
-        
+
+
     @inlineCallbacks
     def loadOriginatorFromRequestDetails(self):
         # Get the originator who is the authenticated user
@@ -255,14 +266,15 @@
 
         if not originator:
             log.err("%s request must have Originator" % (self.method,))
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(self.errorResponse(
                 responsecode.FORBIDDEN,
-                (caldav_namespace, "originator-specified"),
+                self.errorElements["originator-missing"],
                 "Missing originator",
             ))
         else:
             self.originator = originator
 
+
     def loadRecipientsFromCalendarData(self):
 
         # Get the ATTENDEEs
@@ -272,17 +284,18 @@
             if attendee not in unique_set:
                 attendees.append(attendee)
                 unique_set.add(attendee)
-        
+
         if not attendees:
             log.err("%s request must have at least one Recipient" % (self.method,))
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(self.errorResponse(
                 responsecode.FORBIDDEN,
-                (caldav_namespace, "recipient-specified"),
+                self.errorElements["recipient-missing"],
                 "Must have recipients",
             ))
         else:
             self.recipients = list(attendees)
 
+
     def loadFromRequestHeaders(self):
         """
         Load Originator and Recipient from request headers.
@@ -290,30 +303,32 @@
         self.loadOriginatorFromRequestHeaders()
         self.loadRecipientsFromRequestHeaders()
 
+
     def loadOriginatorFromRequestHeaders(self):
         # Must have Originator header
         originator = self.request.headers.getRawHeaders("originator")
         if originator is None or (len(originator) != 1):
             log.err("%s request must have Originator header" % (self.method,))
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(self.errorResponse(
                 responsecode.FORBIDDEN,
-                (caldav_namespace, "originator-specified"),
+                self.errorElements["originator-missing"],
                 "Missing originator",
             ))
         else:
             self.originator = originator[0]
-    
+
+
     def loadRecipientsFromRequestHeaders(self):
         # Get list of Recipient headers
         rawRecipients = self.request.headers.getRawHeaders("recipient")
         if rawRecipients is None or (len(rawRecipients) == 0):
             log.err("%s request must have at least one Recipient header" % (self.method,))
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(self.errorResponse(
                 responsecode.FORBIDDEN,
-                (caldav_namespace, "recipient-specified"),
+                self.errorElements["recipient-missing"],
                 "No recipients",
             ))
-    
+
         # Recipient header may be comma separated list
         self.recipients = []
         for rawRecipient in rawRecipients:
@@ -321,33 +336,35 @@
                 r = r.strip()
                 if len(r):
                     self.recipients.append(r)
-        
+
+
     @inlineCallbacks
     def loadCalendarFromRequest(self):
         # Must be content-type text/calendar
         contentType = self.request.headers.getHeader("content-type")
         if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
             log.err("MIME type %s not allowed in calendar collection" % (contentType,))
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(self.errorResponse(
                 responsecode.FORBIDDEN,
-                (caldav_namespace, "supported-calendar-data"),
+                self.errorElements["invalid-calendar-data-type"],
                 "Data is not calendar data",
             ))
-    
+
         # Parse the calendar object from the HTTP request stream
         try:
             self.calendar = (yield Component.fromIStream(self.request.stream))
-            
+
             self.preProcessCalendarData()
         except:
             # FIXME: Bare except
             log.err("Error while handling %s: %s" % (self.method, Failure(),))
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(self.errorResponse(
                 responsecode.FORBIDDEN,
-                (caldav_namespace, "valid-calendar-data"),
+                self.errorElements["invalid-calendar-data"],
                 description="Can't parse calendar data"
             ))
 
+
     def preProcessCalendarData(self):
         """
         After loading calendar data from the request, do some optional processing of it. This method will be
@@ -355,68 +372,75 @@
         """
         pass
 
+
     def checkAuthorization(self):
         raise NotImplementedError
 
+
     def checkOriginator(self):
         raise NotImplementedError
 
+
     def checkRecipients(self):
         raise NotImplementedError
 
+
     def checkOrganizer(self):
         raise NotImplementedError
 
+
     def checkOrganizerAsOriginator(self):
         raise NotImplementedError
 
+
     def checkAttendeeAsOriginator(self):
         raise NotImplementedError
 
+
     def checkCalendarData(self):
-        
+
         # Skip all the valid data checks for an internal request as we are going to assume all the internal
         # request data has been generated properly.
-    
+
         if not self.internal_request:
             # Must be a valid calendar
             try:
                 self.calendar.validCalendarData()
             except ValueError, e:
                 log.err("%s request calendar component is not valid:%s %s" % (self.method, e, self.calendar,))
-                raise HTTPError(ErrorResponse(
+                raise HTTPError(self.errorResponse(
                     responsecode.FORBIDDEN,
-                    (caldav_namespace, "valid-calendar-data"),
+                    self.errorElements["invalid-calendar-data"],
                     description="Calendar component is not valid"
                 ))
-        
+
             # Must have a METHOD
             if not self.calendar.isValidMethod():
                 log.err("%s request must have valid METHOD property in calendar component: %s" % (self.method, self.calendar,))
-                raise HTTPError(ErrorResponse(
+                raise HTTPError(self.errorResponse(
                     responsecode.FORBIDDEN,
-                    (caldav_namespace, "valid-calendar-data"),
+                    self.errorElements["invalid-scheduling-message"],
                     description="Must have valid METHOD property"
                 ))
-            
+
             # Verify iTIP behavior
             if not self.calendar.isValidITIP():
                 log.err("%s request must have a calendar component that satisfies iTIP requirements: %s" % (self.method, self.calendar,))
-                raise HTTPError(ErrorResponse(
+                raise HTTPError(self.errorResponse(
                     responsecode.FORBIDDEN,
-                    (caldav_namespace, "valid-calendar-data"),
+                    self.errorElements["invalid-scheduling-message"],
                     description="Must have a calendar component that satisfies iTIP requirements"
                 ))
-    
+
             # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
             if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
                 log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component %s request: %s" % (self.method, self.calendar,))
-                raise HTTPError(ErrorResponse(
+                raise HTTPError(self.errorResponse(
                     responsecode.FORBIDDEN,
                     (calendarserver_namespace, "no-access-restrictions"),
                     "Private events cannot be scheduled",
                 ))
-    
+
         # Determine iTIP method mode
         if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
             self.isiTIPRequest = True
@@ -426,13 +450,13 @@
 
             # Verify that there is a single ATTENDEE property
             attendees = self.calendar.getAttendees()
-        
+
             # Must have only one
             if len(attendees) != 1:
                 log.err("Wrong number of ATTENDEEs in calendar data: %s" % (str(self.calendar),))
-                raise HTTPError(ErrorResponse(
+                raise HTTPError(self.errorResponse(
                     responsecode.FORBIDDEN,
-                    (caldav_namespace, "attendee-allowed"),
+                    self.errorElements["invalid-scheduling-message"],
                     "Wrong number of attendees",
                 ))
             self.attendee = attendees[0]
@@ -440,12 +464,13 @@
         else:
             msg = "Unknown iTIP METHOD: %s" % (self.calendar.propertyValue("METHOD"),)
             log.err(msg)
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(self.errorResponse(
                 responsecode.FORBIDDEN,
-                (caldav_namespace, "valid-calendar-data"),
+                self.errorElements["invalid-scheduling-message"],
                 description=msg
             ))
 
+
     def checkForFreeBusy(self):
         if not hasattr(self, "isfreebusy"):
             if (self.calendar.propertyValue("METHOD") == "REQUEST") and (self.calendar.mainType() == "VFREEBUSY"):
@@ -453,48 +478,50 @@
                 vfreebusies = [v for v in self.calendar.subcomponents() if v.name() == "VFREEBUSY"]
                 if len(vfreebusies) != 1:
                     log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (self.calendar,))
-                    raise HTTPError(ErrorResponse(
+                    raise HTTPError(self.errorResponse(
                         responsecode.FORBIDDEN,
-                        (caldav_namespace, "valid-calendar-data"),
+                        self.errorElements["invalid-scheduling-message"],
                         "iTIP data is not valid for a VFREEBUSY request",
                     ))
                 dtstart = vfreebusies[0].getStartDateUTC()
                 dtend = vfreebusies[0].getEndDateUTC()
                 if dtstart is None or dtend is None:
                     log.err("VFREEBUSY start/end not valid: %s" % (self.calendar,))
-                    raise HTTPError(ErrorResponse(
+                    raise HTTPError(self.errorResponse(
                         responsecode.FORBIDDEN,
-                        (caldav_namespace, "valid-calendar-data"),
+                        self.errorElements["invalid-scheduling-message"],
                         "VFREEBUSY start/end not valid",
                     ))
 
                 # Some clients send floating instead of UTC - coerce to UTC
                 if not dtstart.utc() or not dtend.utc():
                     log.err("VFREEBUSY start or end not UTC: %s" % (self.calendar,))
-                    raise HTTPError(ErrorResponse(
+                    raise HTTPError(self.errorResponse(
                         responsecode.FORBIDDEN,
-                        (caldav_namespace, "valid-calendar-data"),
+                        self.errorElements["invalid-scheduling-message"],
                         "VFREEBUSY start or end not UTC",
                     ))
 
                 self.timeRange = caldavxml.TimeRange(start=dtstart.getText(), end=dtend.getText())
                 self.timeRange.start = dtstart
                 self.timeRange.end = dtend
-        
+
                 # Look for masked UID
                 self.excludeUID = self.calendar.getMaskUID()
-        
+
                 # Do free busy operation
                 self.isfreebusy = True
             else:
                 # Do regular invite (fan-out)
                 self.isfreebusy = False
-        
+
         return self.isfreebusy
-    
+
+
     def securityChecks(self):
         raise NotImplementedError
 
+
     def doAccounting(self):
         #
         # Accounting
@@ -519,12 +546,14 @@
                     )
                 )
 
+
     def finalChecks(self):
         """
         Final checks before doing the actual scheduling.
         """
         pass
 
+
     @inlineCallbacks
     def generateSchedulingResponse(self):
 
@@ -534,8 +563,8 @@
         freebusy = self.checkForFreeBusy()
 
         # Prepare for multiple responses
-        responses = ScheduleResponseQueue(self.method, responsecode.OK)
-    
+        responses = self.scheduleResponse(self.method, responsecode.OK)
+
         # Loop over each recipient and aggregate into lists by service types.
         caldav_recipients = []
         partitioned_recipients = []
@@ -543,20 +572,20 @@
         remote_recipients = []
         imip_recipients = []
         for ctr, recipient in enumerate(self.recipients):
-    
+
             # Check for freebusy limit
             if freebusy and config.Scheduling.Options.LimitFreeBusyAttendees and ctr >= config.Scheduling.Options.LimitFreeBusyAttendees:
-                err = HTTPError(ErrorResponse(
+                err = HTTPError(self.errorResponse(
                     responsecode.NOT_FOUND,
-                    (caldav_namespace, "recipient-limit"),
+                    self.errorElements["max-recipients"],
                     "Too many attendees",
                 ))
                 responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
                 continue
-                
+
             if self.fakeTheResult:
                 responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS if freebusy else iTIPRequestStatus.MESSAGE_DELIVERED)
-                
+
             elif isinstance(recipient, LocalCalendarUser):
                 caldav_recipients.append(recipient)
 
@@ -573,9 +602,9 @@
                 imip_recipients.append(recipient)
 
             else:
-                err = HTTPError(ErrorResponse(
+                err = HTTPError(self.errorResponse(
                     responsecode.NOT_FOUND,
-                    (caldav_namespace, "recipient-exists"),
+                    self.errorElements["recipient-invalid"],
                     "Unknown recipient",
                 ))
                 responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.INVALID_CALENDAR_USER)
@@ -605,7 +634,8 @@
 
         # Return with final response if we are done
         returnValue(responses)
-    
+
+
     def generateLocalSchedulingResponses(self, recipients, responses, freebusy):
         """
         Generate scheduling responses for CalDAV recipients.
@@ -615,6 +645,7 @@
         requestor = ScheduleViaCalDAV(self, recipients, responses, freebusy)
         return requestor.generateSchedulingResponses()
 
+
     def generateRemoteSchedulingResponses(self, recipients, responses, freebusy, refreshOnly=False):
         """
         Generate scheduling responses for remote recipients.
@@ -624,6 +655,7 @@
         requestor = ScheduleViaISchedule(self, recipients, responses, freebusy)
         return requestor.generateSchedulingResponses(refreshOnly)
 
+
     def generateIMIPSchedulingResponses(self, recipients, responses, freebusy):
         """
         Generate scheduling responses for iMIP recipients.
@@ -633,222 +665,8 @@
         requestor = ScheduleViaIMip(self, recipients, responses, freebusy)
         return requestor.generateSchedulingResponses()
 
-class CalDAVScheduler(Scheduler):
 
-    def __init__(self, request, resource):
-        super(CalDAVScheduler, self).__init__(request, resource)
-        self.doingPOST = False
 
-    def doSchedulingViaPOST(self, transaction):
-        """
-        The Scheduling POST operation on an Outbox.
-        """
-        self.doingPOST = True
-        return super(CalDAVScheduler, self).doSchedulingViaPOST(transaction)
-
-    def checkAuthorization(self):
-        # Must have an authenticated user
-        if not self.internal_request and self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
-            log.err("Unauthenticated originators not allowed: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "originator-allowed"),
-                "Invalid originator",
-            ))
-
-    def checkOriginator(self):
-        """
-        Check the validity of the Originator header. Extract the corresponding principal.
-        """
-    
-        # Verify that Originator is a valid calendar user
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        if originatorPrincipal is None:
-            # Local requests MUST have a principal.
-            log.err("Could not find principal for originator: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "originator-allowed"),
-                "No principal for originator",
-            ))
-        else:
-            # Must have a valid Inbox.
-            inboxURL = originatorPrincipal.scheduleInboxURL()
-            if inboxURL is None:
-                log.err("Could not find inbox for originator: %s" % (self.originator,))
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (caldav_namespace, "originator-allowed"),
-                    "Originator cannot be scheduled",
-                ))
-
-            self.originator = LocalCalendarUser(self.originator, originatorPrincipal)
-
-    @inlineCallbacks
-    def checkRecipients(self):
-        """
-        Check the validity of the Recipient header values. Map these into local or
-        remote CalendarUsers.
-        """
-        
-        results = []
-        for recipient in self.recipients:
-            # Get the principal resource for this recipient
-            principal = self.resource.principalForCalendarUserAddress(recipient)
-            
-            # If no principal we may have a remote recipient but we should check whether
-            # the address is one that ought to be on our server and treat that as a missing
-            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
-            if principal is None:
-                address = (yield addressmapping.mapper.getCalendarUser(recipient, principal))
-                if isinstance(address, InvalidCalendarUser):
-                    log.err("Unknown calendar user address: %s" % (recipient,))
-                results.append(address)
-            else:
-                # Map recipient to their inbox
-                inboxURL = principal.scheduleInboxURL()
-                inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
-
-                if inbox:
-                    results.append(calendarUserFromPrincipal(recipient, principal, inbox, inboxURL))
-                else:
-                    log.err("No schedule inbox for principal: %s" % (principal,))
-                    results.append(InvalidCalendarUser(recipient))
-        
-        self.recipients = results
-
-    @inlineCallbacks
-    def checkOrganizer(self):
-        """
-        Check the validity of the ORGANIZER value. ORGANIZER must be local.
-        """
-        
-        # Verify that the ORGANIZER's cu address maps to a valid user
-        organizer = self.calendar.getOrganizer()
-        if organizer:
-            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
-            if organizerPrincipal:
-                outboxURL = organizerPrincipal.scheduleOutboxURL()
-                if outboxURL:
-                    
-                    # Only do this check for a freebusy request. A check for an invite needs
-                    # to be handled later when we know whether a new invite is being added
-                    # (which we reject) vs an update to an existing one (which we allow).
-                    if self.checkForFreeBusy() and not organizerPrincipal.enabledAsOrganizer():
-                        log.err("ORGANIZER not allowed to be an Organizer: %s" % (self.calendar,))
-                        raise HTTPError(ErrorResponse(
-                            responsecode.FORBIDDEN,
-                            (caldav_namespace, "organizer-allowed"),
-                            "Organizer cannot schedule",
-                        ))
-
-                    self.organizer = LocalCalendarUser(organizer, organizerPrincipal)
-                else:
-                    log.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
-                    raise HTTPError(ErrorResponse(
-                        responsecode.FORBIDDEN,
-                        (caldav_namespace, "organizer-allowed"),
-                        "Organizer cannot schedule",
-                    ))
-            else:
-                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
-                if localUser:
-                    log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendar,))
-                    raise HTTPError(ErrorResponse(
-                        responsecode.FORBIDDEN,
-                        (caldav_namespace, "organizer-allowed"),
-                        "No principal for organizer",
-                    ))
-                else:
-                    self.organizer = RemoteCalendarUser(organizer) 
-        else:
-            log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "organizer-allowed"),
-                "Missing organizer",
-            ))
-
-    def checkOrganizerAsOriginator(self):
-
-        # Make sure that the ORGANIZER is local
-        if not isinstance(self.organizer, LocalCalendarUser):
-            log.err("ORGANIZER is not local to server in calendar data: %s" % (self.calendar,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "organizer-allowed"),
-                "Organizer is not local to server",
-            ))
-
-        # Make sure that the ORGANIZER's Outbox is the request URI
-        if self.doingPOST and self.organizer.principal.scheduleOutboxURL() != self.request.uri:
-            log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "organizer-allowed"),
-                "Outbox does not belong to organizer",
-            ))
-
-    def checkAttendeeAsOriginator(self):
-        """
-        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
-        Only local attendees are allowed for message originating from this server.
-        """
-        
-        # Attendee's Outbox MUST be the request URI
-        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
-        if attendeePrincipal:
-            if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != self.request.uri:
-                log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendar,))
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (caldav_namespace, "attendee-allowed"),
-                    "Outbox does not belong to attendee",
-                ))
-        else:
-            log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "attendee-allowed"),
-                "No principal for attendee",
-            ))
-    
-    def securityChecks(self):
-        """
-        Check that the originator has the appropriate rights to send this type of iTIP message.
-        """
-    
-        # Prevent spoofing of ORGANIZER with specific METHODs when local
-        if self.isiTIPRequest:
-            self.checkOrganizerAsOriginator()
-    
-        # Prevent spoofing when doing reply-like METHODs
-        else:
-            self.checkAttendeeAsOriginator()
-
-    def finalChecks(self):
-        """
-        Final checks before doing the actual scheduling.
-        """
-        
-        # With implicit scheduling only certain types of iTIP operations are allowed for POST.
-        
-        if self.doingPOST:
-            # Freebusy requests always processed
-            if self.checkForFreeBusy():
-                return
-            
-            # COUNTER and DECLINE-COUNTER allowed
-            if self.calendar.propertyValue("METHOD") in ("COUNTER", "DECLINECOUNTER"):
-                return
-            
-            # Anything else is not allowed. However, for compatibility we will optionally 
-            # return a success response for all attendees.
-            if config.Scheduling.CalDAV.OldDraftCompatibility:
-                self.fakeTheResult = True
-            else:
-                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid iTIP message for implicit scheduling"))
-
 class RemoteScheduler(Scheduler):
 
     def checkOrganizer(self):
@@ -857,18 +675,19 @@
         """
         pass
 
+
     @inlineCallbacks
     def checkRecipients(self):
         """
         Check the validity of the Recipient header values. These must all be local as there
         is no concept of server-to-server relaying.
         """
-        
+
         results = []
         for recipient in self.recipients:
             # Get the principal resource for this recipient
             principal = self.resource.principalForCalendarUserAddress(recipient)
-            
+
             # If no principal we may have a remote recipient but we should check whether
             # the address is one that ought to be on our server and treat that as a missing
             # user. Also if server-to-server is not enabled then remote addresses are not allowed.
@@ -889,358 +708,52 @@
                 else:
                     log.err("No schedule inbox for principal: %s" % (principal,))
                     results.append(InvalidCalendarUser(recipient))
-        
+
         self.recipients = results
 
-class IScheduleScheduler(RemoteScheduler):
 
-    def loadFromRequestHeaders(self):
-        """
-        Load Originator and Recipient from request headers.
-        """
-        super(IScheduleScheduler, self).loadFromRequestHeaders()
-        
-        if self.request.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
-            self.request.doing_attendee_refresh = 1
-        
-    def preProcessCalendarData(self):
-        """
-        For data coming in from outside we need to normalize the calendar user addresses so that later iTIP
-        processing will match calendar users against those in stored calendar data. Only do that for invites
-        not freebusy.
-        """
 
-        if not self.checkForFreeBusy():
-            self.calendar.normalizeCalendarUserAddresses(normalizationLookup,
-                self.resource.principalForCalendarUserAddress)
-
-    def checkAuthorization(self):
-        # Must have an unauthenticated user
-        if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
-            log.err("Authenticated originators not allowed: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "originator-allowed"),
-                "Authentication not allowed",
-            ))
-
-    @inlineCallbacks
-    def checkOriginator(self):
-        """
-        Check the validity of the Originator header.
-        """
-    
-        # For remote requests we do not allow the originator to be a local user or one within our domain.
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
-        if originatorPrincipal or localUser:
-            if originatorPrincipal.locallyHosted():
-                log.err("Cannot use originator that is on this server: %s" % (self.originator,))
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (caldav_namespace, "originator-allowed"),
-                    "Originator cannot be local to server",
-                ))
-            else:
-                self.originator = calendarUserFromPrincipal(self.originator, originatorPrincipal)
-                self._validAlternateServer(originatorPrincipal)
-        else:
-            self.originator = RemoteCalendarUser(self.originator)
-            self._validiScheduleServer()
-
-    def _validiScheduleServer(self):
-        """
-        Check the validity of the iSchedule host.
-        """
-    
-        # We will only accept originator in known domains.
-        servermgr = IScheduleServers()
-        server = servermgr.mapDomain(self.originator.domain)
-        if not server or not server.allow_from:
-            log.err("Originator not on recognized server: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "originator-allowed"),
-                "Originator not recognized by server",
-            ))
-        else:
-            # Get the request IP and map to hostname.
-            clientip = self.request.remoteAddr.host
-            
-            # First compare as dotted IP
-            matched = False
-            compare_with = (server.host,) + tuple(server.client_hosts)
-            if clientip in compare_with:
-                matched = True
-            else:
-                # Now do hostname lookup
-                try:
-                    host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
-                    for host in itertools.chain((host,), aliases):
-                        # Try simple match first
-                        if host in compare_with:
-                            matched = True
-                            break
-                        
-                        # Try pattern match next
-                        for pattern in compare_with:
-                            try:
-                                if re.match(pattern, host) is not None:
-                                    matched = True
-                                    break
-                            except re.error:
-                                log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
-                        else:
-                            continue
-                        break
-                except socket.herror, e:
-                    log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-                        
-            if not matched:
-                log.err("Originator not on allowed server: %s" % (self.originator,))
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (caldav_namespace, "originator-allowed"),
-                    "Originator not allowed to send to this server",
-                ))
-
-    def _validAlternateServer(self, principal):
-        """
-        Check the validity of the partitioned host.
-        """
-
-        # Extract expected host/port. This will be the partitionURI, or if no partitions,
-        # the serverURI
-        expected_uri = principal.partitionURI()
-        if expected_uri is None:
-            expected_uri = principal.serverURI()
-        expected_uri = urlparse.urlparse(expected_uri)
-        
-        # Get the request IP and map to hostname.
-        clientip = self.request.remoteAddr.host
-        
-        # Check against this server (or any of its partitions). We need this because an external iTIP message
-        # may be addressed to users on different partitions, and the node receiving the iTIP message will need to
-        # forward it to the partition nodes, thus the client ip seen by the partitions will in fact be the initial
-        # receiving node.
-        matched = False
-        if Servers.getThisServer().checkThisIP(clientip):
-            matched = True
-    
-        # Checked allowed IPs - if any were defined we only check against them, we do not
-        # go on to check the expected server host ip
-        elif Servers.getThisServer().hasAllowedFromIP():
-            matched = Servers.getThisServer().checkAllowedFromIP(clientip)
-            if not matched:
-                log.error("Invalid iSchedule connection from client: %s" % (clientip,))
-
-        # Next compare as dotted IP
-        elif isIPAddress(expected_uri.hostname):
-            if clientip == expected_uri.hostname:
-                matched = True
-        else:
-            # Now do expected hostname -> IP lookup
-            try:
-                # So now try the lookup of the expected host
-                _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(expected_uri.hostname)
-                for ip in ips:
-                    if ip == clientip:
-                        matched = True
-                        break
-            except socket.herror, e:
-                log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-        
-        # Check possible shared secret
-        if matched and not Servers.getThisServer().checkSharedSecret(self.request):
-            log.err("Invalid iSchedule shared secret")
-            matched = False
-
-        if not matched:
-            log.err("Originator not on allowed server: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "originator-allowed"),
-                "Originator not allowed to send to this server",
-            ))
-
-    @inlineCallbacks
-    def checkOrganizerAsOriginator(self):
-        """
-        Check the validity of the ORGANIZER value. ORGANIZER must not be local.
-        """
-        
-        # Verify that the ORGANIZER's cu address does not map to a valid user
-        organizer = self.calendar.getOrganizer()
-        if organizer:
-            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
-            if organizerPrincipal:
-                if organizerPrincipal.locallyHosted():
-                    log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
-                    raise HTTPError(ErrorResponse(
-                        responsecode.FORBIDDEN,
-                        (caldav_namespace, "organizer-allowed"),
-                        "Organizer is not local to server",
-                    ))
-                else:
-                    # Check that the origin server is the correct partition
-                    self.organizer = calendarUserFromPrincipal(organizer, organizerPrincipal)
-                    self._validAlternateServer(self.organizer.principal)
-            else:
-                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
-                if localUser:
-                    log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendar,))
-                    raise HTTPError(ErrorResponse(
-                        responsecode.FORBIDDEN,
-                        (caldav_namespace, "organizer-allowed"),
-                        "Organizer not allowed to be originator",
-                    ))
-                else:
-                    self.organizer = RemoteCalendarUser(organizer)
-        else:
-            log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "organizer-allowed"),
-                "No organizer in calendar data",
-            ))
-
-    @inlineCallbacks
-    def checkAttendeeAsOriginator(self):
-        """
-        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
-        Only local attendees are allowed for message originating from this server.
-        """
-        
-        # Attendee cannot be local.
-        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
-        if attendeePrincipal:
-            if attendeePrincipal.locallyHosted():
-                log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (caldav_namespace, "attendee-allowed"),
-                    "Local attendee cannot send to this server",
-                ))
-            else:
-                self._validAlternateServer(attendeePrincipal)
-        else:
-            localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
-            if localUser:
-                log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (caldav_namespace, "attendee-allowed"),
-                    "Attendee not allowed to schedule",
-                ))
-    
-        # TODO: in this case we should check that the ORGANIZER is the sole recipient.
-
-    @inlineCallbacks
-    def securityChecks(self):
-        """
-        Check that the originator has the appropriate rights to send this type of iTIP message.
-        """
-
-        # Prevent spoofing of ORGANIZER with specific METHODs when local
-        if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
-            yield self.checkOrganizerAsOriginator()
-    
-        # Prevent spoofing when doing reply-like METHODs
-        elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
-            yield self.checkAttendeeAsOriginator()
-            
-        else:
-            log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "valid-calendar-data"),
-                "Unknown iTIP method",
-            ))
-
-
 class DirectScheduler(Scheduler):
     """ An implicit scheduler meant for use by local processes which don't
         need to go through all these checks. """
 
+    errorResponse = ErrorResponse
+
     def checkAuthorization(self):
         pass
 
+
     def checkOrganizer(self):
         pass
 
+
     def checkOrganizerAsOriginator(self):
         pass
 
+
     def checkAttendeeAsOriginator(self):
         pass
 
+
     def securityChecks(self):
         pass
 
+
     def checkOriginator(self):
         pass
 
+
     def checkRecipients(self):
         pass
 
 
-class IMIPScheduler(RemoteScheduler):
 
-    def checkAuthorization(self):
-        pass
-
-    @inlineCallbacks
-    def checkOriginator(self):
-        """
-        Check the validity of the Originator header.
-        """
-    
-        # For remote requests we do not allow the originator to be a local user or one within our domain.
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
-        if originatorPrincipal or localUser:
-            log.err("Cannot use originator that is on this server: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "originator-allowed"),
-                "Originator cannot be local to server",
-            ))
-        else:
-            self.originator = RemoteCalendarUser(self.originator)
-
-    def checkOrganizerAsOriginator(self):
-        pass
-
-    def checkAttendeeAsOriginator(self):
-        pass
-
-    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(ErrorResponse(
-                responsecode.FORBIDDEN,
-                (caldav_namespace, "originator-allowed"),
-                "Originator server not allowed to send to this server",
-            ))
-
-
 class ScheduleResponseResponse (Response):
     """
     ScheduleResponse L{Response} object.
     Renders itself as a CalDAV:schedule-response XML document.
     """
-    def __init__(self, xml_responses, location=None):
+    def __init__(self, schedule_response_element, xml_responses, location=None):
         """
         @param xml_responses: an iterable of davxml.Response objects.
         @param location:      the value of the location header to return in the response,
@@ -1248,29 +761,42 @@
         """
 
         Response.__init__(self, code=responsecode.OK,
-                          stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+                          stream=schedule_response_element(*xml_responses).toxml())
 
         self.headers.setHeader("content-type", MimeType("text", "xml"))
-    
+
         if location is not None:
             self.headers.setHeader("location", location)
 
+
+
 class ScheduleResponseQueue (LoggingMixIn):
     """
     Stores a list of (typically error) responses for use in a
     L{ScheduleResponse}.
     """
+
+    schedule_response_element = caldavxml.ScheduleResponse
+    response_element = caldavxml.Response
+    recipient_element = caldavxml.Recipient
+    recipient_uses_href = True
+    request_status_element = caldavxml.RequestStatus
+    error_element = davxml.Error
+    response_description_element = davxml.ResponseDescription
+    calendar_data_element = caldavxml.CalendarData
+
     def __init__(self, method, success_response):
         """
         @param method: the name of the method generating the queue.
         @param success_response: the response to return in lieu of a
             L{ScheduleResponse} if no responses are added to this queue.
         """
-        self.responses         = []
-        self.method            = method
-        self.success_response  = success_response
-        self.location          = None
+        self.responses = []
+        self.method = method
+        self.success_response = success_response
+        self.location = None
 
+
     def setLocation(self, location):
         """
         @param location:      the value of the location header to return in the response,
@@ -1278,6 +804,7 @@
         """
         self.location = location
 
+
     def add(self, recipient, what, reqstatus=None, calendar=None, suppressErrorLog=False):
         """
         Add a response.
@@ -1290,12 +817,12 @@
             supported.
         """
         if type(what) is int:
-            code    = what
-            error   = None
+            code = what
+            error = None
             message = responsecode.RESPONSES[code]
         elif isinstance(what, Failure):
-            code    = statusForFailure(what)
-            error   = errorForFailure(what)
+            code = statusForFailure(what)
+            error = self.errorForFailure(what)
             message = messageForFailure(what)
         else:
             raise AssertionError("Unknown data type: %r" % (what,))
@@ -1304,41 +831,42 @@
             self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
 
         children = []
-        children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
-        children.append(caldavxml.RequestStatus(reqstatus))
+        children.append(self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient))
+        children.append(self.request_status_element(reqstatus))
         if calendar is not None:
-            children.append(caldavxml.CalendarData.fromCalendar(calendar))
+            children.append(self.calendar_data_element.fromCalendar(calendar))
         if error is not None:
             children.append(error)
         if message is not None:
-            children.append(davxml.ResponseDescription(message))
-        self.responses.append(caldavxml.Response(*children))
+            children.append(self.response_description_element(message))
+        self.responses.append(self.response_element(*children))
 
-    def clone(self, clone):
+
+    def errorForFailure(self, failure):
+        if failure.check(HTTPError) and isinstance(failure.value.response, ErrorResponse):
+            return self.error_element(failure.value.response.error)
+        else:
+            return None
+
+
+    def clone(self, recipient, request_status, calendar_data, error, desc):
         """
-        Add a response cloned from an existing caldavxml.Response element.
+        Add a response cloned from existing data.
         @param clone: the response to clone.
         """
-        if not isinstance(clone, caldavxml.Response):
-            raise AssertionError("Incorrect element type: %r" % (clone,))
 
-        recipient = clone.childOfType(caldavxml.Recipient)
-        request_status = clone.childOfType(caldavxml.RequestStatus)
-        calendar_data = clone.childOfType(caldavxml.CalendarData)
-        error = clone.childOfType(davxml.Error)
-        desc = clone.childOfType(davxml.ResponseDescription)
-
         children = []
-        children.append(recipient)
-        children.append(request_status)
+        children.append(self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient))
+        children.append(self.request_status_element.fromString(request_status))
         if calendar_data is not None:
-            children.append(calendar_data)
+            children.append(self.calendar_data_element.fromCalendar(calendar_data))
         if error is not None:
-            children.append(error)
+            children.append(self.error_element(*error))
         if desc is not None:
-            children.append(desc)
-        self.responses.append(caldavxml.Response(*children))
+            children.append(self.response_description_element.fromString(desc))
+        self.responses.append(self.response_element(*children))
 
+
     def response(self):
         """
         Generate a L{ScheduleResponseResponse} with the responses contained in the
@@ -1347,6 +875,6 @@
         @return: the response.
         """
         if self.responses:
-            return ScheduleResponseResponse(self.responses, self.location)
+            return ScheduleResponseResponse(self.schedule_response_element, self.responses, self.location)
         else:
             return self.success_response

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_caldav.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_caldav.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_caldav.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,33 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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 twistedcaldav.test.util
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
-from twistedcaldav.config import config
-
-class CalDAV (twistedcaldav.test.util.TestCase):
-    """
-    twistedcaldav.scheduling.caldav tests
-    """
-
-    def test_matchCalendarUserAddress(self):
-        """
-        Make sure we do an exact comparison on EmailDomain
-        """
-        config.Scheduling[ScheduleViaCalDAV.serviceType()]["EmailDomain"] = "example.com"
-        self.assertTrue(ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at example.com"))
-        self.assertFalse(ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at foo.example.com"))
-        self.assertFalse(ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at xyzexample.com"))

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_icaldiff.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_icaldiff.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -27,7 +27,7 @@
     """
 
     def test_organizer_diff(self):
-        
+
         data = (
             (
                 "#1.1 Simple component, no change",
@@ -477,7 +477,6 @@
 
     def test_attendee_merge_simple(self):
 
-        
         data = (
             (
                 "#1.1 Simple component, no change",
@@ -1185,8 +1184,9 @@
             )
             self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
     def test_attendee_merge_complex(self):
-        
+
         data = (
             (
                 "#1.1 Complex component, no change",
@@ -1684,7 +1684,7 @@
 
 
     def test_attendee_merge_exdate(self):
-        
+
         data = (
             (
                 "#1.1 Single component, one EXDATE",
@@ -1738,6 +1738,7 @@
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
 ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
 X-CALENDARSERVER-HIDDEN-INSTANCE:T
 END:VEVENT
 END:VCALENDAR
@@ -1795,6 +1796,7 @@
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
 ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
 X-CALENDARSERVER-HIDDEN-INSTANCE:T
 END:VEVENT
 BEGIN:VEVENT
@@ -1805,6 +1807,7 @@
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
 ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
 X-CALENDARSERVER-HIDDEN-INSTANCE:T
 END:VEVENT
 END:VCALENDAR
@@ -1871,6 +1874,7 @@
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE;PARTSTAT=DECLINED;RSVP=TRUE:mailto:user2 at example.com
 ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
 X-CALENDARSERVER-HIDDEN-INSTANCE:T
 END:VEVENT
 END:VCALENDAR
@@ -1938,6 +1942,7 @@
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE;PARTSTAT=DECLINED;RSVP=TRUE:mailto:user2 at example.com
 ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
 X-CALENDARSERVER-HIDDEN-INSTANCE:T
 END:VEVENT
 BEGIN:VEVENT
@@ -1948,6 +1953,7 @@
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE;PARTSTAT=DECLINED;RSVP=TRUE:mailto:user2 at example.com
 ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
 X-CALENDARSERVER-HIDDEN-INSTANCE:T
 END:VEVENT
 END:VCALENDAR
@@ -2021,8 +2027,9 @@
             )
             self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
     def test_attendee_merge_cancelled(self):
-        
+
         data = (
             (
                 "#1.1 Remove EXDATE add CANCELLED",
@@ -2385,8 +2392,9 @@
             )
             self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
     def test_attendee_merge_dropbox(self):
-        
+
         data = (
             (
                 "#1.1 Remove dropbox",
@@ -2664,9 +2672,9 @@
             )
             self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
     def test_attendee_merge_fake_master(self):
 
-        
         data = (
             (
                 "#1.1 Single overridden component, partstat change - ok",
@@ -2851,7 +2859,7 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True, ('20080601T120000Z','20080602T120000Z',), """BEGIN:VCALENDAR
+                (True, True, ('20080601T120000Z', '20080602T120000Z',), """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
@@ -2923,7 +2931,7 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True, ('20080601T120000Z','20080602T120000Z',), """BEGIN:VCALENDAR
+                (True, True, ('20080601T120000Z', '20080602T120000Z',), """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
@@ -3214,8 +3222,9 @@
             )
             self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
     def test_what_is_different(self):
-        
+
         data1 = (
             (
                 "#1.1 Simple component, no change",
@@ -3273,7 +3282,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(),}},
+                {"": {"SUMMARY": set(), }},
             ),
             (
                 "#1.3 Simple component, one property change, one addition, one removal",
@@ -3301,7 +3310,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(), "LOCATION":set(), "DESCRIPTION":set(),}},
+                {"": {"SUMMARY": set(), "LOCATION": set(), "DESCRIPTION": set(), }},
             ),
             (
                 "#1.4 Simple component, add attendee",
@@ -3334,7 +3343,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#1.5 Simple component, remove attendee",
@@ -3365,7 +3374,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#1.6 Simple component, attendee PARTSTAT only",
@@ -3397,7 +3406,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#1.7 Simple component, attendee PARTSTAT and addition",
@@ -3430,7 +3439,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#1.8 Simple component, attendee RSVP only",
@@ -3462,7 +3471,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#1.9 Simple component, DTSTART/DTEND VALUE",
@@ -3494,7 +3503,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"DTEND":set(("VALUE",)), "DTSTART":set(("VALUE",)),}},
+                {"": {"DTEND": set(("VALUE",)), "DTSTART": set(("VALUE",)), }},
             ),
             (
                 "#1.10 Simple component, DTSTART/DTEND TZID",
@@ -3562,10 +3571,10 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"DTEND":set(("TZID",)), "DTSTART":set(("TZID",)),}},
+                {"": {"DTEND": set(("TZID",)), "DTSTART": set(("TZID",)), }},
             ),
         )
-        
+
         data2 = (
             (
                 "#2.1 Simple recurring component, no change",
@@ -3627,7 +3636,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(),}},
+                {"": {"SUMMARY": set(), }},
             ),
             (
                 "#2.3 Simple component, one property change, one addition, one removal",
@@ -3657,7 +3666,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(), "LOCATION":set(), "DESCRIPTION":set(),}},
+                {"": {"SUMMARY": set(), "LOCATION": set(), "DESCRIPTION": set(), }},
             ),
             (
                 "#2.4 Simple component, add attendee",
@@ -3692,7 +3701,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#2.5 Simple component, remove attendee",
@@ -3725,7 +3734,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#2.6 Simple component, attendee PARTSTAT only",
@@ -3759,7 +3768,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#2.7 Simple component, attendee PARTSTAT and addition",
@@ -3794,7 +3803,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#2.8 Simple recurring component, property order change",
@@ -3835,7 +3844,7 @@
                 {},
             ),
         )
-        
+
         data3 = (
             (
                 "#3.1 Complex recurring component, no change",
@@ -3937,7 +3946,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"20080602T120000Z":{"SUMMARY":set(),}},
+                {"20080602T120000Z": {"SUMMARY": set(), }},
             ),
             (
                 "#3.3 Simple component, one property change in master",
@@ -3985,7 +3994,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(),}},
+                {"": {"SUMMARY": set(), }},
             ),
             (
                 "#3.4 Simple component, one property change in master and instance",
@@ -4033,7 +4042,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(),}, "20080602T120000Z":{"SUMMARY":set(),}},
+                {"": {"SUMMARY": set(), }, "20080602T120000Z": {"SUMMARY": set(), }},
             ),
             (
                 "#3.5 Simple component, different property change in master and instance",
@@ -4082,7 +4091,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set()}, "20080602T120000Z":{"Description":set()}},
+                {"": {"SUMMARY": set()}, "20080602T120000Z": {"Description": set()}},
             ),
             (
                 "#3.6 Simple component, instance added no change",
@@ -4152,7 +4161,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"20080602T120000Z":{"DTSTART":set(), "DTEND":set(), }},
+                {"20080602T120000Z": {"DTSTART": set(), "DTEND": set(), }},
             ),
             (
                 "#3.8 Simple component, instance removed no change",
@@ -4222,17 +4231,18 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"20080602T120000Z":{"DTSTART":set(), "DTEND":set(), }},
+                {"20080602T120000Z": {"DTSTART": set(), "DTEND": set(), }},
             ),
         )
-        
+
         for description, calendar1, calendar2, rids in itertools.chain(data1, data2, data3,):
             differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
             got_rids = differ.whatIsDifferent()
             self.assertEqual(got_rids, rids, msg="%s expected R-IDs: '%s', got: '%s'" % (description, rids, got_rids,))
-        
+
+
     def test_organizer_smart_merge(self):
-        
+
         data1 = (
             (
                 "#1.1 Simple component, no change",
@@ -4475,7 +4485,7 @@
 """,
             ),
         )
-        
+
         data2 = (
             (
                 "#2.1 Simple recurring component, no change",
@@ -5030,7 +5040,7 @@
 """,
             ),
         )
-        
+
         for description, calendar1, calendar2, changed_calendar in itertools.chain(data1, data2,):
             cal1 = Component.fromString(calendar1)
             cal2 = Component.fromString(calendar2)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_imip.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_imip.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_imip.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,70 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks
-from twext.web2 import responsecode
-from twistedcaldav.ical import Component
-from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
-from twistedcaldav.scheduling.imip import ScheduleViaIMip
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.scheduling.scheduler import ScheduleResponseQueue
-import twistedcaldav.test.util
-from twistedcaldav.config import config
-
-class iMIPProcessing (twistedcaldav.test.util.TestCase):
-    """
-    iCalendar support tests
-    """
-
-    class FakeSchedule(object):
-        
-        def __init__(self, calendar):
-            self.calendar = calendar
-
-    @inlineCallbacks
-    def test_no_freebusy(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REQUEST
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VFREEBUSY
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VFREEBUSY
-END:VCALENDAR
-"""
-
-        scheduler = iMIPProcessing.FakeSchedule(Component.fromString(data))
-        recipients = (RemoteCalendarUser("mailto:user1 at example.com"),)
-        responses = ScheduleResponseQueue("REQUEST", responsecode.OK)
-
-        delivery = ScheduleViaIMip(scheduler, recipients, responses, True)
-        yield delivery.generateSchedulingResponses()
-        
-        self.assertEqual(len(responses.responses), 1)
-        self.assertEqual(str(responses.responses[0].children[1]), iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
-
-    def test_matchCalendarUserAddress(self):
-        # iMIP not sensitive to case:
-        self.patch(config.Scheduling[ScheduleViaIMip.serviceType()], "AddressPatterns", ["mailto:.*"])
-        self.assertTrue(ScheduleViaIMip.matchCalendarUserAddress("mailto:user at xyzexample.com"))
-        self.assertTrue(ScheduleViaIMip.matchCalendarUserAddress("MAILTO:user at xyzexample.com"))

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_implicit.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_implicit.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_implicit.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -30,29 +30,35 @@
     """
     A fake CalDAVScheduler that does nothing except track who messages were sent to.
     """
-    
+
     def __init__(self, recipients):
         self.recipients = recipients
 
+
     def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
         self.recipients.extend(recipients)
         return succeed(ScheduleResponseQueue("FAKE", responsecode.OK))
 
+
+
 class FakePrincipal(object):
-    
+
     def __init__(self, cuaddr):
         self.cuaddr = cuaddr
-        
+
+
     def calendarUserAddresses(self):
         return (self.cuaddr,)
 
+
+
 class Implicit (twistedcaldav.test.util.TestCase):
     """
     iCalendar support tests
     """
 
     def test_removed_attendees(self):
-        
+
         data = (
             (
                 "#1.1 Simple component, no change",
@@ -785,7 +791,7 @@
             self.assertEqual(scheduler.cancelledAttendees, set(result), msg=description)
 
 
-    @inlineCallbacks   
+    @inlineCallbacks
     def test_process_request_excludes_includes(self):
         """
         Test that processRequests correctly excludes or includes the specified attendees.
@@ -826,25 +832,27 @@
             scheduler.only_refresh_attendees = includes
             scheduler.changed_rids = None
             scheduler.reinvites = None
-    
+
             # Get some useful information from the calendar
             yield scheduler.extractCalendarData()
             scheduler.organizerPrincipal = FakePrincipal(scheduler.organizer)
-    
+
             recipients = []
-            
+
             def makeFakeScheduler():
                 return FakeScheduler(recipients)
             scheduler.makeScheduler = makeFakeScheduler
-            
+
             count = (yield scheduler.processRequests())
             self.assertEqual(count, result_count)
             self.assertEqual(len(recipients), result_count)
             self.assertEqual(set(recipients), set(result_set))
 
+
+
 class ImplicitRequests (HomeTestCase):
     """
-    Test twistedcaldav.scheduyling.implicit with a Request object. 
+    Test twistedcaldav.scheduyling.implicit with a Request object.
     """
 
     @inlineCallbacks
@@ -852,7 +860,7 @@
         """
         Test that checkImplicitState() always returns True for any organizer, valid or not.
         """
-        
+
         data = (
             (
                 """BEGIN:VCALENDAR
@@ -907,25 +915,26 @@
             request = SimpleRequest(self.site, "PUT", "/calendar/1.ics")
             calresource = yield request.locateResource("/calendar/1.ics")
             self.assertEqual(calresource.isScheduleObject, None)
-            
+
             scheduler = ImplicitScheduler()
             doAction, isScheduleObject = (yield scheduler.testImplicitSchedulingPUT(request, calresource, "/calendar/1.ics", calendar, False))
             self.assertEqual(doAction, result)
             self.assertEqual(isScheduleObject, result)
             request._newStoreTransaction.abort()
 
+
     @inlineCallbacks
     def test_testImplicitSchedulingPUT_FixScheduleState(self):
         """
         Test that testImplicitSchedulingPUT will fix an old cached schedule object state by
         re-evaluating the calendar data.
         """
-        
+
         request = SimpleRequest(self.site, "PUT", "/calendar/1.ics")
         calresource = yield request.locateResource("/calendar/1.ics")
         self.assertEqual(calresource.isScheduleObject, None)
         calresource.isScheduleObject = False
-        
+
         calendarOld = Component.fromString("""BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -940,7 +949,6 @@
 END:VCALENDAR
 """)
 
-
         calendarNew = Component.fromString("""BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -955,9 +963,9 @@
 END:VCALENDAR
 """)
 
-        calresource.exists = lambda :True
-        calresource.iCalendarForUser = lambda request:succeed(calendarOld)
-        
+        calresource.exists = lambda : True
+        calresource.iCalendarForUser = lambda request: succeed(calendarOld)
+
         scheduler = ImplicitScheduler()
         try:
             doAction, isScheduleObject = (yield scheduler.testImplicitSchedulingPUT(request, calresource, "/calendars/users/user01/calendar/1.ics", calendarNew, False))
@@ -966,18 +974,19 @@
         self.assertTrue(doAction)
         self.assertTrue(isScheduleObject)
 
+
     @inlineCallbacks
     def test_testImplicitSchedulingPUT_NoChangeScheduleState(self):
         """
         Test that testImplicitSchedulingPUT will prevent attendees from changing the
         schedule object state.
         """
-        
+
         request = SimpleRequest(self.site, "PUT", "/calendar/1.ics")
         calresource = yield request.locateResource("/calendar/1.ics")
         self.assertEqual(calresource.isScheduleObject, None)
         calresource.isScheduleObject = False
-        
+
         calendarOld = Component.fromString("""BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -989,7 +998,6 @@
 END:VCALENDAR
 """)
 
-
         calendarNew = Component.fromString("""BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -1004,9 +1012,9 @@
 END:VCALENDAR
 """)
 
-        calresource.exists = lambda :True
-        calresource.iCalendarForUser = lambda request:succeed(calendarOld)
-        
+        calresource.exists = lambda : True
+        calresource.iCalendarForUser = lambda request: succeed(calendarOld)
+
         scheduler = ImplicitScheduler()
         try:
             yield scheduler.testImplicitSchedulingPUT(request, calresource, "/calendars/users/user01/calendar/1.ics", calendarNew, False)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_ischeduleservers.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_ischeduleservers.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_ischeduleservers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,49 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.python.filepath import FilePath
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServersParser
-import twistedcaldav.test.util
-
-class Test_IScheduleServersParser(twistedcaldav.test.util.TestCase):
-    """
-    Test L{IScheduleServersParser} implementation.
-    """
-
-    def test_readXML(self):
-
-        fp = FilePath(self.mktemp())
-        fp.open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE servers SYSTEM "servertoserver.dtd">
-<servers>
-  <server>
-    <uri>https://localhost:8543/inbox</uri>
-    <allow-requests-from/>
-    <allow-requests-to/>
-    <domains>
-        <domain>example.org</domain>
-    </domains>
-    <hosts>
-        <host>127.0.0.1</host>
-    </hosts>
-  </server>
-</servers>
-"""
-)
-        
-        parser = IScheduleServersParser(fp)
-        self.assertEqual(len(parser.servers), 1)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_itip.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/test/test_itip.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -27,7 +27,7 @@
     """
 
     def test_update_attendee_partstat(self):
-        
+
         data = (
             (
                 "#1.1 Simple component, accepted",
@@ -888,7 +888,7 @@
                     msg=description
                 )
                 self.assertEqual(
-                    tuple(sorted(list(reply_rids), key=lambda x:x[0])),
+                    tuple(sorted(list(reply_rids), key=lambda x: x[0])),
                     rids,
                     msg=description
                 )
@@ -899,11 +899,12 @@
                     msg=description
                 )
 
+
     def test_sequenceComparison(self):
         """
         Test iTIPProcessing.sequenceComparison
         """
-        
+
         data = (
             (
                 "1.1 Simple Update - SEQUENCE change",
@@ -1385,15 +1386,16 @@
                 True,
             ),
         )
-        
+
         for title, calendar_txt, itip_txt, expected in data:
             calendar = Component.fromString(calendar_txt)
             itip = Component.fromString(itip_txt)
 
             result = iTipProcessing.sequenceComparison(itip, calendar)
             self.assertEqual(result, expected, msg="Result mismatch: %s" % (title,))
-            
 
+
+
 class iTIPGenerator (twistedcaldav.test.util.TestCase):
     """
     iCalendar support tests
@@ -1401,7 +1403,7 @@
     data_dir = os.path.join(os.path.dirname(__file__), "data")
 
     def test_request(self):
-        
+
         data = (
             # Simple component, no Attendees - no filtering
             (
@@ -1571,7 +1573,7 @@
 END:VCALENDAR
 """,
                 ("mailto:user3 at example.com",)
-            ),        
+            ),
 
             # Recurring component with one instance, master with one attendee, instance without attendee - filtering match
             (
@@ -1704,7 +1706,7 @@
                 ("mailto:user3 at example.com",)
             ),
         )
-        
+
         for original, filtered, attendees in data:
             component = Component.fromString(original)
             itipped = iTipGenerator.generateAttendeeRequest(component, attendees, None)
@@ -1712,8 +1714,9 @@
             itipped = "".join([line for line in itipped.splitlines(True) if not line.startswith("DTSTAMP:")])
             self.assertEqual(filtered, itipped)
 
+
     def test_cancel(self):
-        
+
         data = (
             # Simple component, with two attendees - cancel one
             (
@@ -1810,7 +1813,7 @@
 END:VCALENDAR
 """,
                 ("mailto:user2 at example.com",),
-                (PyCalendarDateTime(2008, 11, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)), ),
+                (PyCalendarDateTime(2008, 11, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)),),
             ),
 
             # Recurring component with one instance, each with one attendee - cancel instance
@@ -1851,7 +1854,7 @@
 END:VCALENDAR
 """,
                 ("mailto:user2 at example.com",),
-                (PyCalendarDateTime(2008, 11, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)), ),
+                (PyCalendarDateTime(2008, 11, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)),),
             ),
 
             # Recurring component with one instance, each with one attendee - cancel master
@@ -1892,7 +1895,7 @@
 END:VCALENDAR
 """,
                 ("mailto:user2 at example.com",),
-                (None, ),
+                (None,),
             ),
 
             # Recurring component - cancel non-existent instance
@@ -1912,11 +1915,11 @@
 """,
                 "",
                 ("mailto:user2 at example.com",),
-                (PyCalendarDateTime(2008, 12, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)), ),
+                (PyCalendarDateTime(2008, 12, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)),),
             ),
 
         )
-        
+
         for original, filtered, attendees, instances in data:
             component = Component.fromString(original)
             itipped = iTipGenerator.generateCancel(component, attendees, instances)
@@ -1924,6 +1927,7 @@
             itipped = "".join([line for line in itipped.splitlines(True) if not line.startswith("DTSTAMP:")])
             self.assertEqual(filtered, itipped)
 
+
     def test_missingAttendee(self):
         """
         When generating a reply, remove all components that are missing

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/utils.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/utils.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/scheduling/utils.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -43,7 +43,7 @@
 
         # Get matching newstore objects
         objectResources = (yield calendar_home.getCalendarResourcesForUID(uid, allow_shared))
-        
+
         # 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())
@@ -54,4 +54,3 @@
             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"],))
-

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/servers.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/servers.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/servers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,295 +0,0 @@
-##
-# Copyright (c) 2011-2012 Apple Inc. All 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.python.log import Logger
-from twisted.internet.abstract import isIPAddress
-from twistedcaldav.client.pool import installPool
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.xmlutil import readXML
-import socket
-import urlparse
-
-"""
-XML based server configuration file handling.
-
-This is used in an environment where more than one server is being used within a single domain. i.e., all
-the principals across the whole domain need to be able to directly schedule each other and know of each others
-existence. A common scenario would be a production server and a development/test server.
-
-Each server is identified by an id and url. The id is used when assigning principals to a specific server. Each
-server can also support multiple partitions, and each of those is identified by an id and url, with the id also
-being used to assign principals to a specific partition.
-"""
-
-__all__ = [
-    "Servers",
-]
-
-log = Logger()
-
-SERVER_SECRET_HEADER = "X-CALENDARSERVER-ISCHEDULE"
-
-class ServersDB(object):
-    """
-    Represents the set of servers within the same domain.
-    """
-    
-    def __init__(self):
-        
-        self._servers = {}
-        self._xmlFile = None
-        self._thisServer = None
-
-    def load(self, xmlFile=None, ignoreIPLookupFailures=False):
-        if self._xmlFile is None or xmlFile is not None:
-            self._servers = {}
-            if xmlFile:
-                self._xmlFile = xmlFile
-            else:
-                self._xmlFile = fullServerPath(
-                    config.ConfigRoot,
-                    config.Servers.ConfigFile
-                )
-        self._servers = ServersParser.parse(self._xmlFile, ignoreIPLookupFailures=ignoreIPLookupFailures)
-        for server in self._servers.values():
-            if server.thisServer:
-                self._thisServer = server
-                break
-        else:
-            raise ValueError("No server in %s matches this server." % (self._xmlFile,))
-    
-    def clear(self):
-        self._servers = {}
-        self._xmlFile = None
-        self._thisServer = None
-
-    def getServerById(self, id):
-        return self._servers.get(id)
-        
-    def getServerURIById(self, id):
-        try:
-            return self._servers[id].uri
-        except KeyError:
-            return None
-    
-    def getThisServer(self):
-        return self._thisServer
-
-Servers = ServersDB()   # Global server DB
-
-class Server(object):
-    """
-    Represents a server which may itself be partitioned.
-    """
-    
-    def __init__(self):
-        self.id = None
-        self.uri = None
-        self.thisServer = False
-        self.ips = set()
-        self.allowed_from_ips = set()
-        self.shared_secret = None
-        self.partitions = {}
-        self.partitions_ips = set()
-        self.isImplicit = True
-    
-    def check(self, ignoreIPLookupFailures=False):
-        # Check whether this matches the current server
-        parsed_uri = urlparse.urlparse(self.uri)
-        if parsed_uri.hostname == config.ServerHostName:
-            if parsed_uri.scheme == "http":
-                if config.HTTPPort:
-                    self.thisServer = parsed_uri.port in (config.HTTPPort,) + tuple(config.BindHTTPPorts)
-            elif parsed_uri.scheme == "https":
-                if config.SSLPort:
-                    self.thisServer = parsed_uri.port in (config.SSLPort,) + tuple(config.BindSSLPorts)
-        
-        # Need to cache IP addresses
-        try:
-            _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(parsed_uri.hostname)
-        except socket.gaierror, e:
-            msg = "Unable to lookup ip-addr for server '%s': %s" % (parsed_uri.hostname, str(e))
-            log.error(msg)
-            if ignoreIPLookupFailures:
-                ips = ()
-            else:
-                raise ValueError(msg)
-        self.ips = set(ips)
-
-        actual_ips = set()
-        for item in self.allowed_from_ips:
-            if not isIPAddress(item):
-                try:
-                    _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(item)
-                except socket.gaierror, e:
-                    msg = "Unable to lookup ip-addr for allowed-from '%s': %s" % (item, str(e))
-                    log.error(msg)
-                    if not ignoreIPLookupFailures:
-                        raise ValueError(msg)
-                else:
-                    actual_ips.update(ips)
-            else:
-                actual_ips.add(item)
-        self.allowed_from_ips = actual_ips
-            
-        for uri in self.partitions.values():
-            parsed_uri = urlparse.urlparse(uri)
-            try:
-                _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(parsed_uri.hostname)
-            except socket.gaierror, e:
-                msg = "Unable to lookup ip-addr for partition '%s': %s" % (parsed_uri.hostname, str(e))
-                log.error(msg)
-                if ignoreIPLookupFailures:
-                    ips = ()
-                else:
-                    raise ValueError(msg)
-            self.partitions_ips.update(ips)
-    
-    def checkThisIP(self, ip):
-        """
-        Check that the passed in IP address corresponds to this server or one of its partitions.
-        """
-        return (ip in self.ips) or (ip in self.partitions_ips)
-
-    def hasAllowedFromIP(self):
-        return len(self.allowed_from_ips) > 0
-
-    def checkAllowedFromIP(self, ip):
-        return ip in self.allowed_from_ips
-
-    def checkSharedSecret(self, request):
-        
-        # Get header from the request
-        request_secret = request.headers.getRawHeaders(SERVER_SECRET_HEADER)
-        
-        if request_secret is not None and self.shared_secret is None:
-            log.error("iSchedule request included unexpected %s header" % (SERVER_SECRET_HEADER,))
-            return False
-        elif request_secret is None and self.shared_secret is not None:
-            log.error("iSchedule request did not include required %s header" % (SERVER_SECRET_HEADER,))
-            return False
-        elif (request_secret[0] if request_secret else None) != self.shared_secret:
-            log.error("iSchedule request %s header did not match" % (SERVER_SECRET_HEADER,))
-            return False
-        else:
-            return True
-
-    def secretHeader(self):
-        """
-        Return a tuple of header name, header value
-        """
-        return (SERVER_SECRET_HEADER, self.shared_secret,)
-
-    def addPartition(self, id, uri):
-        self.partitions[id] = uri
-    
-    def getPartitionURIForId(self, id):
-        return self.partitions.get(id)
-    
-    def isPartitioned(self):
-        return len(self.partitions) != 0
-
-    def installReverseProxies(self, ownUID, maxClients):
-        
-        for partition, url in self.partitions.iteritems():
-            if partition != ownUID:
-                installPool(
-                    partition,
-                    url,
-                    maxClients,
-                )
-    
-        
-        
-ELEMENT_SERVERS                 = "servers"
-ELEMENT_SERVER                  = "server"
-ELEMENT_ID                      = "id"
-ELEMENT_URI                     = "uri"
-ELEMENT_ALLOWED_FROM            = "allowed-from"
-ELEMENT_SHARED_SECRET           = "shared-secret"
-ELEMENT_PARTITIONS              = "partitions"
-ELEMENT_PARTITION               = "partition"
-ATTR_IMPLICIT                   = "implicit"
-ATTR_VALUE_YES                  = "yes"
-ATTR_VALUE_NO                   = "no"
-
-class ServersParser(object):
-    """
-    Servers configuration file parser.
-    """
-    @staticmethod
-    def parse(xmlFile, ignoreIPLookupFailures=False):
-
-        results = {}
-
-        # Read in XML
-        try:
-            _ignore_tree, servers_node = readXML(xmlFile, ELEMENT_SERVERS)
-        except ValueError, e:
-            log.error("XML parse error for '%s' because: %s" % (xmlFile, e,), raiseException=RuntimeError)
-
-        for child in servers_node.getchildren():
-            
-            if child.tag != ELEMENT_SERVER:
-                log.error("Unknown server type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
-            server = Server()
-            server.isImplicit = child.get(ATTR_IMPLICIT, ATTR_VALUE_YES) == ATTR_VALUE_YES
-
-            for node in child.getchildren():
-                if node.tag == ELEMENT_ID:
-                    server.id = node.text
-                elif node.tag == ELEMENT_URI:
-                    server.uri = node.text
-                elif node.tag == ELEMENT_ALLOWED_FROM:
-                    server.allowed_from_ips.add(node.text)
-                elif node.tag == ELEMENT_SHARED_SECRET:
-                    server.shared_secret = node.text
-                elif node.tag == ELEMENT_PARTITIONS:
-                    ServersParser._parsePartition(xmlFile, node, server)
-                else:
-                    log.error("Invalid element '%s' in servers file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
-
-            if server.id is None or server.uri is None:
-                log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
-            server.check(ignoreIPLookupFailures=ignoreIPLookupFailures)
-            results[server.id] = server
-
-        return results
-
-    @staticmethod
-    def _parsePartition(xmlFile, partitions, server):
-
-        for child in partitions.getchildren():
-            
-            if child.tag != ELEMENT_PARTITION:
-                log.error("Unknown partition type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
-            id = None
-            uri = None
-            for node in child.getchildren():
-                if node.tag == ELEMENT_ID:
-                    id = node.text
-                elif node.tag == ELEMENT_URI:
-                    uri = node.text
-                else:
-                    log.error("Invalid element '%s' in augment file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
-        
-            if id is None or uri is None:
-                log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-            
-            server.addPartition(id, uri)

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/simpleresource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/simpleresource.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/simpleresource.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -23,17 +23,20 @@
     "SimpleResource",
     "SimpleCalDAVResource",
     "SimpleRedirectResource",
+    "SimpleDataResource",
 ]
 
 from twext.web2 import http
-from txdav.xml import element as davxml
 from twext.web2.dav.noneprops import NonePropertyStore
+from twext.web2.http import Response
 
 from twisted.internet.defer import succeed
 
+from twistedcaldav.config import config
 from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.config import config
 
+from txdav.xml import element as davxml
+
 class SimpleResource (
     CalDAVResource,
 ):
@@ -63,22 +66,28 @@
         self._isDir = isdir
         self.defaultACL = defaultACL
 
+
     def isCollection(self):
         return self._isDir
 
+
     def deadProperties(self):
         if not hasattr(self, "_dead_properties"):
             self._dead_properties = NonePropertyStore(self)
         return self._dead_properties
 
+
     def etag(self):
         return succeed(None)
 
+
     def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
         return succeed(self.defaultACL)
 
 SimpleCalDAVResource = SimpleResource
 
+
+
 class SimpleRedirectResource(SimpleResource):
     """
     A L{SimpleResource} which always performs a redirect.
@@ -94,5 +103,34 @@
         SimpleResource.__init__(self, principalCollections=principalCollections, isdir=isdir, defaultACL=defaultACL)
         self._kwargs = kwargs
 
+
     def renderHTTP(self, request):
         return http.RedirectResponse(request.unparseURL(host=config.ServerHostName, **self._kwargs))
+
+
+
+class SimpleDataResource(SimpleResource):
+    """
+    A L{SimpleResource} which returns fixed content.
+    """
+
+    def __init__(self, principalCollections, content_type, data, defaultACL=SimpleResource.authReadACL):
+        """
+        @param content_type: the mime content-type of the data
+        @type content_type: L{MimeType}
+        @param data: the data
+        @type data: C{str}
+        """
+        SimpleResource.__init__(self, principalCollections=principalCollections, isdir=False, defaultACL=defaultACL)
+        self.content_type = content_type
+        self.data = data
+
+
+    def contentType(self):
+        return self.content_type
+
+
+    def render(self, request):
+        response = Response(200, {}, self.data)
+        response.headers.setHeader("content-type", self.content_type)
+        return response

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -178,16 +178,16 @@
 
 DEFAULT_AUGMENT_PARAMS = {
     "twistedcaldav.directory.augment.AugmentXMLDB": {
-        "xmlFiles": ["augments.xml",],
+        "xmlFiles": ["augments.xml", ],
         "statSeconds" : 15,
     },
     "twistedcaldav.directory.augment.AugmentSqliteDB": {
         "dbpath": "augments.sqlite",
     },
     "twistedcaldav.directory.augment.AugmentPostgreSQLDB": {
-        "host":     "localhost",
+        "host": "localhost",
         "database": "augments",
-        "user":     "",
+        "user": "",
         "password": "",
     },
 }
@@ -197,9 +197,9 @@
         "dbpath": "proxies.sqlite",
     },
     "twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB": {
-        "host":     "localhost",
+        "host": "localhost",
         "database": "proxies",
-        "user":     "",
+        "user": "",
         "password": "",
         "dbtype": "",
     },
@@ -215,10 +215,10 @@
         "peopleNode": "/Search/Contacts",
         "queryUserRecords": True,
         "userNode": "/Search/Contacts",
-        "maxDSQueryRecords":0,
+        "maxDSQueryRecords": 0,
         "queryDSLocal": False,
         "ignoreSystemRecords": True,
-        "dsLocalCacheTimeout":30,
+        "dsLocalCacheTimeout": 30,
         "liveQuery": True,
         "fakeETag": True,
         "cacheQuery": False,
@@ -243,12 +243,12 @@
     #    default.  For example, it may be the address of a load balancer or
     #    proxy which forwards connections to the server.
     #
-    "ServerHostName": "",          # Network host name.
-    "HTTPPort": 0,                 # HTTP port (0 to disable HTTP)
-    "SSLPort" : 0,                 # SSL port (0 to disable HTTPS)
-    "EnableSSL" : False,           # Whether to listen on SSL port(s)
+    "ServerHostName": "", # Network host name.
+    "HTTPPort": 0, # HTTP port (0 to disable HTTP)
+    "SSLPort" : 0, # SSL port (0 to disable HTTPS)
+    "EnableSSL" : False, # Whether to listen on SSL port(s)
     "RedirectHTTPToHTTPS" : False, # If True, all nonSSL requests redirected to an SSL Port
-    "SSLMethod" : "SSLv3_METHOD",  # SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD
+    "SSLMethod" : "SSLv3_METHOD", # SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD
     "SSLCiphers" : "ALL:!aNULL:!ADH:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM",
 
     #
@@ -256,38 +256,40 @@
     #
     #    This configures the actual network address that the server binds to.
     #
-    "BindAddresses": [],   # List of IP addresses to bind to [empty = all]
-    "BindHTTPPorts": [],   # List of port numbers to bind to for HTTP
+    "BindAddresses": [], # List of IP addresses to bind to [empty = all]
+    "BindHTTPPorts": [], # List of port numbers to bind to for HTTP
                            # [empty = same as "Port"]
-    "BindSSLPorts" : [],   # List of port numbers to bind to for SSL
+    "BindSSLPorts" : [], # List of port numbers to bind to for SSL
                            # [empty = same as "SSLPort"]
-    "InheritFDs"   : [],   # File descriptors to inherit for HTTP requests
+    "InheritFDs"   : [], # File descriptors to inherit for HTTP requests
                            # (empty = don't inherit)
-    "InheritSSLFDs": [],   # File descriptors to inherit for HTTPS requests
+    "InheritSSLFDs": [], # File descriptors to inherit for HTTPS requests
                            # (empty = don't inherit)
-    "MetaFD"       : 0,    # Inherited file descriptor to call recvmsg() on to
+    "MetaFD"       : 0, # Inherited file descriptor to call recvmsg() on to
                            # receive sockets (none = don't inherit)
 
     "UseMetaFD"    : True, # Use a 'meta' FD, i.e. an FD to transmit other FDs
                            # to slave processes.
 
     "UseDatabase"  : True, # True: database; False: files
-    
+
     "TransactionTimeoutSeconds" : 0, # Timeout transactions that take longer than
                               # the specified number of seconds. Zero means
                               # no timeouts
 
-    "DBType"       : "",   # 2 possible values: empty, meaning 'spawn postgres
+    "DBType"       : "", # 2 possible values: empty, meaning 'spawn postgres
                            # yourself', or 'postgres', meaning 'connect to a
                            # postgres database as specified by the 'DSN'
                            # configuration key.  Will support more values in
                            # the future.
 
-    "DSN"          : "",   # Data Source Name.  Used to connect to an external
+    "SpawnedDBUser" : "caldav", # The username to use when DBType is empty
+
+    "DSN"          : "", # Data Source Name.  Used to connect to an external
                            # database if DBType is non-empty.  Format varies
                            # depending on database type.
 
-    "DBAMPFD"      : 0,    # Internally used by database to tell slave
+    "DBAMPFD"      : 0, # Internally used by database to tell slave
                            # processes to inherit a file descriptor and use it
                            # as an AMP connection over a UNIX socket; see
                            # twext.enterprise.adbapi2.ConnectionPoolConnection
@@ -299,8 +301,8 @@
     #
     # Types of service provided
     #
-    "EnableCalDAV"  : True,  # Enable CalDAV service
-    "EnableCardDAV" : True,  # Enable CardDAV service
+    "EnableCalDAV"  : True, # Enable CalDAV service
+    "EnableCardDAV" : True, # Enable CardDAV service
 
     #
     # Data store
@@ -314,20 +316,20 @@
     "LogRoot"                 : "/var/log/caldavd",
     "RunRoot"                 : "/var/run/caldavd",
     "WebCalendarRoot"         : "/Applications/Server.app/Contents/ServerRoot/usr/share/collabd",
-    
+
     #
     # Quotas
     #
-    
+
     # Attachments
     "UserQuota"                 : 104857600, # User attachment quota (in bytes)
-    
+
     # Resource data
-    "MaxCollectionsPerHome"     :      50, # Maximum number of calendars/address books allowed in a home
-    "MaxResourcesPerCollection" :   10000, # Maximum number of resources in a calendar/address book
+    "MaxCollectionsPerHome"     : 50, # Maximum number of calendars/address books allowed in a home
+    "MaxResourcesPerCollection" : 10000, # Maximum number of resources in a calendar/address book
     "MaxResourceSize"           : 1048576, # Maximum resource size (in bytes)
-    "MaxAttendeesPerInstance"   :     100, # Maximum number of unique attendees
-    "MaxAllowedInstances"       :    3000, # Maximum number of instances the server will index
+    "MaxAttendeesPerInstance"   : 100, # Maximum number of unique attendees
+    "MaxAllowedInstances"       : 3000, # Maximum number of instances the server will index
 
     # Set to URL path of wiki authentication service, e.g. "/auth", in order
     # to use javascript authentication dialog.  Empty string indicates standard
@@ -376,22 +378,22 @@
         "type": "twistedcaldav.directory.calendaruserproxy.ProxySqliteDB",
         "params": DEFAULT_PROXYDB_PARAMS["twistedcaldav.directory.calendaruserproxy.ProxySqliteDB"],
     },
-    "ProxyLoadFromFile": "",    # Allows for initialization of the proxy database from an XML file
+    "ProxyLoadFromFile": "", # Allows for initialization of the proxy database from an XML file
 
     #
     # Special principals
     #
-    "AdminPrincipals": [],                       # Principals with "DAV:all" access (relative URLs)
-    "ReadPrincipals": [],                        # Principals with "DAV:read" access (relative URLs)
-    "EnableProxyPrincipals": True,               # Create "proxy access" principals
+    "AdminPrincipals": [], # Principals with "DAV:all" access (relative URLs)
+    "ReadPrincipals": [], # Principals with "DAV:read" access (relative URLs)
+    "EnableProxyPrincipals": True, # Create "proxy access" principals
 
     #
     # Permissions
     #
-    "EnableAnonymousReadRoot": True,    # Allow unauthenticated read access to /
-    "EnableAnonymousReadNav": False,    # Allow unauthenticated read access to hierarchy
-    "EnablePrincipalListings": True,    # Allow listing of principal collections
-    "EnableMonolithicCalendars": True,  # Render calendar collections as a monolithic iCalendar object
+    "EnableAnonymousReadRoot": True, # Allow unauthenticated read access to /
+    "EnableAnonymousReadNav": False, # Allow unauthenticated read access to hierarchy
+    "EnablePrincipalListings": True, # Allow listing of principal collections
+    "EnableMonolithicCalendars": True, # Render calendar collections as a monolithic iCalendar object
 
     #
     # Client controls
@@ -402,7 +404,7 @@
     # Authentication
     #
     "Authentication": {
-        "Basic": { "Enabled": False },     # Clear text; best avoided
+        "Basic": { "Enabled": False }, # Clear text; best avoided
         "Digest": {                        # Digest challenge/response
             "Enabled": True,
             "Algorithm": "md5",
@@ -427,11 +429,11 @@
     #
     # Logging
     #
-    "AccessLogFile"  : "access.log",  # Apache-style access log
-    "ErrorLogFile"   : "error.log",   # Server activity log
-    "ErrorLogEnabled"   : True,       # True = use log file, False = stdout
-    "ErrorLogRotateMB"  : 10,         # Rotate error log after so many megabytes
-    "ErrorLogMaxRotatedFiles"  : 5,   # Retain this many error log files
+    "AccessLogFile"  : "access.log", # Apache-style access log
+    "ErrorLogFile"   : "error.log", # Server activity log
+    "ErrorLogEnabled"   : True, # True = use log file, False = stdout
+    "ErrorLogRotateMB"  : 10, # Rotate error log after so many megabytes
+    "ErrorLogMaxRotatedFiles"  : 5, # Retain this many error log files
     "PIDFile"        : "caldavd.pid",
     "RotateAccessLog"   : False,
     "EnableExtendedAccessLog": True,
@@ -452,21 +454,21 @@
         "EnableTCPStatsSocket"   : False,
         "TCPStatsPort"           : 8100,
     },
-    
+
     "LogDatabase" : {
         "LabelsInSQL"            : False,
         "Statistics"             : False,
         "StatisticsLogFile"      : "sqlstats.log",
         "SQLStatements"          : False,
-        "TransactionWaitSeconds" : 0, 
+        "TransactionWaitSeconds" : 0,
     },
 
     #
     # SSL/TLS
     #
-    "SSLCertificate"     : "",  # Public key
-    "SSLPrivateKey"      : "",  # Private key
-    "SSLAuthorityChain"  : "",  # Certificate Authority Chain
+    "SSLCertificate"     : "", # Public key
+    "SSLPrivateKey"      : "", # Private key
+    "SSLAuthorityChain"  : "", # Certificate Authority Chain
     "SSLPassPhraseDialog": "/etc/apache2/getsslpassphrase",
     "SSLCertAdmin"       : "/Applications/Server.app/Contents/ServerRoot/usr/sbin/certadmin",
 
@@ -491,6 +493,15 @@
         },
     },
 
+    # How large a spawned process is allowed to get before it's stopped
+    "MemoryLimiter" : {
+        "Enabled" : True,
+        "Seconds" : 60, # How often to check memory sizes (in seconds)
+        "Bytes"   : 2 * 1024 * 1024 * 1024, # Memory limit (RSS in bytes)
+        "ResidentOnly" : True,  # True: only take into account resident memory;
+                                # False: include virtual memory
+    },
+
     #
     # Service ACLs
     #
@@ -501,11 +512,11 @@
     #
     # Standard (or draft) WebDAV extensions
     #
-    "EnableAddMember"             : True,  # POST ;add-member extension
-    "EnableSyncReport"            : True,  # REPORT collection-sync
-    "EnableSyncReportHome"        : True,  # REPORT collection-sync on home collections
-    "EnableWellKnown"             : True,  # /.well-known resource
-    "EnableCalendarQueryExtended" : True,  # Extended calendar-query REPORT
+    "EnableAddMember"             : True, # POST ;add-member extension
+    "EnableSyncReport"            : True, # REPORT collection-sync
+    "EnableSyncReportHome"        : True, # REPORT collection-sync on home collections
+    "EnableWellKnown"             : True, # /.well-known resource
+    "EnableCalendarQueryExtended" : True, # Extended calendar-query REPORT
 
     #
     # Non-standard CalDAV extensions
@@ -513,50 +524,50 @@
     "EnableDropBox"           : False, # Calendar Drop Box
     "EnablePrivateEvents"     : False, # Private Events
     "EnableTimezoneService"   : False, # Old Timezone service
-    
+
     "TimezoneService"         : {    # New standard timezone service
-        "Enabled"       : False,     # Overall on/off switch
+        "Enabled"       : False, # Overall on/off switch
         "Mode"          : "primary", # Can be "primary" or "secondary"
-        "BasePath"      : "",        # Path to zoneinfo - if None use default package path
+        "BasePath"      : "", # Path to zoneinfo - if None use default package path
                                      # secondary service MUST define its own writable path
-        "XMLInfoPath"   : "",        # Path to db cache info - if None use default package path
+        "XMLInfoPath"   : "", # Path to db cache info - if None use default package path
                                      # secondary service MUST define its own writable path if
                                      # not None
-        
+
         "SecondaryService" : {
             # Only one of these should be used when a secondary service is used
-            "Host"                  : "",        # Domain/IP of secondary service to discover
-            "URI"                   : "",        # HTTP(s) URI to secondary service
+            "Host"                  : "", # Domain/IP of secondary service to discover
+            "URI"                   : "", # HTTP(s) URI to secondary service
 
             "UpdateIntervalMinutes" : 24 * 60,
         }
     },
-    
+
     "EnableTimezonesByReference" : False, # Strip out VTIMEZONES that are known
 
-    "EnableBatchUpload"       : True,     # POST batch uploads
-    "MaxResourcesBatchUpload" : 100,      # Maximum number of resources in a batch POST
+    "EnableBatchUpload"       : True, # POST batch uploads
+    "MaxResourcesBatchUpload" : 100, # Maximum number of resources in a batch POST
     "MaxBytesBatchUpload"     : 10485760, # Maximum size of a batch POST (10 MB)
-    
+
     "Sharing": {
         "Enabled"             : False, # Overall on/off switch
         "AllowExternalUsers"  : False, # External (non-principal) sharees allowed
 
         "Calendars" : {
-            "Enabled"         : True,  # Calendar on/off switch
+            "Enabled"         : True, # Calendar on/off switch
         },
         "AddressBooks" : {
-            "Enabled"         : True,  # Address Books on/off switch
-        }        
+            "Enabled"         : True, # Address Books on/off switch
+        }
     },
-    
-    "RestrictCalendarsToOneComponentType" : True,  # Only allow calendars to be created with a single component type
+
+    "RestrictCalendarsToOneComponentType" : True, # Only allow calendars to be created with a single component type
                                                    # If this is on, it will also trigger an upgrade behavior that will
                                                    # split existing calendars into multiples based on component type.
                                                    # If on, it will also cause new accounts to provision with separate
                                                    # calendars for events and tasks.
 
-    "ParallelUpgrades" : False,    # Perform upgrades - currently only the
+    "ParallelUpgrades" : False, # Perform upgrades - currently only the
                                    # database -> filesystem migration - but in
                                    # the future, hopefully all relevant
                                    # upgrades - in parallel in subprocesses.
@@ -566,23 +577,23 @@
                             # the database, merge the data from the filesystem
                             # into the database homes.
 
-    "EnableDefaultAlarms" :  True, # Support for default alarms generated by the server
-    "RemoveDuplicateAlarms": True,  # Remove duplicate alarms on PUT
+    "EnableDefaultAlarms" : True, # Support for default alarms generated by the server
+    "RemoveDuplicateAlarms": True, # Remove duplicate alarms on PUT
 
     # CardDAV Features
     "DirectoryAddressBook": {
         "Enabled": True,
-        "type":    "twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService",
-        "params":  directoryAddressBookBackingServiceDefaultParams["twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService"],
-        "name":    "directory",
+        "type": "twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService",
+        "params": directoryAddressBookBackingServiceDefaultParams["twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService"],
+        "name": "directory",
         "MaxQueryResults": 1000,
     },
     "EnableSearchAddressBook": False, # /directory resource exists
     "AnonymousDirectoryAddressBookAccess": False, # Anonymous users may access directory address book
 
     "GlobalAddressBook": {
-        "Enabled":                   False,
-        "Name":                      "global-addressbook",
+        "Enabled": False,
+        "Name": "global-addressbook",
         "EnableAnonymousReadAccess": False,
     },
 
@@ -598,20 +609,35 @@
     #
 
     "Scheduling": {
-        
+
         "CalDAV": {
-            "EmailDomain"                : "",    # Domain for mailto calendar user addresses on this server
-            "HTTPDomain"                 : "",    # Domain for http calendar user addresses on this server
-            "AddressPatterns"            : [],    # Regex patterns to match local calendar user addresses
-            "OldDraftCompatibility"      : True,  # Whether to maintain compatibility with non-implicit mode
-            "ScheduleTagCompatibility"   : True,  # Whether to support older clients that do not use Schedule-Tag feature
-            "EnablePrivateComments"      : True,  # Private comments from attendees to organizer
+            "EmailDomain"                : "", # Domain for mailto calendar user addresses on this server
+            "HTTPDomain"                 : "", # Domain for http calendar user addresses on this server
+            "AddressPatterns"            : [], # Regex patterns to match local calendar user addresses
+            "OldDraftCompatibility"      : True, # Whether to maintain compatibility with non-implicit mode
+            "ScheduleTagCompatibility"   : True, # Whether to support older clients that do not use Schedule-Tag feature
+            "EnablePrivateComments"      : True, # Private comments from attendees to organizer
         },
 
         "iSchedule": {
             "Enabled"          : False, # iSchedule protocol
-            "AddressPatterns"  : [],    # Reg-ex patterns to match iSchedule-able calendar user addresses
-            "Servers"          : "servertoserver.xml",    # iSchedule server configurations
+            "AddressPatterns"  : [], # Reg-ex patterns to match iSchedule-able calendar user addresses
+            "RemoteServers"    : "remoteservers.xml", # iSchedule server configurations
+            "DNSDebug"         : "", # File where a fake Bind zone exists for creating fake DNS results
+            "DKIM"             : {      # DKIM options
+                "Enabled"               : True, # DKIM signing/verification enabled
+                "Domain"                : "", # Domain for DKIM (defaults to ServerHostName)
+                "KeySelector"           : "ischedule", # Selector for public key
+                "SignatureAlgorithm"    : "rsa-sha256", # Signature algorithm (one of rsa-sha1 or rsa-sha256)
+                "UseDNSKey"             : True, # This server's public key stored in DNS
+                "UseHTTPKey"            : True, # This server's public key stored in HTTP /.well-known
+                "UsePrivateExchangeKey" : True, # This server's public key manually exchanged with others
+                "ExpireSeconds"         : 3600, # Expiration time for signature verification
+                "PrivateKeyFile"        : "", # File where private key is stored
+                "PublicKeyFile"         : "", # File where public key is stored
+                "PrivateExchanges"      : "", # Directory where private exchange public keys are stored
+                "ProtocolDebug"         : False, # Turn on protocol level debugging to return detailed information to the requestor
+            },
         },
 
         "iMIP": {
@@ -619,28 +645,28 @@
             "MailGatewayServer" : "localhost",
             "MailGatewayPort"   : 62310,
             "Username"          : "com.apple.calendarserver", # For account injecting replies
-            "Password"          : "",    # 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
-                "Address"       : "",    # 'From' address for server
+                "Server"        : "", # SMTP server to relay messages through
+                "Port"          : 587, # SMTP server port to relay messages through
+                "Address"       : "", # 'From' address for server
                 "UseSSL"        : True,
-                "Username"      : "",    # For account sending mail
-                "Password"      : "",    # For account sending mail
-                "SuppressionDays" : 7,   # Messages for events older than this may days are not sent
+                "Username"      : "", # For account sending mail
+                "Password"      : "", # For account sending mail
+                "SuppressionDays" : 7, # Messages for events older than this may days are not sent
             },
             "Receiving": {
-                "Server"        : "",    # Server to retrieve email messages from
-                "Port"          : 0,     # Server port to retrieve email messages from
+                "Server"        : "", # Server to retrieve email messages from
+                "Port"          : 0, # Server port to retrieve email messages from
                 "UseSSL"        : True,
-                "Type"          : "",    # Type of message access server: 'pop' or 'imap'
-                "PollingSeconds"    : 30,  # How often to fetch mail
-                "Username"      : "",    # For account receiving mail
-                "Password"      : "",    # For account receiving mail
+                "Type"          : "", # Type of message access server: 'pop' or 'imap'
+                "PollingSeconds"    : 30, # How often to fetch mail
+                "Username"      : "", # For account receiving mail
+                "Password"      : "", # For account receiving mail
             },
-            "AddressPatterns"   : [],    # Regex patterns to match iMIP-able calendar user addresses
+            "AddressPatterns"   : [], # Regex patterns to match iMIP-able calendar user addresses
             "MailTemplatesDirectory": "/Applications/Server.app/Contents/ServerRoot/usr/share/caldavd/share/email_templates", # Directory containing HTML templates for email invitations (invite.html, cancel.html)
             "MailIconsDirectory": "/Applications/Server.app/Contents/ServerRoot/usr/share/caldavd/share/date_icons", # Directory containing language-specific subdirectories containing date-specific icons for email invitations
             "InvitationDaysToLive" : 90, # How many days invitations are valid
@@ -650,23 +676,23 @@
             "AllowGroupAsOrganizer"               : False, # Allow groups to be Organizers
             "AllowLocationAsOrganizer"            : False, # Allow locations to be Organizers
             "AllowResourceAsOrganizer"            : False, # Allow resources to be Organizers
-            "LimitFreeBusyAttendees"              :  30,    # Maximum number of attendees to request freebusy for
-            "AttendeeRefreshBatch"                :   5,    # Number of attendees to do batched refreshes: 0 - no batching
-            "AttendeeRefreshBatchDelaySeconds"    :   5,    # Time after an iTIP REPLY for first batched attendee refresh
-            "AttendeeRefreshBatchIntervalSeconds" :   5,    # Time between attendee batch refreshes
-            "UIDLockTimeoutSeconds"               :  60,    # Time for implicit UID lock timeout
-            "UIDLockExpirySeconds"                : 300,    # Expiration time for UID lock,
-            "V1Compatibility"                     : False,  # Allow /path-based CUAs in scheduling replies
-            "PrincipalHostAliases"                : [],     # Hostnames matched in http(s) CUAs
-            
-            "DelegeteRichFreeBusy"                : True,   # Delegates can get extra info in a freebusy request
-            "RoomResourceRichFreeBusy"            : True,   # Any user can get extra info for rooms/resources in a freebusy request 
+            "LimitFreeBusyAttendees"              : 30, # Maximum number of attendees to request freebusy for
+            "AttendeeRefreshBatch"                : 5, # Number of attendees to do batched refreshes: 0 - no batching
+            "AttendeeRefreshBatchDelaySeconds"    : 5, # Time after an iTIP REPLY for first batched attendee refresh
+            "AttendeeRefreshBatchIntervalSeconds" : 5, # Time between attendee batch refreshes
+            "UIDLockTimeoutSeconds"               : 60, # Time for implicit UID lock timeout
+            "UIDLockExpirySeconds"                : 300, # Expiration time for UID lock,
+            "V1Compatibility"                     : False, # Allow /path-based CUAs in scheduling replies
+            "PrincipalHostAliases"                : [], # Hostnames matched in http(s) CUAs
 
+            "DelegeteRichFreeBusy"                : True, # Delegates can get extra info in a freebusy request
+            "RoomResourceRichFreeBusy"            : True, # Any user can get extra info for rooms/resources in a freebusy request
+
             "AutoSchedule" : {
-                "Enabled"                         : True,   # Auto-scheduling will never occur if set to False
-                "Always"                          : False,  # Override augments setting and always auto-schedule
-                "AllowUsers"                      : False,  # Allow auto-schedule for users
-                "DefaultMode"                     : "automatic",   # Default mode for auto-schedule processing, one of:
+                "Enabled"                         : True, # Auto-scheduling will never occur if set to False
+                "Always"                          : False, # Override augments setting and always auto-schedule
+                "AllowUsers"                      : False, # Allow auto-schedule for users
+                "DefaultMode"                     : "automatic", # Default mode for auto-schedule processing, one of:
                                                                    # "none"            - no auto-scheduling
                                                                    # "accept-always"   - always accept, ignore busy time
                                                                    # "decline-always"  - always decline, ignore free time
@@ -679,7 +705,7 @@
 
     "FreeBusyURL": {
         "Enabled"          : False, # Per-user free-busy-url protocol
-        "TimePeriod"       : 14,    # Number of days into the future to generate f-b data if no explicit time-range is specified
+        "TimePeriod"       : 14, # Number of days into the future to generate f-b data if no explicit time-range is specified
         "AnonymousAccess"  : False, # Allow anonymous read access to free-busy URL
     },
 
@@ -770,11 +796,11 @@
     # Support multiple hosts within a domain
     #
     "Servers" : {
-        "Enabled": False,                          # Multiple servers/partitions enabled or not
-        "ConfigFile": "servers.xml",               # File path for server information
-        "MaxClients": 5,                           # Pool size for connections to each partition
+        "Enabled": False, # Multiple servers/partitions enabled or not
+        "ConfigFile": "localservers.xml", # File path for server information
+        "MaxClients": 5, # Pool size for connections to each partition
     },
-    "ServerPartitionID": "",                       # Unique ID for this server's partition instance.
+    "ServerPartitionID": "", # Unique ID for this server's partition instance.
 
     #
     # Performance tuning
@@ -926,17 +952,17 @@
 
     "EnableKeepAlive": False,
 
-    "EnableResponseCache":  True,
+    "EnableResponseCache": True,
     "ResponseCacheTimeout": 30, # Minutes
 
-    "EnableFreeBusyCache":          True,
-    "FreeBusyCacheDaysBack":        7,
-    "FreeBusyCacheDaysForward":     12 * 7,
+    "EnableFreeBusyCache": True,
+    "FreeBusyCacheDaysBack": 7,
+    "FreeBusyCacheDaysForward": 12 * 7,
 
-    "FreeBusyIndexLowerLimitDays":  365,
+    "FreeBusyIndexLowerLimitDays": 365,
     "FreeBusyIndexExpandAheadDays": 365,
-    "FreeBusyIndexExpandMaxDays":   5 * 365,
-    "FreeBusyIndexDelayedExpand":   True,
+    "FreeBusyIndexExpandMaxDays": 5 * 365,
+    "FreeBusyIndexDelayedExpand": True,
 
     # Specify which opendirectory module to use:
     # "opendirectory" is PyOpenDirectory (the old one which uses
@@ -956,7 +982,7 @@
     # during migration
     "MigratedInboxDaysCutoff": 60,
 
-    "Includes": [],     # Other plists to parse after this one
+    "Includes": [], # Other plists to parse after this one
 }
 
 
@@ -979,7 +1005,7 @@
 
 
 class PListConfigProvider(ConfigProvider):
-    
+
     def loadConfig(self):
         configDict = {}
         if self._configFileName:
@@ -1002,12 +1028,14 @@
         try:
             configDict = parser.parse(open(filename))
         except (IOError, OSError):
-            log.err("Configuration file does not exist or is inaccessible: %s" % (filename, ))
-            raise ConfigurationError("Configuration file does not exist or is inaccessible: %s" % (filename, ))
+            log.err("Configuration file does not exist or is inaccessible: %s" % (filename,))
+            raise ConfigurationError("Configuration file does not exist or is inaccessible: %s" % (filename,))
         else:
             configDict = _cleanup(configDict, self._defaults)
         return configDict
 
+
+
 def _expandPath(path):
     if '$' in path:
         return path.replace('$', getfqdn())
@@ -1026,6 +1054,10 @@
     ("DataRoot", "AttachmentsRoot"),
     ("DataRoot", ("TimezoneService", "BasePath",)),
     ("ConfigRoot", "SudoersFile"),
+    ("ConfigRoot", ("Scheduling", "iSchedule", "DNSDebug",)),
+    ("ConfigRoot", ("Scheduling", "iSchedule", "DKIM", "PrivateKeyFile",)),
+    ("ConfigRoot", ("Scheduling", "iSchedule", "DKIM", "PublicKeyFile",)),
+    ("ConfigRoot", ("Scheduling", "iSchedule", "DKIM", "PrivateExchanges",)),
     ("LogRoot", "AccessLogFile"),
     ("LogRoot", "ErrorLogFile"),
     ("LogRoot", ("Postgres", "LogFile",)),
@@ -1053,7 +1085,7 @@
         if root in configDict:
             if isinstance(relativePath, str):
                 relativePath = (relativePath,)
-            
+
             inDict = configDict
             for segment in relativePath[:-1]:
                 if segment not in inDict:
@@ -1065,7 +1097,7 @@
             if inDict and lastPath in inDict:
                 previousAbsoluteName = ".absolute." + relativePath
                 previousRelativeName = ".relative." + relativePath
-    
+
                 # If we previously made the name absolute, and the name in the
                 # config is still the same absolute name that we made it, let's
                 # change it to be the relative name again.  (This is necessary
@@ -1086,6 +1118,7 @@
                 configDict[previousAbsoluteName] = newAbsolutePath
 
 
+
 def _updateHostName(configDict, reloading=False):
     if not configDict.ServerHostName:
         hostname = getfqdn()
@@ -1093,6 +1126,8 @@
             hostname = "localhost"
         configDict.ServerHostName = hostname
 
+
+
 def _preUpdateDirectoryService(configDict, items, reloading=False):
     # Special handling for directory services configs
     dsType = items.get("DirectoryService", {}).get("type", None)
@@ -1112,13 +1147,17 @@
     for param in items.get("DirectoryService", {}).get("params", {}):
         if dsType in DEFAULT_SERVICE_PARAMS and param not in DEFAULT_SERVICE_PARAMS[dsType]:
             log.warn("Parameter %s is not supported by service %s" % (param, dsType))
-            
+
+
+
 def _postUpdateDirectoryService(configDict, reloading=False):
     if configDict.DirectoryService.type in DEFAULT_SERVICE_PARAMS:
         for param in tuple(configDict.DirectoryService.params):
             if param not in DEFAULT_SERVICE_PARAMS[configDict.DirectoryService.type]:
                 del configDict.DirectoryService.params[param]
 
+
+
 def _preUpdateResourceService(configDict, items, reloading=False):
     # Special handling for directory services configs
     dsType = items.get("ResourceService", {}).get("type", None)
@@ -1138,7 +1177,9 @@
     for param in items.get("ResourceService", {}).get("params", {}):
         if dsType in DEFAULT_RESOURCE_PARAMS and param not in DEFAULT_RESOURCE_PARAMS[dsType]:
             log.warn("Parameter %s is not supported by service %s" % (param, dsType))
-            
+
+
+
 def _postUpdateResourceService(configDict, reloading=False):
     if configDict.ResourceService.type in DEFAULT_RESOURCE_PARAMS:
         for param in tuple(configDict.ResourceService.params):
@@ -1146,6 +1187,7 @@
                 del configDict.ResourceService.params[param]
 
 
+
 def _preUpdateDirectoryAddressBookBackingDirectoryService(configDict, items, reloading=False):
     #
     # Special handling for directory address book configs
@@ -1174,6 +1216,8 @@
         if param not in directoryAddressBookBackingServiceDefaultParams[configDict.DirectoryAddressBook.type]:
             del configDict.DirectoryAddressBook.params[param]
 
+
+
 def _postUpdateAugmentService(configDict, reloading=False):
     if configDict.AugmentService.type in DEFAULT_AUGMENT_PARAMS:
         for param in tuple(configDict.AugmentService.params):
@@ -1181,6 +1225,8 @@
                 log.warn("Parameter %s is not supported by service %s" % (param, configDict.AugmentService.type))
                 del configDict.AugmentService.params[param]
 
+
+
 def _postUpdateProxyDBService(configDict, reloading=False):
     if configDict.ProxyDBService.type in DEFAULT_PROXYDB_PARAMS:
         for param in tuple(configDict.ProxyDBService.params):
@@ -1188,6 +1234,8 @@
                 log.warn("Parameter %s is not supported by service %s" % (param, configDict.ProxyDBService.type))
                 del configDict.ProxyDBService.params[param]
 
+
+
 def _updateACLs(configDict, reloading=False):
     #
     # Base resource ACLs
@@ -1235,7 +1283,7 @@
         readOnlyACE(configDict.EnableAnonymousReadRoot),
 
         # Add inheritable all access for admins
-        *configDict.AdminACEs
+        * configDict.AdminACEs
     )
 
     log.debug("Root ACL: %s" % (configDict.RootResourceACL.toxml(),))
@@ -1245,7 +1293,7 @@
         readOnlyACE(configDict.EnableAnonymousReadNav),
 
         # Add read and read-acl access for admins
-        *[
+        * [
             davxml.ACE(
                 davxml.Principal(davxml.HRef(principal)),
                 davxml.Grant(
@@ -1283,6 +1331,8 @@
     except re.error, e:
         raise ConfigurationError("Invalid regular expression in RejectClients: %s" % (e,))
 
+
+
 def _updateLogLevels(configDict, reloading=False):
     clearLogLevels()
 
@@ -1299,6 +1349,8 @@
     except InvalidLogLevelError, e:
         raise ConfigurationError("Invalid log level: %s" % (e.level))
 
+
+
 def _updateNotifications(configDict, reloading=False):
     # Reloading not supported -- requires process running as root
     if reloading:
@@ -1349,7 +1401,6 @@
                     # 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["Enabled"]
@@ -1431,17 +1482,21 @@
                     log.info("iMIP %s password not found in keychain" %
                         (direction,))
 
+
+
 def _updateServers(configDict, reloading=False):
-    import servers
+    from twistedcaldav.scheduling.ischedule.localservers import Servers
     if configDict.Servers.Enabled:
-        servers.Servers.load()
-        servers.Servers.getThisServer().installReverseProxies(
+        Servers.load()
+        Servers.getThisServer().installReverseProxies(
             configDict.ServerPartitionID,
             configDict.Servers.MaxClients,
         )
     else:
-        servers.Servers.clear()
+        Servers.clear()
 
+
+
 def _updateCompliance(configDict, reloading=False):
 
     if configDict.EnableCalDAV:
@@ -1499,7 +1554,7 @@
     _updateServers,
     _updateCompliance,
     )
-    
+
 def _cleanup(configDict, defaultDict):
     cleanDict = copy.deepcopy(configDict)
 
@@ -1512,6 +1567,7 @@
             log.err("Ignoring unknown configuration option: %r" % (key,))
             del cleanDict[key]
 
+
     def deprecated(oldKey, newKey):
         log.err("Configuration option %r is deprecated in favor of %r." % (oldKey, newKey))
         if oldKey in configDict and newKey in configDict:
@@ -1520,6 +1576,7 @@
                 % (oldKey, newKey, newKey)
             )
 
+
     def renamed(oldKey, newKey):
         deprecated(oldKey, newKey)
         cleanDict[newKey] = configDict[oldKey]
@@ -1561,6 +1618,8 @@
     }
     return preserved
 
+
+
 def _restoreConfig(configDict, preserved):
     """
     Restore certain config keys across reset( ) because these can't be

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/storebridge.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/storebridge.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,5 +1,5 @@
 # -*- test-case-name: twistedcaldav.test.test_wrapping -*-
-##
+# #
 # Copyright (c) 2005-2012 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,7 @@
 # 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 time
 import hashlib
@@ -49,12 +49,12 @@
 )
 
 from twistedcaldav import customxml, carddavxml, caldavxml
-from twistedcaldav.cache import CacheStoreNotifier, ResponseCacheMixin,\
+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.ical import Component as VCalendar, Property as VProperty,\
+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
@@ -62,9 +62,9 @@
 from twistedcaldav.notifications import (
     NotificationCollectionResource, NotificationResource
 )
-from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource,\
+from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource, \
     DefaultAlarmPropertyMixin
-from twistedcaldav.schedule import ScheduleInboxResource
+from twistedcaldav.scheduling.caldav.resource import ScheduleInboxResource
 from twistedcaldav.scheduling.implicit import ImplicitScheduler
 from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
 
@@ -91,6 +91,7 @@
         """
         self._newPropertyStore = newPropertyStore
 
+
     @classmethod
     def _convertKey(cls, qname):
         namespace, name = qname
@@ -115,7 +116,6 @@
                 FORBIDDEN,
                 "Property cannot be changed: %s" % (property.sname(),)
             ))
-            
 
 
     def delete(self, qname):
@@ -171,6 +171,7 @@
     def exists(self):
         return self._newStoreObject is not None
 
+
     def name(self):
         return self._newStoreObject.name() if self._newStoreObject is not None else self._name
 
@@ -232,7 +233,7 @@
     def liveProperties(self):
 
         props = super(_CommonHomeChildCollectionMixin, self).liveProperties()
-        
+
         if config.MaxResourcesPerCollection:
             props += (customxml.MaxResources.qname(),)
 
@@ -241,6 +242,7 @@
 
         return props
 
+
     @inlineCallbacks
     def readProperty(self, property, request):
         if type(property) is tuple:
@@ -253,12 +255,15 @@
 
         returnValue((yield super(_CommonHomeChildCollectionMixin, self).readProperty(property, request)))
 
+
     def url(self):
         return joinURL(self._parentResource.url(), self._name, "/")
 
+
     def parentResource(self):
         return self._parentResource
 
+
     def index(self):
         """
         Retrieve the new-style index wrapper.
@@ -288,19 +293,20 @@
 
         if self._newStoreObject:
             newStoreObject = yield self._newStoreObject.objectResourceWithName(name)
-    
+
             similar = self._childClass(
                 newStoreObject,
                 self._newStoreObject,
                 name,
                 principalCollections=self._principalCollections
             )
-    
+
             self.propagateTransaction(similar)
             returnValue(similar)
         else:
             returnValue(NoParent())
 
+
     @inlineCallbacks
     def listChildren(self):
         """
@@ -310,12 +316,14 @@
         children.update((yield self._newStoreObject.listObjectResources()))
         returnValue(sorted(children))
 
+
     def countChildren(self):
         """
         @return: L{Deferred} with the count of all known children of this resource.
         """
         return self._newStoreObject.countObjectResources()
 
+
     def name(self):
         return self._name
 
@@ -343,6 +351,7 @@
     def getInternalSyncToken(self):
         return self._newStoreObject.syncToken() if self._newStoreObject else None
 
+
     @inlineCallbacks
     def findChildrenFaster(
         self, depth, request, okcallback, badcallback, missingcallback,
@@ -351,36 +360,38 @@
         """
         Override to pre-load children in certain collection types for better performance.
         """
-        
+
         if depth == "1":
             if names:
                 yield self._newStoreObject.objectResourcesWithNames(names)
             else:
                 yield self._newStoreObject.objectResources()
-        
+
         result = (yield super(_CommonHomeChildCollectionMixin, self).findChildrenFaster(
             depth, request, okcallback, badcallback, missingcallback, names, privileges, inherited_aces
         ))
-        
+
         returnValue(result)
-    
+
+
     @inlineCallbacks
     def createCollection(self):
         """
         Override C{createCollection} to actually do the work.
         """
         self._newStoreObject = (yield self._newStoreParentHome.createChildWithName(self._name))
-        
+
         # Re-initialize to get stuff setup again now we have a "real" object
         self._initializeWithHomeChild(self._newStoreObject, self._parentResource)
 
         returnValue(CREATED)
 
+
     @requiresPermissions(fromParent=[davxml.Unbind()])
     @inlineCallbacks
     def http_DELETE(self, request):
         """
-        Override http_DELETE to validate 'depth' header. 
+        Override http_DELETE to validate 'depth' header.
         """
 
         if not self.exists():
@@ -397,6 +408,7 @@
         response = (yield self.storeRemove(request, True, request.uri))
         returnValue(response)
 
+
     @inlineCallbacks
     def storeRemove(self, request, viaRequest, where):
         """
@@ -464,7 +476,7 @@
 
         # Actually delete it.
         yield self._newStoreObject.remove()
-        
+
         # Re-initialize to get stuff setup again now we have no object
         self._initializeWithHomeChild(None, self._parentResource)
 
@@ -478,6 +490,7 @@
 
         returnValue(response)
 
+
     def http_COPY(self, request):
         """
         Copying of calendar collections isn't allowed.
@@ -506,7 +519,7 @@
         destination = yield request.locateResource(destinationURI)
         if destination.exists():
             returnValue(FORBIDDEN)
-            
+
         # Forget the destination now as after the move we will need to re-init it with its
         # new store object
         request._forgetResource(destination, destinationURI)
@@ -516,6 +529,7 @@
         yield self._newStoreObject.rename(basename)
         returnValue(NO_CONTENT)
 
+
     @inlineCallbacks
     def _readGlobalProperty(self, qname, property, request):
 
@@ -534,6 +548,7 @@
             result = (yield super(_CommonHomeChildCollectionMixin, self)._readGlobalProperty(qname, property, request))
             returnValue(result)
 
+
     @inlineCallbacks
     def checkCTagPrecondition(self, request):
         if request.headers.hasHeader("If"):
@@ -547,6 +562,7 @@
                 if testctag != ctag:
                     raise HTTPError(StatusResponse(responsecode.PRECONDITION_FAILED, "CTag pre-condition failure"))
 
+
     def checkReturnChanged(self, request):
         if request.headers.hasHeader("X-MobileMe-DAV-Options"):
             return_changed = request.headers.getRawHeaders("X-MobileMe-DAV-Options")[0]
@@ -554,34 +570,35 @@
         else:
             return False
 
+
     @requiresPermissions(davxml.Bind())
     @inlineCallbacks
     def simpleBatchPOST(self, request):
-        
+
         # If CTag precondition
         yield self.checkCTagPrecondition(request)
-        
+
         # Look for return changed data option
         return_changed = self.checkReturnChanged(request)
 
         # Read in all data
         data = (yield allDataFromStream(request.stream))
-        
+
         components = self.componentsFromData(data)
         if components is None:
             raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body"))
-        
+
         # Build response
         xmlresponses = []
         for ctr, component in enumerate(components):
-            
+
             code = None
             error = None
             dataChanged = None
             try:
                 # Create a new name if one was not provided
-                name =  md5(str(ctr) + component.resourceUID() + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
-            
+                name = md5(str(ctr) + component.resourceUID() + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
+
                 # Get a resource for the new item
                 newchildURL = joinURL(request.path, name)
                 newchild = (yield request.locateResource(newchildURL))
@@ -595,9 +612,9 @@
                     error = (error.namespace, error.name,)
             except Exception:
                 code = responsecode.BAD_REQUEST
-            
+
             if code is None:
-                
+
                 etag = (yield newchild.etag())
                 if not return_changed or dataChanged is None:
                     xmlresponses.append(
@@ -625,7 +642,7 @@
                             )
                         )
                     )
-                
+
             else:
                 xmlresponses.append(
                     davxml.StatusResponse(
@@ -637,9 +654,9 @@
                     ) if error else None,
                     )
                 )
-        
+
         result = MultiStatusResponse(xmlresponses)
-        
+
         newctag = (yield self.getInternalSyncToken())
         result.headers.setRawHeaders("CTag", (newctag,))
 
@@ -650,16 +667,17 @@
         request.extendedLogItems["rcount"] = len(xmlresponses)
 
         returnValue(result)
-        
+
+
     @inlineCallbacks
     def crudBatchPOST(self, request, xmlroot):
-        
+
         # Need to force some kind of overall authentication on the request
         yield self.authorize(request, (davxml.Read(), davxml.Write(),))
 
         # If CTag precondition
         yield self.checkCTagPrecondition(request)
-        
+
         # Look for return changed data option
         return_changed = self.checkReturnChanged(request)
 
@@ -671,7 +689,7 @@
         updateCount = 0
         deleteCount = 0
         for xmlchild in xmlroot.children:
-            
+
             # Determine the multiput operation: create, update, delete
             href = xmlchild.childOfType(davxml.HRef.qname())
             set = xmlchild.childOfType(davxml.Set.qname())
@@ -679,11 +697,11 @@
             xmldata_root = prop if prop else set
             xmldata = xmldata_root.childOfType(self.xmlDataElementType().qname()) if xmldata_root is not None else None
             if href is None:
-                
+
                 if xmldata is None:
                     raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body without a DAV:Href present"))
-                
-                # Do privilege check on collection once 
+
+                # Do privilege check on collection once
                 if checkedBindPrivelege is None:
                     try:
                         yield self.authorize(request, (davxml.Bind(),))
@@ -707,7 +725,7 @@
                     yield self.crudUpdate(request, str(href), xmldata, ifmatch, return_changed, xmlresponses)
                     updateCount += 1
                 else:
-                    # Do privilege check on collection once 
+                    # Do privilege check on collection once
                     if checkedUnbindPrivelege is None:
                         try:
                             yield self.authorize(request, (davxml.Unbind(),))
@@ -715,11 +733,11 @@
                         except HTTPError, e:
                             checkedUnbindPrivelege = e
 
-                    yield self.crudDelete(request, str(href), ifmatch, xmlresponses, checkedUnbindPrivelege);
+                    yield self.crudDelete(request, str(href), ifmatch, xmlresponses, checkedUnbindPrivelege)
                     deleteCount += 1
-        
+
         result = MultiStatusResponse(xmlresponses)
-        
+
         newctag = (yield self.getInternalSyncToken())
         result.headers.setRawHeaders("CTag", (newctag,))
 
@@ -737,9 +755,10 @@
 
         returnValue(result)
 
+
     @inlineCallbacks
     def crudCreate(self, request, xmldata, xmlresponses, return_changed, hasPrivilege):
-        
+
         code = None
         error = None
         try:
@@ -750,8 +769,8 @@
             component = xmldata.generateComponent()
 
             # Create a new name if one was not provided
-            name =  md5(str(componentdata) + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
-        
+            name = md5(str(componentdata) + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
+
             # Get a resource for the new item
             newchildURL = joinURL(request.path, name)
             newchild = (yield request.locateResource(newchildURL))
@@ -768,7 +787,7 @@
 
         except Exception:
             code = responsecode.BAD_REQUEST
-        
+
         if code is None:
             etag = (yield newchild.etag())
             xmlresponses.append(
@@ -795,6 +814,7 @@
                 )
             )
 
+
     @inlineCallbacks
     def crudUpdate(self, request, href, xmldata, ifmatch, return_changed, xmlresponses):
         code = None
@@ -814,7 +834,7 @@
             etag = (yield updateResource.etag())
             if ifmatch and ifmatch != etag.generate():
                 raise HTTPError(responsecode.PRECONDITION_FAILED)
-            
+
             yield self.storeResourceData(request, updateResource, href, component, componentdata)
 
             # FIXME: figure out return_changed behavior
@@ -828,7 +848,7 @@
 
         except Exception:
             code = responsecode.BAD_REQUEST
-        
+
         if code is None:
             xmlresponses.append(
                 davxml.PropertyStatusResponse(
@@ -852,6 +872,7 @@
                 )
             )
 
+
     @inlineCallbacks
     def crudDelete(self, request, href, ifmatch, xmlresponses, hasPrivilege):
         code = None
@@ -870,7 +891,7 @@
                 raise HTTPError(responsecode.PRECONDITION_FAILED)
 
             yield deleteResource.storeRemove(
-                request, 
+                request,
                 True,
                 href,
             )
@@ -884,7 +905,7 @@
 
         except Exception:
             code = responsecode.BAD_REQUEST
-        
+
         if code is None:
             xmlresponses.append(
                 davxml.StatusResponse(
@@ -907,14 +928,17 @@
     def notifierID(self, label="default"):
         self._newStoreObject.notifierID(label)
 
+
     def notifyChanged(self):
         return self._newStoreObject.notifyChanged()
 
+
+
 class _CalendarCollectionBehaviorMixin():
     """
     Functions common to calendar and inbox collections
     """
-    
+
     # Support component set behaviors
     def setSupportedComponentSet(self, support_components_property):
         """
@@ -922,7 +946,8 @@
         """
         support_components = tuple([comp.attributes["name"].upper() for comp in support_components_property.children])
         return self.setSupportedComponents(support_components)
-    
+
+
     def getSupportedComponentSet(self):
         comps = self._newStoreObject.getSupportedComponents()
         if comps:
@@ -933,6 +958,7 @@
             *[caldavxml.CalendarComponent(name=item) for item in comps]
         )
 
+
     def setSupportedComponents(self, components):
         """
         Set the allowed component set for this calendar.
@@ -940,14 +966,15 @@
         @param components: list of names of components to support
         @type components: C{list}
         """
-        
+
         # Validate them first - raise on failure
         if not self.validSupportedComponents(components):
             raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Invalid CALDAV:supported-calendar-component-set"))
 
         support_components = ",".join(sorted([comp.upper() for comp in components]))
         return maybeDeferred(self._newStoreObject.setSupportedComponents, support_components)
-    
+
+
     def getSupportedComponents(self):
         comps = self._newStoreObject.getSupportedComponents()
         if comps:
@@ -956,9 +983,11 @@
             comps = allowedComponents
         return comps
 
+
     def isSupportedComponent(self, componentType):
         return self._newStoreObject.isSupportedComponent(componentType)
 
+
     def validSupportedComponents(self, components):
         """
         Test whether the supplied set of components is valid for the current server's component set
@@ -968,13 +997,13 @@
             return components in (("VEVENT",), ("VTODO",),)
         return True
 
-    
+
+
 class CalendarCollectionResource(DefaultAlarmPropertyMixin, _CalendarCollectionBehaviorMixin, _CommonHomeChildCollectionMixin, CalDAVResource):
     """
     Wrapper around a L{txdav.caldav.icalendar.ICalendar}.
     """
 
- 
     def __init__(self, calendar, home, name=None, *args, **kw):
         """
         Create a CalendarCollectionResource from a L{txdav.caldav.icalendar.ICalendar}
@@ -990,6 +1019,7 @@
             self._postHandlers[("text", "calendar")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
             self.xmlDocHandlers[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST
 
+
     def __repr__(self):
         return "<Calendar Collection Resource %r:%r %s>" % (
             self._newStoreParentHome.uid(),
@@ -1008,6 +1038,7 @@
         """
         return True
 
+
     @inlineCallbacks
     def iCalendarRolledup(self, request):
         # FIXME: uncached: implement cache in the storage layer
@@ -1031,7 +1062,7 @@
         isowner = (yield self.isOwner(request))
         accessPrincipal = (yield self.resourceOwnerPrincipal(request))
 
-        for name, uid, type in (yield maybeDeferred(self.index().bruteForceSearch)): #@UnusedVariable
+        for name, uid, type in (yield maybeDeferred(self.index().bruteForceSearch)):  # @UnusedVariable
             try:
                 child = yield request.locateChildResource(self, name)
             except TypeError:
@@ -1052,7 +1083,7 @@
                 assert subcalendar.name() == "VCALENDAR"
 
                 for component in subcalendar.subcomponents():
-                    
+
                     # Only insert VTIMEZONEs once
                     if component.name() == "VTIMEZONE":
                         tzid = component.propertyValue("TZID")
@@ -1068,21 +1099,21 @@
 
         returnValue(calendar)
 
-
     createCalendarCollection = _CommonHomeChildCollectionMixin.createCollection
 
+
     @classmethod
     def componentsFromData(cls, data):
         """
         Need to split a single VCALENDAR into separate ones based on UID with the
         appropriate VTIEMZONES included.
         """
-        
+
         results = []
 
         # Split into components by UID and TZID
         try:
-            vcal =  VCalendar.fromString(data)
+            vcal = VCalendar.fromString(data)
         except InvalidICalendarDataError:
             return None
 
@@ -1093,14 +1124,14 @@
                 by_tzid[subcomponent.propertyValue("TZID")] = subcomponent
             else:
                 by_uid.setdefault(subcomponent.propertyValue("UID"), []).append(subcomponent)
-        
+
         # Re-constitute as separate VCALENDAR objects
         for components in by_uid.values():
-            
+
             newvcal = VCalendar("VCALENDAR")
             newvcal.addProperty(VProperty("VERSION", "2.0"))
             newvcal.addProperty(VProperty("PRODID", vcal.propertyValue("PRODID")))
-            
+
             # Get the set of TZIDs and include them
             tzids = set()
             for component in components:
@@ -1113,39 +1144,42 @@
                     # We ignore the error and generate invalid ics which someone will
                     # complain about at some point
                     pass
-            
+
             # Now add each component
             for component in components:
                 newvcal.addComponent(component.duplicate())
- 
+
             results.append(newvcal)
-        
+
         return results
 
+
     @classmethod
     def resourceSuffix(cls):
         return ".ics"
 
+
     @classmethod
     def xmlDataElementType(cls):
         return caldavxml.CalendarData
 
+
     @inlineCallbacks
     def storeResourceData(self, request, newchild, newchildURL, component, returnData=False):
         storer = StoreCalendarObjectResource(
-            request = request,
-            destination = newchild,
-            destination_uri = newchildURL,
-            destinationcal = True,
-            destinationparent = self,
-            calendar = component,
-            returnData = returnData,
+            request=request,
+            destination=newchild,
+            destination_uri=newchildURL,
+            destinationcal=True,
+            destinationparent=self,
+            calendar=component,
+            returnData=returnData,
         )
         yield storer.run()
-        
+
         returnValue(storer.storeddata if hasattr(storer, "storeddata") else None)
-            
 
+
     @inlineCallbacks
     def storeRemove(self, request, implicitly, where):
         """
@@ -1206,7 +1240,7 @@
         that calendar's name.
         """
         defaultCalendarType = (yield self.isDefaultCalendar(request))
-        
+
         result = (yield super(CalendarCollectionResource, self).http_MOVE(request))
         if result == NO_CONTENT:
             destinationURI = urlsplit(request.headers.getHeader("destination"))[2]
@@ -1216,6 +1250,7 @@
         returnValue(result)
 
 
+
 class StoreScheduleInboxResource(_CalendarCollectionBehaviorMixin, _CommonHomeChildCollectionMixin, ScheduleInboxResource):
 
     def __init__(self, *a, **kw):
@@ -1249,19 +1284,24 @@
     def provisionFile(self):
         pass
 
+
     def provision(self):
         pass
 
+
     def http_DELETE(self, request):
         return FORBIDDEN
 
+
     def http_COPY(self, request):
         return FORBIDDEN
 
+
     def http_MOVE(self, request):
         return FORBIDDEN
 
 
+
 class _GetChildHelper(CalDAVResource):
 
     def locateChild(self, request, segments):
@@ -1330,7 +1370,7 @@
 
 
     def resourceType(self,):
-        return davxml.ResourceType.dropboxhome #@UndefinedVariable
+        return davxml.ResourceType.dropboxhome  # @UndefinedVariable
 
 
     def listChildren(self):
@@ -1378,7 +1418,7 @@
 
 
     def resourceType(self):
-        return davxml.ResourceType.dropbox #@UndefinedVariable
+        return davxml.ResourceType.dropbox  # @UndefinedVariable
 
 
     @inlineCallbacks
@@ -1522,6 +1562,7 @@
 
         returnValue(davxml.ACL(*tuple(originalACEs + newACEs)))
 
+
     @inlineCallbacks
     def sharedDropboxACEs(self):
 
@@ -1577,6 +1618,7 @@
         returnValue(aces)
 
 
+
 class CalendarAttachment(_NewStoreFileMetaDataHelper, _GetChildHelper):
 
     def __init__(self, calendarObject, attachment, attachmentName, **kw):
@@ -1611,8 +1653,6 @@
             yield readStream(request.stream, t.write)
         except Exception, e:
             log.error("Unable to store attachment: %s" % (e,))
-            # Signal to abort in twistedcaldav.resource.CalDAVResource.RenderHTTP
-            self.transactionError()
             raise HTTPError(SERVICE_UNAVAILABLE)
 
         try:
@@ -1634,6 +1674,8 @@
 
         stream = ProducerStream()
         class StreamProtocol(Protocol):
+            def connectionMade(self):
+                stream.registerProducer(self.transport, False)
             def dataReceived(self, data):
                 stream.write(data)
             def connectionLost(self, reason):
@@ -1643,8 +1685,7 @@
         except IOError, e:
             log.error("Unable to read attachment: %s, due to: %s" % (self, e,))
             raise HTTPError(responsecode.NOT_FOUND)
-            
-        return Response(OK, {"content-type":self.contentType()}, stream)
+        return Response(OK, {"content-type": self.contentType()}, stream)
 
 
     @requiresPermissions(fromParent=[davxml.Unbind()])
@@ -1660,30 +1701,37 @@
         self._newStoreAttachment = self._newStoreCalendarObject = None
         returnValue(NO_CONTENT)
 
-
     http_MKCOL = None
     http_MKCALENDAR = None
 
+
     def http_PROPPATCH(self, request):
         """
-        No dead properties allowed on attachments. 
+        No dead properties allowed on attachments.
         """
         return FORBIDDEN
 
+
     def isCollection(self):
         return False
 
 
+
 class NoParent(CalDAVResource):
+
     def http_MKCALENDAR(self, request):
         return CONFLICT
 
+
     def http_PUT(self, request):
         return CONFLICT
 
+
     def isCollection(self):
         return False
 
+
+
 class _CommonObjectResource(_NewStoreFileMetaDataHelper, CalDAVResource, FancyEqMixin):
 
     _componentFromStream = None
@@ -1720,6 +1768,7 @@
     def component(self):
         return self._newStoreObject.component()
 
+
     @inlineCallbacks
     def render(self, request):
         if not self.exists():
@@ -1736,7 +1785,7 @@
     @requiresPermissions(fromParent=[davxml.Unbind()])
     def http_DELETE(self, request):
         """
-        Override http_DELETE to validate 'depth' header. 
+        Override http_DELETE to validate 'depth' header.
         """
         if not self.exists():
             log.debug("Resource not found: %s" % (self,))
@@ -1744,15 +1793,17 @@
 
         return self.storeRemove(request, True, request.uri)
 
+
     def http_PROPPATCH(self, request):
         """
-        No dead properties allowed on object resources. 
+        No dead properties allowed on object resources.
         """
         if self._newStoreParent.objectResourcesHaveProperties():
             return super(_CommonObjectResource, self).http_PROPPATCH(request)
         else:
             return FORBIDDEN
 
+
     @inlineCallbacks
     def storeStream(self, stream):
 
@@ -1773,6 +1824,7 @@
 
             returnValue(CREATED)
 
+
     @inlineCallbacks
     def storeComponent(self, component):
 
@@ -1790,7 +1842,6 @@
             returnValue(CREATED)
 
 
-
     @inlineCallbacks
     def storeRemove(self, request, implicitly, where):
         """
@@ -1850,13 +1901,13 @@
 
 class _CalendarObjectMetaDataMixin(object):
     """
-    Dynamically create the required meta-data for an object resource 
+    Dynamically create the required meta-data for an object resource
     """
 
-    accessMode        = _MetadataProperty("accessMode")
-    isScheduleObject  = _MetadataProperty("isScheduleObject")
-    scheduleTag       = _MetadataProperty("scheduleTag")
-    scheduleEtags     = _MetadataProperty("scheduleEtags")
+    accessMode = _MetadataProperty("accessMode")
+    isScheduleObject = _MetadataProperty("isScheduleObject")
+    scheduleTag = _MetadataProperty("scheduleTag")
+    scheduleEtags = _MetadataProperty("scheduleEtags")
     hasPrivateComment = _MetadataProperty("hasPrivateComment")
 
 
@@ -2011,6 +2062,7 @@
         returnValue(NO_CONTENT)
 
 
+
 class AddressBookCollectionResource(_CommonHomeChildCollectionMixin, CalDAVResource):
     """
     Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.
@@ -2031,6 +2083,7 @@
             self._postHandlers[("text", "vcard")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
             self.xmlDocHandlers[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST
 
+
     def __repr__(self):
         return "<AddressBook Collection Resource %r:%r %s>" % (
             self._newStoreParentHome.uid(),
@@ -2049,9 +2102,9 @@
         """
         return True
 
-
     createAddressBookCollection = _CommonHomeChildCollectionMixin.createCollection
 
+
     @classmethod
     def componentsFromData(cls, data):
         try:
@@ -2059,30 +2112,34 @@
         except InvalidVCardDataError:
             return None
 
+
     @classmethod
     def resourceSuffix(cls):
         return ".vcf"
 
+
     @classmethod
     def xmlDataElementType(cls):
         return carddavxml.AddressData
 
+
     @inlineCallbacks
     def storeResourceData(self, request, newchild, newchildURL, component, returnData=False):
         storer = StoreAddressObjectResource(
-            request = request,
-            sourceadbk = False,
-            destination = newchild,
-            destination_uri = newchildURL,
-            destinationadbk = True,
-            destinationparent = self,
-            vcard = component,
-            returnData = returnData,
+            request=request,
+            sourceadbk=False,
+            destination=newchild,
+            destination_uri=newchildURL,
+            destinationadbk=True,
+            destinationparent=self,
+            vcard=component,
+            returnData=returnData,
         )
         yield storer.run()
-        
+
         returnValue(storer.returndata if hasattr(storer, "returndata") else None)
 
+
     @inlineCallbacks
     def storeRemove(self, request, viaRequest, where):
         """
@@ -2130,6 +2187,7 @@
 
         returnValue(response)
 
+
     # FIXME: access control
     @inlineCallbacks
     def http_MOVE(self, request):
@@ -2138,7 +2196,7 @@
         that address book's name.
         """
         defaultAddressBook = (yield self.isDefaultAddressBook(request))
-        
+
         result = (yield super(AddressBookCollectionResource, self).http_MOVE(request))
         if result == NO_CONTENT:
             destinationURI = urlsplit(request.headers.getHeader("destination"))[2]
@@ -2147,12 +2205,16 @@
                                destination, destinationURI)
         returnValue(result)
 
+
+
 class GlobalAddressBookCollectionResource(GlobalAddressBookResource, AddressBookCollectionResource):
     """
     Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.
     """
     pass
 
+
+
 class AddressBookObjectResource(_CommonObjectResource):
     """
     A resource wrapping a addressbook object.
@@ -2172,6 +2234,7 @@
     vCard = _CommonObjectResource.component
 
 
+
 class _NotificationChildHelper(object):
     """
     Methods for things which are like notification objects.
@@ -2261,9 +2324,11 @@
     def name(self):
         return "notification"
 
+
     def url(self):
         return joinURL(self._parentResource.url(), self.name(), "/")
 
+
     @inlineCallbacks
     def listChildren(self):
         l = []
@@ -2271,6 +2336,7 @@
             l.append(notification.name())
         returnValue(l)
 
+
     def isCollection(self):
         return True
 
@@ -2302,6 +2368,7 @@
         )
 
 
+
 class StoreNotificationObjectFile(_NewStoreFileMetaDataHelper, NotificationResource):
     """
     A resource wrapping a calendar object.
@@ -2329,6 +2396,7 @@
         props += (customxml.NotificationType.qname(),)
         return props
 
+
     @inlineCallbacks
     def readProperty(self, property, request):
         if type(property) is tuple:
@@ -2341,6 +2409,7 @@
 
         returnValue((yield super(StoreNotificationObjectFile, self).readProperty(property, request)))
 
+
     def isCollection(self):
         return False
 
@@ -2362,7 +2431,7 @@
             raise HTTPError(responsecode.NOT_FOUND)
 
         returnValue(
-            Response(OK, {"content-type":self.contentType()},
+            Response(OK, {"content-type": self.contentType()},
                      MemoryStream((yield self.text())))
         )
 
@@ -2370,7 +2439,7 @@
     @requiresPermissions(fromParent=[davxml.Unbind()])
     def http_DELETE(self, request):
         """
-        Override http_DELETE to validate 'depth' header. 
+        Override http_DELETE to validate 'depth' header.
         """
         if not self.exists():
             log.debug("Resource not found: %s" % (self,))
@@ -2378,12 +2447,14 @@
 
         return self.storeRemove(request, request.uri)
 
+
     def http_PROPPATCH(self, request):
         """
-        No dead properties allowed on notification objects. 
+        No dead properties allowed on notification objects.
         """
         return FORBIDDEN
 
+
     @inlineCallbacks
     def storeRemove(self, request, where):
         """

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_icalendar.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_icalendar.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -21,7 +21,8 @@
 from twisted.trial.unittest import SkipTest
 
 from twistedcaldav.config import config
-from twistedcaldav.ical import Component, Property, InvalidICalendarDataError
+from twistedcaldav.ical import Component, Property, InvalidICalendarDataError, \
+    normalizeCUAddress
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 import twistedcaldav.test.util
 
@@ -42,7 +43,8 @@
         Properties in components
         """
         calendar = Component.fromStream(file(os.path.join(self.data_dir, "Holidays.ics")))
-        if calendar.name() != "VCALENDAR": self.fail("Calendar is not a VCALENDAR")
+        if calendar.name() != "VCALENDAR":
+            self.fail("Calendar is not a VCALENDAR")
 
         for subcomponent in calendar.subcomponents():
             if subcomponent.name() == "VEVENT":
@@ -78,7 +80,7 @@
 #            calendar2 = Component.fromString(data)
 #
 #            self.assertEqual(calendar1, calendar2)
-            
+
         data1 = (
             (
                 "1.1 Switch property order",
@@ -243,7 +245,7 @@
                 True,
             ),
         )
-        
+
         for description, item1, item2, result in data1:
             if "1.3" not in description:
                 continue
@@ -253,6 +255,7 @@
                 calendar1, calendar2, "%s" % (description,)
             )
 
+
     def test_component_validate(self):
         """
         CalDAV resource validation.
@@ -268,7 +271,8 @@
 
         resource_dir = os.path.join(self.data_dir, "Holidays")
         for filename in resource_dir:
-            if os.path.splitext(filename)[1] != ".ics": continue
+            if os.path.splitext(filename)[1] != ".ics":
+                continue
             filename = os.path.join(resource_dir, filename)
 
             calendar = Component.fromStream(file(filename))
@@ -278,6 +282,7 @@
             except ValueError:
                 self.fail("Resource iCalendar %s didn't validate for CalDAV" % (filename,))
 
+
     def test_component_validate_and_fix(self):
         """
         CalDAV resource validation and fixing.
@@ -570,7 +575,8 @@
             end = instance.end
             self.assertEqual(start, PyCalendarDateTime(year, 7, 4))
             self.assertEqual(end  , PyCalendarDateTime(year, 7, 5))
-            if year == 2050: break
+            if year == 2050:
+                break
             year += 1
 
         self.assertEqual(year, 2050)
@@ -596,7 +602,8 @@
             if year in results:
                 self.assertEqual(start, PyCalendarDateTime(year, results[year][0], results[year][1]))
                 self.assertEqual(end  , PyCalendarDateTime(year, results[year][0], results[year][2]))
-            if year == 2050: break
+            if year == 2050:
+                break
             year += 1
 
         self.assertEqual(year, 2050)
@@ -622,11 +629,13 @@
             if year in results:
                 self.assertEqual(start, PyCalendarDateTime(year, results[year][0], results[year][1]))
                 self.assertEqual(end  , PyCalendarDateTime(year, results[year][0], results[year][2]))
-            if year == 2050: break
+            if year == 2050:
+                break
             year += 1
 
         self.assertEqual(year, 2050)
 
+
     def test_component_timerange(self):
         """
         Component summary time range query.
@@ -640,16 +649,18 @@
             end = instance.end
             self.assertEqual(start, PyCalendarDateTime(2004, 11, 25))
             self.assertEqual(end, PyCalendarDateTime(2004, 11, 27))
-            break;
+            break
 
     #test_component_timerange.todo = "recurrence expansion should give us no end date here"
 
+
     def test_parse_date(self):
         """
         parse_date()
         """
         self.assertEqual(PyCalendarDateTime.parseText("19970714"), PyCalendarDateTime(1997, 7, 14))
 
+
     def test_parse_datetime(self):
         """
         parse_datetime()
@@ -661,6 +672,7 @@
         dt = PyCalendarDateTime.parseText("19980119T070000Z")
         self.assertEqual(dt, PyCalendarDateTime(1998, 1, 19, 7, 0, 0, tzid=PyCalendarTimezone(utc=True)))
 
+
     def test_parse_date_or_datetime(self):
         """
         parse_date_or_datetime()
@@ -674,18 +686,20 @@
         dt = PyCalendarDateTime.parseText("19980119T070000Z")
         self.assertEqual(dt, PyCalendarDateTime(1998, 1, 19, 7, 0, 0, tzid=PyCalendarTimezone(utc=True)))
 
+
     def test_parse_duration(self):
         """
         parse_duration()
         """
-        self.assertEqual(PyCalendarDuration.parseText( "P15DT5H0M20S"), PyCalendarDuration(days= 15, hours= 5, minutes=0, seconds= 20))
-        self.assertEqual(PyCalendarDuration.parseText("+P15DT5H0M20S"), PyCalendarDuration(days= 15, hours= 5, minutes=0, seconds= 20))
+        self.assertEqual(PyCalendarDuration.parseText("P15DT5H0M20S"), PyCalendarDuration(days=15, hours=5, minutes=0, seconds=20))
+        self.assertEqual(PyCalendarDuration.parseText("+P15DT5H0M20S"), PyCalendarDuration(days=15, hours=5, minutes=0, seconds=20))
         self.assertEqual(PyCalendarDuration.parseText("-P15DT5H0M20S"), PyCalendarDuration(days=-15, hours=-5, minutes=0, seconds=-20))
 
         self.assertEqual(PyCalendarDuration.parseText("P7W"), PyCalendarDuration(weeks=7))
 
+
     def test_correct_attendee_properties(self):
-        
+
         data = """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//Apple Computer\, Inc//iCal 2.0//EN
@@ -700,10 +714,11 @@
 """
 
         component = Component.fromString(data)
-        self.assertEqual([p.value() for p in component.getAttendeeProperties(("mailto:user2 at example.com",))], ["mailto:user2 at example.com",])
+        self.assertEqual([p.value() for p in component.getAttendeeProperties(("mailto:user2 at example.com",))], ["mailto:user2 at example.com", ])
 
+
     def test_empty_attendee_properties(self):
-        
+
         data = """BEGIN:VCALENDAR
 VERSION:2.0
 DTSTART:20071114T000000Z
@@ -720,8 +735,9 @@
         component = Component.fromString(data)
         self.assertEqual(component.getAttendeeProperties(("user3 at example.com",)), [])
 
+
     def test_organizers_by_instance(self):
-        
+
         data = (
             (
                 """BEGIN:VCALENDAR
@@ -847,13 +863,14 @@
                 )
             ),
         )
-        
+
         for caldata, result in data:
             component = Component.fromString(caldata)
             self.assertEqual(component.getOrganizersByInstance(), result)
 
+
     def test_attendees_by_instance(self):
-        
+
         data = (
             (
                 """BEGIN:VCALENDAR
@@ -979,11 +996,12 @@
                 )
             ),
         )
-        
+
         for caldata, checkScheduleAgent, result in data:
             component = Component.fromString(caldata)
             self.assertEqual(component.getAttendeesByInstance(onlyScheduleAgentServer=checkScheduleAgent), result)
 
+
     def test_set_parameter_value(self):
         data = (
             # ATTENDEE - no existing parameter
@@ -1131,8 +1149,9 @@
         for original, result, args in data:
             component = Component.fromString(original)
             component.setParameterToValueForPropertyWithValue(*args)
-            self.assertEqual(result, str(component).replace("\r", ""))        
+            self.assertEqual(result, str(component).replace("\r", ""))
 
+
     def test_add_property(self):
         data = (
             # Simple component
@@ -1203,10 +1222,11 @@
         for original, result in data:
             component = Component.fromString(original)
             component.addPropertyToAllComponents(Property("REQUEST-STATUS", ["2.0", "Success"]))
-            self.assertEqual(result, str(component).replace("\r", ""))        
+            self.assertEqual(result, str(component).replace("\r", ""))
 
+
     def test_attendees_views(self):
-        
+
         data = (
             (
                 "1.1 Simple component, no Attendees - no filtering",
@@ -1386,7 +1406,7 @@
 END:VCALENDAR
 """,
                 ("mailto:user3 at example.com",)
-            ),        
+            ),
 
             (
                 "2.3 Recurring component with one instance, master with one attendee, instance without attendee - filtering match",
@@ -1683,14 +1703,15 @@
             ),
 
         )
-        
+
         for description, original, checkScheduleAgent, filtered, attendees in data:
             component = Component.fromString(original)
             component.attendeesView(attendees, onlyScheduleAgentServer=checkScheduleAgent)
             self.assertEqual(filtered, str(component).replace("\r", ""), "Failed: %s" % (description,))
 
+
     def test_all_but_one_attendee(self):
-        
+
         data = (
             # One component, no attendees
             (
@@ -1834,14 +1855,15 @@
             ),
 
         )
-        
+
         for original, result, attendee in data:
             component = Component.fromString(original)
             component.removeAllButOneAttendee(attendee)
             self.assertEqual(result, str(component).replace("\r", ""))
 
+
     def test_filter_properties_keep(self):
-        
+
         data = (
             # One component
             (
@@ -1931,14 +1953,15 @@
             ),
 
         )
-        
+
         for original, result, keep_properties in data:
             component = Component.fromString(original)
             component.filterProperties(keep=keep_properties)
             self.assertEqual(result, str(component).replace("\r", ""))
 
+
     def test_filter_properties_remove(self):
-        
+
         data = (
             # One component
             (
@@ -2029,14 +2052,15 @@
             ),
 
         )
-        
+
         for original, result, remove_properties in data:
             component = Component.fromString(original)
             component.filterProperties(remove=remove_properties)
             self.assertEqual(result, str(component).replace("\r", ""))
 
+
     def test_remove_alarms(self):
-        
+
         data = (
             # One component, no alarms
             (
@@ -2198,14 +2222,15 @@
 """,
             ),
         )
-        
+
         for original, result in data:
             component = Component.fromString(original)
             component.removeAlarms()
             self.assertEqual(result, str(component).replace("\r", ""))
 
+
     def test_expand_instances(self):
-        
+
         data = (
             (
                 "Non recurring",
@@ -2430,7 +2455,7 @@
                 )
             ),
         )
-        
+
         for description, original, ignoreInvalidInstances, results in data:
             component = Component.fromString(original)
             if results is None:
@@ -2443,9 +2468,10 @@
                 for start, end in periods:
                     self.assertEqual(start.isDateOnly(), results[0][0].isDateOnly(), "%s: %s wrong date/time start state" % (description, start,))
                     self.assertEqual(end.isDateOnly(), results[0][1].isDateOnly(), "%s: %s wrong date/time end state" % (description, end,))
-       
+
+
     def test_expand_instances_for_expand(self):
-        
+
         data = (
             (
                 "Non recurring utc",
@@ -2772,7 +2798,7 @@
                 None
             ),
         )
-        
+
         for description, original, ignoreInvalidInstances, results in data:
             component = Component.fromString(original)
             if results is None:
@@ -2785,9 +2811,10 @@
                 for start, end in periods:
                     self.assertEqual(start.isDateOnly(), results[0][0].isDateOnly(), "%s: %s wrong date/time start state" % (description, start,))
                     self.assertEqual(end.isDateOnly(), results[0][1].isDateOnly(), "%s: %s wrong date/time end state" % (description, end,))
-       
+
+
     def test_expand_instances_lowerlimit(self):
-        
+
         data = (
             (
                 "Non recurring - no limit",
@@ -3129,7 +3156,7 @@
                 PyCalendarDateTime(2012, 1, 1),
             ),
         )
-        
+
         for description, original, lowerLimit, results, limited in data:
             component = Component.fromString(original)
             instances = component.expandTimeRanges(PyCalendarDateTime(2100, 1, 1), lowerLimit=lowerLimit)
@@ -3140,9 +3167,10 @@
                 self.assertEqual(start.isDateOnly(), results[0][0].isDateOnly(), "%s: %s wrong date/time start state" % (description, start,))
                 self.assertEqual(end.isDateOnly(), results[0][1].isDateOnly(), "%s: %s wrong date/time end state" % (description, end,))
             self.assertEqual(instances.lowerLimit, limited)
-       
+
+
     def test_has_property_in_any_component(self):
-        
+
         data = (
             (
                 "Single component - True",
@@ -3321,13 +3349,14 @@
                 False,
             ),
         )
-        
+
         for description, caldata, propnames, result in data:
             component = Component.fromString(caldata)
             self.assertTrue(component.hasPropertyInAnyComponent(propnames) == result, "Property name match incorrect: %s" % (description,))
-       
+
+
     def test_transfer_properties(self):
-        
+
         data = (
             (
                 "Non recurring - one property",
@@ -3411,7 +3440,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-            ("X-ITEM2","X-ITEM3",),
+            ("X-ITEM2", "X-ITEM3",),
             ),
             (
                 "Non recurring - two properties - one overlap",
@@ -3454,7 +3483,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-            ("X-ITEM2","X-ITEM1",),
+            ("X-ITEM2", "X-ITEM1",),
             ),
             (
                 "Non recurring - one property",
@@ -3587,7 +3616,7 @@
             ("X-ITEM2",),
             ),
         )
-        
+
         for description, transfer_to, transfer_from, result, propnames in data:
             component_to = Component.fromString(transfer_to)
             component_from = Component.fromString(transfer_from)
@@ -3595,8 +3624,9 @@
             component_to.transferProperties(component_from, propnames)
             self.assertEqual(str(component_to), str(component_result), "%s: mismatch" % (description,))
 
+
     def test_normalize_all(self):
-        
+
         data = (
             (
                 "1.1",
@@ -3763,7 +3793,7 @@
 """,
             ),
         )
-        
+
         for title, original, result in data:
             ical1 = Component.fromString(original)
             ical1.normalizeAll()
@@ -3773,8 +3803,9 @@
             diff = "\n".join(unified_diff(ical1.split("\n"), ical2.split("\n")))
             self.assertEqual(str(ical1), str(ical2), "Failed comparison: %s\n%s" % (title, diff,))
 
+
     def test_normalize_attachments(self):
-        
+
         data = (
             (
                 "1.1 - no attach",
@@ -3878,7 +3909,7 @@
 """,
             ),
         )
-        
+
         for title, original, result in data:
             ical1 = Component.fromString(original)
             ical1.normalizeAttachments()
@@ -3888,8 +3919,9 @@
             diff = "\n".join(unified_diff(ical1.split("\n"), ical2.split("\n")))
             self.assertEqual(str(ical1), str(ical2), "Failed comparison: %s\n%s" % (title, diff,))
 
+
     def test_recurring_unbounded(self):
-        
+
         data = (
             (
                 "1.1 - non-recurring",
@@ -3955,14 +3987,15 @@
                 True
             ),
         )
-        
+
         for title, calendar, expected in data:
             ical = Component.fromString(calendar)
             result = ical.isRecurringUnbounded()
             self.assertEqual(result, expected, "Failed recurring unbounded test: %s" % (title,))
 
+
     def test_derive_instance(self):
-        
+
         data = (
             (
                 "1.1 - simple",
@@ -4223,15 +4256,16 @@
                 None,
             ),
         )
-        
+
         for title, calendar, rid, result in data:
             ical = Component.fromString(calendar)
             derived = ical.deriveInstance(rid)
             derived = str(derived).replace("\r", "") if derived else None
             self.assertEqual(derived, result, "Failed derive instance test: %s" % (title,))
 
+
     def test_derive_instance_multiple(self):
-        
+
         data = (
             (
                 "1.1 - simple",
@@ -4441,7 +4475,7 @@
                 ),
             ),
         )
-        
+
         for title, calendar, rids, results in data:
             ical = Component.fromString(calendar)
             for rid, result in itertools.izip(rids, results):
@@ -4449,11 +4483,12 @@
                 derived = str(derived).replace("\r", "") if derived else None
                 self.assertEqual(derived, result, "Failed derive instance test: %s" % (title,))
 
+
     def test_derive_instance_with_cancel(self):
         """
         Test that derivation of cancelled instances works and only results in one STATUS property present.
         """
-        
+
         data = (
             (
                 "1.1 - simple no existing STATUS",
@@ -4509,19 +4544,20 @@
 """,
             ),
         )
-        
+
         for title, calendar, rid, result in data:
             ical = Component.fromString(calendar)
             derived = ical.deriveInstance(rid, allowCancelled=True)
             derived = str(derived).replace("\r", "") if derived else None
             self.assertEqual(derived, result, "Failed derive instance test: %s" % (title,))
 
+
     def test_derive_instance_cache(self):
         """
         Test that derivation of instances only triggers an instance cache re-expansion when it
         goes past the end of the last cache.
         """
-        
+
         event = """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -4533,23 +4569,23 @@
 RRULE:FREQ=DAILY
 END:VEVENT
 END:VCALENDAR
-"""        
+"""
 
         ical = Component.fromString(event)
         self.assertFalse(hasattr(ical, "cachedInstances"))
-        
+
         # Derive one day apart - no re-cache
         ical.deriveInstance(PyCalendarDateTime(2009, 1, 2, 8, 0, 0, tzid=PyCalendarTimezone(utc=True)))
         self.assertTrue(hasattr(ical, "cachedInstances"))
         oldLimit = ical.cachedInstances.limit
         ical.deriveInstance(PyCalendarDateTime(2009, 1, 3, 8, 0, 0, tzid=PyCalendarTimezone(utc=True)))
         self.assertEqual(ical.cachedInstances.limit, oldLimit)
-        
+
         # Derive several years ahead - re-cached
         ical.deriveInstance(PyCalendarDateTime(2011, 1, 1, 8, 0, 0, tzid=PyCalendarTimezone(utc=True)))
         self.assertNotEqual(ical.cachedInstances.limit, oldLimit)
         oldLimit = ical.cachedInstances.limit
-        
+
         # Check one day ahead again - no re-cache
         ical.deriveInstance(PyCalendarDateTime(2011, 1, 2, 8, 0, 0, tzid=PyCalendarTimezone(utc=True)))
         self.assertEqual(ical.cachedInstances.limit, oldLimit)
@@ -4560,7 +4596,7 @@
         Test that derivation of instances only triggers an instance cache re-expansion when it
         goes past the end of the last cache.
         """
-        
+
         event = """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -4594,7 +4630,7 @@
 
         ical = Component.fromString(event)
         masterDerived = ical.masterDerived()
-        
+
         # Derive one day apart - no re-cache
         result = ical.deriveInstance(PyCalendarDateTime(2009, 1, 2, 8, 0, 0, tzid=PyCalendarTimezone(utc=True)), newcomp=masterDerived)
         self.assertEqual(str(result), derived1)
@@ -4609,7 +4645,7 @@
 
 
     def test_truncate_recurrence(self):
-        
+
         data = (
             (
                 "1.1 - no recurrence",
@@ -4746,7 +4782,7 @@
 """,
             ),
         )
-        
+
         for title, original, result in data:
             ical1 = Component.fromString(original)
             changed = ical1.truncateRecurrence(400)
@@ -4758,14 +4794,15 @@
                 else:
                     ical2 = Component.fromString(result)
                     ical2 = str(ical2)
-    
+
                     diff = "\n".join(unified_diff(ical1.split("\n"), ical2.split("\n")))
                     self.assertEqual(str(ical1), str(ical2), "Failed comparison: %s\n%s" % (title, diff,))
             elif changed:
                 self.fail("Truncation happened when not expected: %s" % (title,))
 
+
     def test_valid_recurrence(self):
-        
+
         data = (
             (
                 "1.1 - no recurrence",
@@ -5022,23 +5059,24 @@
                 )
             ),
         )
-        
+
         for clear_cache in (True, False):
             for title, calendar, tests in data:
                 ical = Component.fromString(calendar)
                 for ctr, item in enumerate(tests):
                     rid, result = item
-                    self.assertEqual(ical.validInstance(rid, clear_cache=clear_cache), result, "Failed comparison: %s #%d" % (title, ctr+1,))
+                    self.assertEqual(ical.validInstance(rid, clear_cache=clear_cache), result, "Failed comparison: %s #%d" % (title, ctr + 1,))
 
         for title, calendar, tests in data:
             ical = Component.fromString(calendar)
             rids = set([rid for rid, result in tests])
-            expected_results = set([rid for rid, result in tests if result==True])
+            expected_results = set([rid for rid, result in tests if result == True])
             actual_results = ical.validInstances(rids)
             self.assertEqual(actual_results, expected_results, "Failed comparison: %s %s" % (title, actual_results,))
 
+
     def test_valid_recurrence_ids(self):
-        
+
         data = (
             (
                 "1.1 - fake master",
@@ -5241,7 +5279,7 @@
             1, 0,
             ),
         )
-        
+
         for title, calendar, result_calendar, result_fixed, result_unfixed in data:
             ical = Component.fromString(calendar)
             fixed, unfixed = ical.validRecurrenceIDs(doFix=True)
@@ -5249,6 +5287,7 @@
             self.assertEqual(len(fixed), result_fixed, "Failed fixed comparison: %s %s" % (title, fixed,))
             self.assertEqual(len(unfixed), result_unfixed, "Failed unfixed: %s %s" % (title, unfixed,))
 
+
     def test_mismatched_until(self):
         invalid = (
             """BEGIN:VCALENDAR
@@ -5335,7 +5374,6 @@
 """,
         )
 
-
         for text in invalid:
             calendar = Component.fromString(text)
             self.assertRaises(InvalidICalendarDataError, calendar.validCalendarData, doFix=False)
@@ -5347,6 +5385,7 @@
             except:
                 self.fail("Valid calendar should validate")
 
+
     def test_allperuseruids(self):
         data = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -5388,6 +5427,7 @@
             "user02",
         )))
 
+
     def test_perUserTransparency(self):
         data = (
                     (
@@ -5876,6 +5916,7 @@
             for rid, result in results:
                 self.assertEqual(calendar.perUserTransparency(rid), result, "Failed comparison: %s %s" % (title, rid,))
 
+
     def test_needsiTIPSequenceChange(self):
 
         data = (
@@ -6153,6 +6194,7 @@
             ical_new = Component.fromString(new_txt)
             self.assertEqual(ical_new.needsiTIPSequenceChange(ical_old), result, "Failed: %s" % (title,))
 
+
     def test_bumpiTIPInfo(self):
 
         data = (
@@ -6592,16 +6634,16 @@
                 True,
             ),
         )
-        
+
         for title, old_txt, ical_txt, result_txt, doSequence in data:
             old = Component.fromString(old_txt) if old_txt else None
             ical = Component.fromString(ical_txt)
             result = Component.fromString(result_txt)
             ical.bumpiTIPInfo(oldcalendar=old, doSequence=doSequence)
-            
+
             ical1 = str(ical).split("\n")
             ical2 = str(result).split("\n")
-            
+
             # Check without DTSTAMPs which we expect to be different
             ical1_withoutDTSTAMP = [item for item in ical1 if not item.startswith("DTSTAMP:")]
             ical2_withoutDTSTAMP = [item for item in ical2 if not item.startswith("DTSTAMP:")]
@@ -6609,10 +6651,10 @@
             diff = "\n".join(unified_diff(ical1_withoutDTSTAMP, ical2_withoutDTSTAMP))
             self.assertEqual("\n".join(ical1_withoutDTSTAMP), "\n".join(ical2_withoutDTSTAMP), "Failed comparison: %s\n%s" % (title, diff,))
 
-            # Check that all DTSTAMPs changed    
+            # Check that all DTSTAMPs changed
             dtstamps1 = set([item for item in ical1 if item.startswith("DTSTAMP:")])
             dtstamps2 = set([item for item in ical2 if item.startswith("DTSTAMP:")])
-            
+
             diff = "\n".join(unified_diff(ical1, ical2))
             self.assertEqual(len(dtstamps1 & dtstamps2), 0, "Failed comparison: %s\n%s" % (title, diff,))
 
@@ -6860,16 +6902,16 @@
 """,
             ),
         )
-        
+
         for title, old_txt, ical_txt, result_txt in data:
             old = Component.fromString(old_txt)
             ical = Component.fromString(ical_txt)
             result = Component.fromString(result_txt)
             ical.sequenceInSync(old)
-            
+
             ical1 = str(ical).split("\n")
             ical2 = str(result).split("\n")
-            
+
             diff = "\n".join(unified_diff(ical1, ical2))
             self.assertEqual("\n".join(ical1), "\n".join(ical2), "Failed comparison: %s\n%s" % (title, diff,))
 
@@ -7271,6 +7313,7 @@
 
         component = Component.fromString(data)
 
+
         def lookupFunction(cuaddr, ignored1, ignored2):
             return {
                 "urn:uuid:foo" : (
@@ -7328,17 +7371,18 @@
 
         component = Component.fromString(data)
 
+
         def lookupFunction(cuaddr, ignored1, ignored2):
             return {
                 "/principals/users/foo" : (
                     "Foo",
                     "foo",
-                    ("urn:uuid:foo", )
+                    ("urn:uuid:foo",)
                 ),
                 "http://example.com/principals/users/buz" : (
                     "Buz",
                     "buz",
-                    ("urn:uuid:buz", )
+                    ("urn:uuid:buz",)
                 ),
             }[cuaddr]
 
@@ -7442,6 +7486,7 @@
         self.assertEquals(subComponent._parent, None)
         self.assertEquals(component._cachedCopy, None) # cache is invalidated
 
+
     def test_hasDuplicateAlarms(self):
         """
         Test that L{Component.hasDuplicateAlarms} correctly detects, but does not fix, duplicate alarms.
@@ -7465,7 +7510,7 @@
 """,
                 False,
             ),
-            
+
             # One alarm
             (
                 """BEGIN:VCALENDAR
@@ -7488,7 +7533,7 @@
 """,
                 False,
             ),
-            
+
             # Two different alarms
             (
                 """BEGIN:VCALENDAR
@@ -7516,7 +7561,7 @@
 """,
                 False,
             ),
-            
+
             # Two duplicates, one different
             (
                 """BEGIN:VCALENDAR
@@ -7562,7 +7607,7 @@
 """,
                 True,
             ),
-            
+
             # Two duplicates in one component, three different in another
             (
                 """BEGIN:VCALENDAR
@@ -7618,7 +7663,7 @@
 """,
                 True,
             ),
-            
+
             # Four duplicates in X-CALENDARSERVER-PERINSTANCE
             (
                 """BEGIN:VCALENDAR
@@ -7664,11 +7709,12 @@
             ),
         )
 
-        for txt, result in data: 
+        for txt, result in data:
             component = Component.fromString(txt)
             self.assertEqual(component.hasDuplicateAlarms(doFix=False), result)
             self.assertEqual(str(component), txt.replace("\n", "\r\n"))
 
+
     def test_hasDuplicateAlarms_withFix(self):
         """
         Test that L{Component.hasDuplicateAlarms} correctly removes duplicate alarms.
@@ -7705,7 +7751,7 @@
 """,
                 False,
             ),
-            
+
             # One alarm
             (
                 """BEGIN:VCALENDAR
@@ -7746,7 +7792,7 @@
 """,
                 False,
             ),
-            
+
             # Two different alarms
             (
                 """BEGIN:VCALENDAR
@@ -7797,7 +7843,7 @@
 """,
                 False,
             ),
-            
+
             # Two duplicates, one different
             (
                 """BEGIN:VCALENDAR
@@ -7879,7 +7925,7 @@
 """,
                 True,
             ),
-            
+
             # Two duplicates in one component, three different in another
             (
                 """BEGIN:VCALENDAR
@@ -7976,7 +8022,7 @@
 """,
                 True,
             ),
-            
+
             # Four duplicates in X-CALENDARSERVER-PERINSTANCE
             (
                 """BEGIN:VCALENDAR
@@ -8046,8 +8092,81 @@
             ),
         )
 
-        for txt, result, result_changed in data: 
+        for txt, result, result_changed in data:
             component = Component.fromString(txt)
             changed = component.hasDuplicateAlarms(doFix=True)
             self.assertEqual(str(component), result.replace("\n", "\r\n"))
             self.assertEqual(changed, result_changed)
+
+
+    def test_normalizeCUAddressFromUUID(self):
+        """
+        Ensure mailto is preferred, followed by path form, then http form.
+        If CALENDARSERVER-OLD-CUA parameter is present, restore that value.
+        """
+
+        data = (
+            ("urn:uuid:foo", "/foo"),
+            ("urn:uuid:bar", "mailto:bar at example.com",),
+            ("urn:uuid:baz", "http://example.com/baz",),
+            ("urn:uuid:buz", "urn:uuid:buz",),
+        )
+
+        def lookupFunction(cuaddr, ignored1, ignored2):
+            return {
+                "urn:uuid:foo" : (
+                    "Foo",
+                    "foo",
+                    ("urn:uuid:foo", "http://example.com/foo", "/foo")
+                ),
+                "urn:uuid:bar" : (
+                    "Bar",
+                    "bar",
+                    ("urn:uuid:bar", "mailto:bar at example.com", "http://example.com/bar", "/bar")
+                ),
+                "urn:uuid:baz" : (
+                    "Baz",
+                    "baz",
+                    ("urn:uuid:baz", "http://example.com/baz")
+                ),
+                "urn:uuid:buz" : (
+                    "Buz",
+                    "buz",
+                    ("urn:uuid:buz",)
+                ),
+            }[cuaddr]
+
+        for cuaddr, result in data:
+            new_cuaddr = normalizeCUAddress(cuaddr, lookupFunction, None, toUUID=False)
+            self.assertEquals(new_cuaddr, result)
+
+
+    def test_normalizeCUAddressToUUID(self):
+        """
+        Ensure http(s) and /path CUA values are tucked away into the property
+        using CALENDARSERVER-OLD-CUA parameter.
+        """
+
+        data = (
+            ("/principals/users/foo", "urn:uuid:foo",),
+            ("http://example.com/principals/users/buz", "urn:uuid:buz",),
+        )
+
+
+        def lookupFunction(cuaddr, ignored1, ignored2):
+            return {
+                "/principals/users/foo" : (
+                    "Foo",
+                    "foo",
+                    ("urn:uuid:foo",)
+                ),
+                "http://example.com/principals/users/buz" : (
+                    "Buz",
+                    "buz",
+                    ("urn:uuid:buz",)
+                ),
+            }[cuaddr]
+
+        for cuaddr, result in data:
+            new_cuaddr = normalizeCUAddress(cuaddr, lookupFunction, None, toUUID=True)
+            self.assertEquals(new_cuaddr, result)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_mail.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_mail.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_mail.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,867 +0,0 @@
-##
-# Copyright (c) 2008-2012 Apple Inc. All 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 cStringIO import StringIO
-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.mail import injectionSettingsFromURL
-from twistedcaldav.mail import MailGatewayTokensDatabase
-from twistedcaldav.mail import MailHandler
-from twistedcaldav.mail import StringFormatTemplateLoader
-from twistedcaldav.mail import serverForOrganizer
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.servers import Servers
-from twistedcaldav.test.util import TestCase
-from twistedcaldav.test.util import xmlFile, augmentsFile
-import datetime
-import email
-import os
-from pycalendar.datetime import PyCalendarDateTime
-
-
-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):
-
-    def setUp(self):
-        super(MailHandlerTests, 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").child("mail")
-
-
-    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
-        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, 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"
-        )
-
-        url, organizer, attendee, calendar, 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"
-        )
-
-        url, organizer, attendee, calendar, 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
-        """
-
-        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"
-
-        (actualInviteState, actualCalendar, actualOrganizerEmail,
-            actualOrganizerName, actualAttendeeList, actualFrom,
-            actualRecipient, 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.
-        """
-        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.
-        """
-        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>
-"""
-
-

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_schedule.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_schedule.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_schedule.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,502 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All 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 import responsecode, http_headers
-from txdav.xml import element as davxml
-from twext.web2.dav.util import davXMLFromStream
-from twext.web2.http import HTTPError
-from twext.web2.iweb import IResponse
-from twext.web2.stream import MemoryStream
-from twext.web2.test.test_server import SimpleRequest
-
-from twisted.internet.defer import inlineCallbacks
-
-from twistedcaldav import caldavxml, customxml
-from twistedcaldav.config import config
-from twistedcaldav.memcachelock import MemcacheLock
-from twistedcaldav.schedule import IScheduleInboxResource
-from twistedcaldav.test.util import HomeTestCase, TestCase
-
-class Properties (HomeTestCase):
-    """
-    CalDAV properties
-    """
-    def test_free_busy_set_prop(self):
-        """
-        Test for PROPFIND on Inbox with missing calendar-free-busy-set property.
-        """
-
-        inbox_uri  = "/inbox/"
-
-        def propfind_cb(response):
-            response = IResponse(response)
-
-            if response.code != responsecode.MULTI_STATUS:
-                self.fail("Incorrect response to PROPFIND: %s" % (response.code,))
-
-            def got_xml(doc):
-                if not isinstance(doc.root_element, davxml.MultiStatus):
-                    self.fail("PROPFIND response XML root element is not multistatus: %r" % (doc.root_element,))
-
-                response = doc.root_element.childOfType(davxml.Response)
-                href = response.childOfType(davxml.HRef)
-                self.failUnless(str(href) == inbox_uri)
-
-                for propstat in response.childrenOfType(davxml.PropertyStatus):
-                    status = propstat.childOfType(davxml.Status)
-                    if status.code != responsecode.OK:
-                        self.fail("Unable to read requested properties (%s): %r"
-                                  % (status, propstat.childOfType(davxml.PropertyContainer).toxml()))
-
-                container = propstat.childOfType(davxml.PropertyContainer)
-
-                #
-                # Check CalDAV:calendar-free-busy-set
-                #
-
-                free_busy_set = container.childOfType(caldavxml.CalendarFreeBusySet)
-                if not free_busy_set:
-                    self.fail("Expected CalDAV:calendar-free-busy-set element; but got none.")
-
-                if not free_busy_set.children:
-                    self.fail("Expected non-empty CalDAV:calendar-free-busy-set element.")
-
-            return davXMLFromStream(response.stream).addCallback(got_xml)
-
-        query = davxml.PropertyFind(
-                    davxml.PropertyContainer(
-                        caldavxml.CalendarFreeBusySet(),
-                    ),
-                )
-
-        request = SimpleRequest(
-            self.site,
-            "PROPFIND",
-            inbox_uri,
-            headers=http_headers.Headers({"Depth":"0"}),
-        )
-        request.stream = MemoryStream(query.toxml())
-        return self.send(request, propfind_cb)
-
-    @inlineCallbacks
-    def test_free_busy_set_remove_broken(self):
-        """
-        ???
-        """
-
-        request = SimpleRequest(self.site, "GET", "/inbox/")
-        inbox = yield request.locateResource("/inbox/")
-        self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
-        oldfbset = set(("/calendar",))
-        oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
-
-        newfbset = set()
-        newfbset.update(oldfbset)
-        newfbset.add("/calendar-broken")
-        newset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in newfbset])
-
-        inbox.writeDeadProperty(newset)
-        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
-        self.assertEqual(tuple(changedset.children), tuple(newset.children))
-        
-        yield inbox.writeProperty(newset, request)
-
-        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
-        self.assertEqual(tuple(changedset.children), tuple(oldset.children))
-
-    @inlineCallbacks
-    def test_free_busy_set_strip_slash(self):
-        """
-        ???
-        """
-
-        request = SimpleRequest(self.site, "GET", "/inbox/")
-        inbox = yield request.locateResource("/inbox/")
-        self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
-
-        oldfbset = set(("/calendar/",))
-        oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
-        inbox.writeDeadProperty(oldset)
-        
-        writefbset = set(("/calendar/",))
-        writeset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in writefbset])
-        yield inbox.writeProperty(writeset, request)
-
-        correctfbset = set(("/calendar",))
-        correctset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in correctfbset])
-        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
-        self.assertEqual(tuple(changedset.children), tuple(correctset.children))
-
-    @inlineCallbacks
-    def test_free_busy_set_strip_slash_remove(self):
-        """
-        ???
-        """
-
-        request = SimpleRequest(self.site, "GET", "/inbox/")
-        inbox = yield request.locateResource("/inbox/")
-        self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
-
-        oldfbset = set(("/calendar/", "/broken/"))
-        oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
-        inbox.writeDeadProperty(oldset)
-        
-        writefbset = set(("/calendar/", "/broken/"))
-        writeset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in writefbset])
-        yield inbox.writeProperty(writeset, request)
-
-        correctfbset = set(("/calendar",))
-        correctset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in correctfbset])
-        changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
-        self.assertEqual(tuple(changedset.children), tuple(correctset.children))
-
-class DefaultCalendar (TestCase):
-
-    def setUp(self):
-        super(DefaultCalendar, self).setUp()
-        self.createStockDirectoryService()
-        self.setupCalendars()
-
-    @inlineCallbacks
-    def test_pick_default_vevent_calendar(self):
-        """
-        Test that pickNewDefaultCalendar will choose the correct calendar.
-        """
-        
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property initially not present
-        try:
-            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
-        yield inbox.pickNewDefaultCalendar(request)
-
-        try:
-            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
-
-        request._newStoreTransaction.abort()
-
-    @inlineCallbacks
-    def test_pick_default_vtodo_calendar(self):
-        """
-        Test that pickNewDefaultCalendar will choose the correct tasks calendar.
-        """
-        
-        
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property initially not present
-        try:
-            inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("customxml.ScheduleDefaultTasksURL is not empty")
-
-        yield inbox.pickNewDefaultCalendar(request, tasks=True)
-
-        try:
-            default = inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
-        except HTTPError:
-            self.fail("customxml.ScheduleDefaultTasksURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/tasks")
-
-        request._newStoreTransaction.abort()
-
-    @inlineCallbacks
-    def test_missing_default_vevent_calendar(self):
-        """
-        Test that pickNewDefaultCalendar will create a missing default calendar.
-        """
-        
-        
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        home = yield request.locateResource("/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property initially not present
-        try:
-            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
-        # Forcibly remove the one we need
-        yield home._newStoreHome.removeChildWithName("calendar")
-        names = [calendarName for calendarName in (yield home._newStoreHome.listCalendars())]
-        self.assertTrue("calendar" not in names)
-
-        yield inbox.pickNewDefaultCalendar(request)
-
-        try:
-            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
-
-        request._newStoreTransaction.abort()
-
-    @inlineCallbacks
-    def test_missing_default_vtodo_calendar(self):
-        """
-        Test that pickNewDefaultCalendar will create a missing default tasks calendar.
-        """
-        
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        home = yield request.locateResource("/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property initially not present
-        try:
-            inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultTasksURL is not empty")
-
-        # Forcibly remove the one we need
-        yield home._newStoreHome.removeChildWithName("tasks")
-        names = [calendarName for calendarName in (yield home._newStoreHome.listCalendars())]
-        self.assertTrue("tasks" not in names)
-
-        yield inbox.pickNewDefaultCalendar(request, tasks=True)
-
-        try:
-            default = inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultTasksURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/tasks")
-
-        request._newStoreTransaction.abort()
-
-    @inlineCallbacks
-    def test_pick_default_other(self):
-        """
-        Make calendar
-        """
-        
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property not present
-        try:
-            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
-        # Create a new default calendar
-        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
-        yield newcalendar.createCalendarCollection()
-        inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(
-            davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-        ))
-        
-        # Delete the normal calendar
-        calendar = yield request.locateResource("/calendars/users/wsanchez/calendar")
-        yield calendar.storeRemove(request, False, "/calendars/users/wsanchez/calendar")
-
-        inbox.removeDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        
-        # default property not present
-        try:
-            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-        request._newStoreTransaction.commit()
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-        yield inbox.pickNewDefaultCalendar(request)
-
-        try:
-            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-
-        request._newStoreTransaction.abort()
-
-    @inlineCallbacks
-    def test_fix_shared_default(self):
-        """
-        Make calendar
-        """
-        
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # Create a new default calendar
-        newcalendar = yield request.locateResource("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-        yield newcalendar.createCalendarCollection()
-        inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(
-            davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-        ))
-        try:
-            default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-        
-        # Force the new calendar to think it is a sharee collection
-        newcalendar._isShareeCollection = True
-        
-        try:
-            default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
-
-        request._newStoreTransaction.abort()
-
-    @inlineCallbacks
-    def test_set_default_vevent_other(self):
-        """
-        Test that the default URL can be set to another VEVENT calendar
-        """
-
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
-        # default property not present
-        try:
-            inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            pass
-        else:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
-        # Create a new default calendar
-        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
-        yield newcalendar.createCalendarCollection()
-        yield newcalendar.setSupportedComponents(("VEVENT",))
-        request._newStoreTransaction.commit()
-        
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-        yield inbox.writeProperty(caldavxml.ScheduleDefaultCalendarURL(davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")), request)
-
-        try:
-            default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-        except HTTPError:
-            self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
-        else:
-            self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-
-        request._newStoreTransaction.commit()
-
-    @inlineCallbacks
-    def test_is_default_calendar(self):
-        """
-        Test .isDefaultCalendar() returns the proper class or None.
-        """
-        
-        # Create a new non-default calendar
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
-        yield newcalendar.createCalendarCollection()
-        yield newcalendar.setSupportedComponents(("VEVENT",))
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-        yield inbox.pickNewDefaultCalendar(request)
-        request._newStoreTransaction.commit()
-        
-        request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
-        inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-        calendar = yield request.locateResource("/calendars/users/wsanchez/calendar")
-        newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
-        tasks = yield request.locateResource("/calendars/users/wsanchez/tasks")
-
-        result = yield inbox.isDefaultCalendar(request, calendar)
-        self.assertEqual(result, caldavxml.ScheduleDefaultCalendarURL)
-        
-        result = yield inbox.isDefaultCalendar(request, newcalendar)
-        self.assertEqual(result, None)
-        
-        result = yield inbox.isDefaultCalendar(request, tasks)
-        self.assertEqual(result, customxml.ScheduleDefaultTasksURL)
-
-        request._newStoreTransaction.commit()
-
-class iSchedulePOST (TestCase):
-
-    def setUp(self):
-        super(iSchedulePOST, self).setUp()
-        self.createStockDirectoryService()
-        self.setupCalendars()
-        self.site.resource.putChild("ischedule", IScheduleInboxResource(self.site.resource, self._newStore))
-
-    @inlineCallbacks
-    def test_deadlock(self):
-        """
-        Make calendar
-        """
-        
-        request = SimpleRequest(
-            self.site,
-            "POST",
-            "/ischedule",
-            headers=http_headers.Headers(rawHeaders={
-                "Originator": ("mailto:wsanchez at example.com",),
-                "Recipient": ("mailto:cdaboo at example.com",),
-            }),
-            content="""BEGIN:VCALENDAR
-CALSCALE:GREGORIAN
-PRODID:-//Example Inc.//Example Calendar//EN
-VERSION:2.0
-BEGIN:VEVENT
-DTSTAMP:20051222T205953Z
-CREATED:20060101T150000Z
-DTSTART:20060101T100000Z
-DURATION:PT1H
-SUMMARY:event 1
-UID:deadlocked
-ORGANIZER:mailto:wsanchez at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:wsanchez at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:cdaboo at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        )
-
-        # Lock the UID here to force a deadlock - but adjust the timeout so the test does not wait too long
-        self.patch(config.Scheduling.Options, "UIDLockTimeoutSeconds", 1)
-        lock = MemcacheLock("ImplicitUIDLock", "deadlocked", timeout=60, expire_time=60)
-        yield lock.acquire()
-        
-        response = (yield self.send(request))
-        self.assertEqual(response.code, responsecode.CONFLICT)

Deleted: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_servers.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_servers.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_servers.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,179 +0,0 @@
-##
-# Copyright (c) 2009-2012 Apple Inc. All 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 twistedcaldav.config import config
-from twistedcaldav.servers import Servers, SERVER_SECRET_HEADER
-from twistedcaldav.test.util import TestCase
-import StringIO as StringIO
-
-class ServerTests(TestCase):
-
-    data1 = """<?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>
-"""
-
-    data2 = """<?xml version="1.0" encoding="utf-8"?>
-<servers>
-  <server>
-    <id>00001</id>
-    <uri>http://caldav1.example.com:8008</uri>
-    <allowed-from>localhost</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>
-"""
-
-    def _setupServers(self, data=data1):
-        self.patch(config, "ServerHostName", "caldav1.example.com")
-        self.patch(config, "HTTPPort", 8008)
-
-        xmlFile = StringIO.StringIO(data)
-        servers = Servers
-        servers.load(xmlFile, ignoreIPLookupFailures=True)
-
-        return servers
-
-    def test_read_ok(self):
-        
-        servers = self._setupServers()
-
-        self.assertTrue(servers.getServerById("00001") is not None)
-        self.assertTrue(servers.getServerById("00002") is not None)
-
-        self.assertEqual(servers.getServerById("00001").uri, "http://caldav1.example.com:8008")
-        self.assertEqual(servers.getServerById("00002").uri, "https://caldav2.example.com:8843")
-
-        self.assertEqual(servers.getServerById("00001").allowed_from_ips, set(("127.0.0.1",)))
-        self.assertEqual(servers.getServerById("00002").allowed_from_ips, set())
-
-        self.assertEqual(servers.getServerById("00001").shared_secret, "foobar")
-        self.assertEqual(servers.getServerById("00002").shared_secret, None)
-
-        self.assertEqual(len(servers.getServerById("00001").partitions), 0)
-        self.assertEqual(len(servers.getServerById("00002").partitions), 2)
-
-        self.assertEqual(servers.getServerById("00002").getPartitionURIForId("A"), "https://machine1.example.com:8443")
-        self.assertEqual(servers.getServerById("00002").getPartitionURIForId("B"), "https://machine2.example.com:8443")
-
-    def test_this_server(self):
-        
-        servers = self._setupServers()
-
-        self.assertTrue(servers.getServerById("00001").thisServer)
-        self.assertFalse(servers.getServerById("00002").thisServer)
-        
-        self.patch(config, "ServerHostName", "caldav2.example.com")
-        self.patch(config, "SSLPort", 8443)
-        self.patch(config, "BindSSLPorts", [8843])
-        
-        xmlFile = StringIO.StringIO(ServerTests.data1)
-        servers = Servers
-        servers.load(xmlFile, ignoreIPLookupFailures=True)
-
-        self.assertFalse(servers.getServerById("00001").thisServer)
-        self.assertTrue(servers.getServerById("00002").thisServer)
-
-    def test_check_is_partitioned(self):
-
-        servers = self._setupServers()
-        
-        self.assertFalse(servers.getServerById("00001").isPartitioned())
-        self.assertTrue(servers.getServerById("00002").isPartitioned())
-
-    def test_check_this_ip(self):
-
-        servers = self._setupServers()
-        servers.getServerById("00001").ips = set(("127.0.0.2",))
-        servers.getServerById("00002").ips = set(("127.0.0.3",))
-        
-        self.assertTrue(servers.getServerById("00001").checkThisIP("127.0.0.2"))
-        self.assertFalse(servers.getServerById("00001").checkThisIP("127.0.0.3"))
-
-    def test_check_allowed_from(self):
-
-        for servers in (self._setupServers(), self._setupServers(data=self.data2),):
-            self.assertTrue(servers.getServerById("00001").hasAllowedFromIP())
-            self.assertFalse(servers.getServerById("00002").hasAllowedFromIP())
-
-            self.assertTrue(servers.getServerById("00001").checkAllowedFromIP("127.0.0.1"))
-            self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.2"))
-            self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.3"))
-            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.1"))
-            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.2"))
-            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.3"))
-
-    def test_check_shared_secret(self):
-
-        servers = self._setupServers()
-        
-        request = SimpleRequest(None, "POST", "/ischedule")
-        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
-        self.assertTrue(servers.getServerById("00001").checkSharedSecret(request))
-        
-        request = SimpleRequest(None, "POST", "/ischedule")
-        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
-        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
-        
-        request = SimpleRequest(None, "POST", "/ischedule")
-        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
-        
-        request = SimpleRequest(None, "POST", "/ischedule")
-        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
-        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
-        
-        request = SimpleRequest(None, "POST", "/ischedule")
-        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
-        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
-        
-        request = SimpleRequest(None, "POST", "/ischedule")
-        self.assertTrue(servers.getServerById("00002").checkSharedSecret(request))
-        

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_upgrade.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_upgrade.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_upgrade.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -15,7 +15,9 @@
 ##
 
 import hashlib
-import os, zlib, cPickle
+import os
+import zlib
+import cPickle
 
 from twisted.python.reflect import namedClass
 from twisted.internet.defer import inlineCallbacks
@@ -26,7 +28,7 @@
 from twistedcaldav.config import config
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
-from twistedcaldav.mail import MailGatewayTokensDatabase
+from twistedcaldav.scheduling.imip.mailgateway import MailGatewayTokensDatabase
 from twistedcaldav.upgrade import (
     xattrname, upgradeData, updateFreeBusySet,
     removeIllegalCharacters, normalizeCUAddrs
@@ -51,7 +53,7 @@
 
 class UpgradeTests(TestCase):
 
-    
+
     def setUpXMLDirectory(self):
         xmlFile = os.path.join(os.path.dirname(os.path.dirname(__file__)),
             "directory", "test", "accounts.xml")
@@ -80,13 +82,13 @@
         self.setUpOldDocRoot()
         self.setUpOldDocRootWithoutDB()
         self.setUpNewDocRoot()
-        
+
         self.setUpNewDataRoot()
         self.setUpDataRootWithProxyDB()
 
 
     def setUpOldDocRoot(self):
-        
+
         # Set up doc root
         self.olddocroot = os.path.abspath(self.mktemp())
         os.mkdir(self.olddocroot)
@@ -104,7 +106,7 @@
 
 
     def setUpOldDocRootWithoutDB(self):
-        
+
         # Set up doc root
         self.olddocrootnodb = os.path.abspath(self.mktemp())
         os.mkdir(self.olddocrootnodb)
@@ -119,22 +121,25 @@
         os.mkdir(os.path.join(principals, "sudoers"))
         os.mkdir(os.path.join(self.olddocrootnodb, "calendars"))
 
+
     def setUpNewDocRoot(self):
-        
+
         # Set up doc root
         self.newdocroot = os.path.abspath(self.mktemp())
         os.mkdir(self.newdocroot)
 
         os.mkdir(os.path.join(self.newdocroot, "calendars"))
 
+
     def setUpNewDataRoot(self):
-        
+
         # Set up data root
         self.newdataroot = os.path.abspath(self.mktemp())
         os.mkdir(self.newdataroot)
 
+
     def setUpDataRootWithProxyDB(self):
-        
+
         # Set up data root
         self.existingdataroot = os.path.abspath(self.mktemp())
         os.mkdir(self.existingdataroot)
@@ -156,7 +161,6 @@
         config.DocumentRoot = self.olddocroot
         config.DataRoot = self.newdataroot
 
-        
         # Check pre-conditions
         self.assertTrue(os.path.exists(os.path.join(config.DocumentRoot, "principals")))
         self.assertTrue(os.path.isdir(os.path.join(config.DocumentRoot, "principals")))
@@ -164,7 +168,7 @@
         self.assertFalse(os.path.exists(os.path.join(config.DataRoot, NEWPROXYFILE)))
 
         (yield self.doUpgrade(config))
-        
+
         # Check post-conditions
         self.assertFalse(os.path.exists(os.path.join(config.DocumentRoot, "principals",)))
         self.assertTrue(os.path.exists(os.path.join(config.DataRoot, NEWPROXYFILE)))
@@ -180,13 +184,13 @@
 
         config.DocumentRoot = self.newdocroot
         config.DataRoot = self.existingdataroot
-        
+
         # Check pre-conditions
         self.assertFalse(os.path.exists(os.path.join(config.DocumentRoot, "principals")))
         self.assertTrue(os.path.exists(os.path.join(config.DataRoot, NEWPROXYFILE)))
 
         (yield self.doUpgrade(config))
-        
+
         # Check post-conditions
         self.assertFalse(os.path.exists(os.path.join(config.DocumentRoot, "principals",)))
         self.assertTrue(os.path.exists(os.path.join(config.DataRoot, NEWPROXYFILE)))
@@ -219,11 +223,9 @@
         value = cPickle.dumps(doc.root_element)
         self.assertEquals(updateFreeBusySet(value, directory), None)
 
-
         #
         # Verify these values do require updating:
         #
-
         expected = "<?xml version='1.0' encoding='UTF-8'?>\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n  <href xmlns='DAV:'>/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar/</href>\r\n</calendar-free-busy-set>"
 
         # Uncompressed XML
@@ -247,11 +249,9 @@
         newValue = zlib.decompress(newValue)
         self.assertEquals(newValue, expected)
 
-
         #
         # Shortname not in directory, return empty string
         #
-
         expected = "<?xml version='1.0' encoding='UTF-8'?>\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>"
         value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n  <href xmlns='DAV:'>/calendars/users/nonexistent/calendar</href>\r\n</calendar-free-busy-set>\r\n"
         newValue = updateFreeBusySet(value, directory)
@@ -309,7 +309,7 @@
                          },
                         "notifications": {
                             "sample-notification.xml": {
-                                "@contents":  "<?xml version='1.0'>\n<should-be-ignored />"
+                                "@contents": "<?xml version='1.0'>\n<should-be-ignored />"
                             }
                         }
                     }
@@ -699,7 +699,7 @@
                 {
                     "@contents" : "",
                 },
-                "__uids__" :ignoredUIDContents,
+                "__uids__" : ignoredUIDContents,
             },
             "principals" :
             {
@@ -810,7 +810,7 @@
                 {
                     "@contents" : "",
                 },
-                "__uids__" :beforeUIDContents,
+                "__uids__" : beforeUIDContents,
             },
             "principals" :
             {
@@ -1098,6 +1098,7 @@
 
         (yield self.verifyDirectoryComparison(before, after, reverify=True))
 
+
     @inlineCallbacks
     def test_calendarsUpgradeWithNoChange(self):
         """
@@ -1226,7 +1227,6 @@
         (yield self.verifyDirectoryComparison(before, after))
 
 
-
     @inlineCallbacks
     def test_calendarsUpgradeWithInboxItems(self):
         """
@@ -1375,7 +1375,6 @@
             }
         }
 
-
         after = {
             ".calendarserver_version" :
             {
@@ -1425,7 +1424,6 @@
             },
         }
 
-
         root = self.createHierarchy(before)
 
         config.DocumentRoot = root
@@ -1435,6 +1433,7 @@
 
         self.assertTrue(self.verifyHierarchy(root, after))
 
+
     @inlineCallbacks
     def test_migrateResourceInfo(self):
         # Fake getResourceInfo( )
@@ -1449,7 +1448,7 @@
         def _getResourceInfo(ignored):
             results = []
             for guid, info in assignments.iteritems():
-                results.append( (guid, info[0], info[1], info[2]) )
+                results.append((guid, info[0], info[1], info[2]))
             return results
 
         self.setUpInitialStates()
@@ -1522,7 +1521,6 @@
             self.assertEquals(info[0], autoSchedule)
 
 
-
     def test_removeIllegalCharacters(self):
         """
         Control characters aside from NL and CR are removed.
@@ -1712,6 +1710,7 @@
         return False
 
 
+
 class ParallelUpgradeTests(UpgradeTests):
     """
     Tests for upgradeData in parallel.
@@ -1721,4 +1720,3 @@
         from txdav.common.datastore.upgrade.test.test_migrate import StubSpawner
         spawner = StubSpawner(config)
         return upgradeData(config, spawner, 2)
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_wrapping.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_wrapping.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/test/test_wrapping.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -53,6 +53,7 @@
 from twistedcaldav.directory.test.test_xmlfile import XMLFileBase
 from txdav.caldav.icalendarstore import ICalendarHome
 from txdav.carddav.iaddressbookstore import IAddressBookHome
+
 from txdav.caldav.datastore.file import Calendar
 
 
@@ -108,6 +109,7 @@
 
         @param objectName: The name of a calendar object.
         @type objectName: str
+
         @param objectText: Some iCalendar text to populate it with.
         @type objectText: str
         """
@@ -167,7 +169,6 @@
 
         @param path: the path from the root of the site (not starting with a
             slash)
-
         @type path: C{str}
 
         @param method: the HTTP method to initialize the request with.
@@ -266,8 +267,8 @@
 
     def test_createStore(self):
         """
-        Creating a DirectoryCalendarHomeProvisioningResource will create a paired
-        CalendarStore.
+        Creating a DirectoryCalendarHomeProvisioningResource will create a
+        paired CalendarStore.
         """
         assertProvides(self, IDataStore, self.calendarCollection._newStore)
 
@@ -518,7 +519,20 @@
                           frozenset([self.principalsResource]))
 
 
+    @inlineCallbacks
+    def assertCalendarEmpty(self, user, calendarName="calendar"):
+        """
+        Assert that a user's calendar is empty (their default calendar by default).
+        """
+        txn = self.calendarStore.newTransaction()
+        self.addCleanup(txn.commit)
+        home = yield txn.calendarHomeWithUID(user, create=True)
+        cal = yield home.calendarWithName(calendarName)
+        objects = yield cal.calendarObjects()
+        self.assertEquals(len(objects), 0)
 
+
+
 class DatabaseWrappingTests(WrappingTests):
 
     @inlineCallbacks
@@ -531,4 +545,73 @@
         return self.calendarStore
 
 
+    @inlineCallbacks
+    def test_invalidCalendarPUT(self):
+        """
+        Exceeding quota on an attachment returns an HTTP error code.
+        """
+        # yield self.populateOneObject("1.ics", test_event_text)
+        @inlineCallbacks
+        def putEvt(txt):
+            calendarObject = yield self.getResource(
+                "/calendars/users/wsanchez/calendar/1.ics",
+                "PUT", "wsanchez"
+            )
+            self.requestUnderTest.stream = MemoryStream(txt)
+            returnValue(
+                ((yield calendarObject.renderHTTP(self.requestUnderTest)),
+                 self.requestUnderTest)
+            )
+        # see twistedcaldav/directory/test/accounts.xml
+        wsanchez = '6423F94A-6B76-4A3A-815B-D52CFD77935D'
+        cdaboo = '5A985493-EE2C-4665-94CF-4DFEA3A89500'
+        eventTemplate="""\
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+UID:20060110T231240Z-4011c71-187-6f73
+ORGANIZER:urn:uuid:{wsanchez}
+ATTENDEE:urn:uuid:{wsanchez}
+DTSTART:20110101T050000Z
+DTSTAMP:20110309T185105Z
+DURATION:PT1H
+SUMMARY:Test
+RRULE:FREQ=DAILY;COUNT=2
+END:VEVENT
+BEGIN:VEVENT
+UID:20060110T231240Z-4011c71-187-6f73
+RECURRENCE-ID:20110102T050000Z
+ORGANIZER:urn:uuid:{wsanchez}
+ATTENDEE:urn:uuid:{wsanchez}
+ATTENDEE:urn:uuid:{cdaboo}
+DTSTART:20110102T050000Z
+DTSTAMP:20110309T185105Z
+DURATION:PT1H
+SUMMARY:Test
+END:VEVENT{0}
+END:VCALENDAR
+"""
+        CR = "\n"
+        CRLF = "\r\n"
+        #validEvent = eventTemplate.format("", wsanchez=wsanchez, cdaboo=cdaboo).replace(CR, CRLF)
+        invalidInstance = """
+BEGIN:VEVENT
+UID:20060110T231240Z-4011c71-187-6f73
+RECURRENCE-ID:20110110T050000Z
+ORGANIZER:urn:uuid:{wsanchez}
+ATTENDEE:urn:uuid:{wsanchez}
+DTSTART:20110110T050000Z
+DTSTAMP:20110309T185105Z
+DURATION:PT1H
+SUMMARY:Test
+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)
+        self.requestUnderTest = None
+        yield self.assertCalendarEmpty(wsanchez)
+        yield self.assertCalendarEmpty(cdaboo)
 
+

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/timezonexml.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/timezonexml.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/timezonexml.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -18,8 +18,8 @@
 This module provides XML definitions for use with Timezone Standard Service.
 """
 
-from txdav.xml import element as davxml
 from txdav.xml.element import registerElement
+from txdav.xml.element import WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
 
 
 ##
@@ -30,7 +30,7 @@
 
 
 @registerElement
-class Capabilities (davxml.WebDAVElement):
+class Capabilities (WebDAVElement):
     namespace = timezone_namespace
     name = "capabilities"
     allowed_children = {
@@ -38,8 +38,9 @@
     }
 
 
+
 @registerElement
-class Operation (davxml.WebDAVElement):
+class Operation (WebDAVElement):
     namespace = timezone_namespace
     name = "operation"
     allowed_children = {
@@ -49,20 +50,23 @@
     }
 
 
+
 @registerElement
-class Action (davxml.WebDAVTextElement):
+class Action (WebDAVTextElement):
     namespace = timezone_namespace
     name = "action"
 
 
+
 @registerElement
-class Description (davxml.WebDAVTextElement):
+class Description (WebDAVTextElement):
     namespace = timezone_namespace
     name = "description"
 
 
+
 @registerElement
-class AcceptParameter (davxml.WebDAVElement):
+class AcceptParameter (WebDAVElement):
     namespace = timezone_namespace
     name = "accept-parameter"
     allowed_children = {
@@ -74,32 +78,37 @@
     }
 
 
+
 @registerElement
-class Name (davxml.WebDAVTextElement):
+class Name (WebDAVTextElement):
     namespace = timezone_namespace
     name = "name"
 
 
+
 @registerElement
-class Required (davxml.WebDAVTextElement):
+class Required (WebDAVTextElement):
     namespace = timezone_namespace
     name = "required"
 
 
+
 @registerElement
-class Multi (davxml.WebDAVTextElement):
+class Multi (WebDAVTextElement):
     namespace = timezone_namespace
     name = "multi"
 
 
+
 @registerElement
-class Value (davxml.WebDAVTextElement):
+class Value (WebDAVTextElement):
     namespace = timezone_namespace
     name = "value"
 
 
+
 @registerElement
-class TimezoneList (davxml.WebDAVElement):
+class TimezoneList (WebDAVElement):
     namespace = timezone_namespace
     name = "timezone-list"
     allowed_children = {
@@ -108,14 +117,16 @@
     }
 
 
+
 @registerElement
-class Dtstamp (davxml.WebDAVTextElement):
+class Dtstamp (WebDAVTextElement):
     namespace = timezone_namespace
     name = "dtstamp"
 
 
+
 @registerElement
-class Summary (davxml.WebDAVElement):
+class Summary (WebDAVElement):
     namespace = timezone_namespace
     name = "summary"
     allowed_children = {
@@ -127,38 +138,44 @@
     }
 
 
+
 @registerElement
-class Tzid (davxml.WebDAVTextElement):
+class Tzid (WebDAVTextElement):
     namespace = timezone_namespace
     name = "tzid"
 
 
+
 @registerElement
-class LastModified (davxml.WebDAVTextElement):
+class LastModified (WebDAVTextElement):
     namespace = timezone_namespace
     name = "last-modified"
 
 
+
 @registerElement
-class LocalName (davxml.WebDAVTextElement):
+class LocalName (WebDAVTextElement):
     namespace = timezone_namespace
     name = "local-name"
 
 
+
 @registerElement
-class Alias (davxml.WebDAVTextElement):
+class Alias (WebDAVTextElement):
     namespace = timezone_namespace
     name = "alias"
 
 
+
 @registerElement
-class Inactive (davxml.WebDAVEmptyElement):
+class Inactive (WebDAVEmptyElement):
     namespace = timezone_namespace
     name = "inactive"
 
 
+
 @registerElement
-class Timezones (davxml.WebDAVElement):
+class Timezones (WebDAVElement):
     namespace = timezone_namespace
     name = "timezones"
     allowed_children = {
@@ -167,8 +184,9 @@
     }
 
 
+
 @registerElement
-class Tzdata (davxml.WebDAVElement):
+class Tzdata (WebDAVElement):
     namespace = timezone_namespace
     name = "tzdata"
     allowed_children = {
@@ -178,14 +196,16 @@
     }
 
 
+
 @registerElement
-class Calscale (davxml.WebDAVTextElement):
+class Calscale (WebDAVTextElement):
     namespace = timezone_namespace
     name = "calscale"
 
 
+
 @registerElement
-class Observance (davxml.WebDAVElement):
+class Observance (WebDAVElement):
     namespace = timezone_namespace
     name = "observance"
     allowed_children = {
@@ -197,19 +217,22 @@
     }
 
 
+
 @registerElement
-class Onset (davxml.WebDAVTextElement):
+class Onset (WebDAVTextElement):
     namespace = timezone_namespace
     name = "onset"
 
 
+
 @registerElement
-class UTCOffsetFrom (davxml.WebDAVTextElement):
+class UTCOffsetFrom (WebDAVTextElement):
     namespace = timezone_namespace
     name = "utc-offset-from"
 
 
+
 @registerElement
-class UTCOffsetTo (davxml.WebDAVTextElement):
+class UTCOffsetTo (WebDAVTextElement):
     namespace = timezone_namespace
     name = "utc-offset-to"

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/upgrade.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/upgrade.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/upgrade.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -17,7 +17,17 @@
 
 from __future__ import with_statement
 
-import xattr, os, zlib, hashlib, datetime, pwd, grp, shutil, errno, operator, time
+import xattr
+import os
+import zlib
+import hashlib
+import datetime
+import pwd
+import grp
+import shutil
+import errno
+import operator
+import time
 from zlib import compress
 from cPickle import loads as unpickle, UnpicklingError
 
@@ -34,8 +44,8 @@
 from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.ical import Component
-from twistedcaldav.mail import MailGatewayTokensDatabase
 from twistedcaldav.scheduling.cuaddress import LocalCalendarUser
+from twistedcaldav.scheduling.imip.mailgateway import MailGatewayTokensDatabase
 from twistedcaldav.scheduling.scheduler import DirectScheduler
 from twistedcaldav.util import normalizationLookup
 
@@ -69,6 +79,8 @@
 def xattrname(n):
     return deadPropertyXattrPrefix + n
 
+
+
 def getCalendarServerIDs(config):
 
     # Determine uid/gid for ownership of directories we create here
@@ -89,6 +101,7 @@
     return uid, gid
 
 
+
 def fixBadQuotes(data):
     if (
         data.find('\\"') != -1 or
@@ -182,7 +195,6 @@
 
             collectionUpdated = True
 
-
     if collectionUpdated:
         ctagValue = "<?xml version='1.0' encoding='UTF-8'?>\r\n<getctag xmlns='http://calendarserver.org/ns/'>%s</getctag>\r\n" % (str(datetime.datetime.now()),)
         ctagValue = zlib.compress(ctagValue)
@@ -295,6 +307,7 @@
             log.err(f)
         errorOccurred.append(True)
 
+
     def doProxyDatabaseMoveUpgrade(config, uid=-1, gid=-1):
         # See if the new one is already present
         oldFilename = ".db.calendaruserproxy"
@@ -403,8 +416,6 @@
         if os.path.exists(journalPath):
             os.chown(journalPath, uid, gid)
 
-
-
     cuaCache = {}
 
     docRoot = config.DocumentRoot
@@ -480,7 +491,6 @@
 
                     os.rmdir(dirPath)
 
-
             # Count how many calendar homes we'll be processing, and build
             # list of pending inbox items
             total = 0
@@ -522,7 +532,7 @@
                     spawner.startService()
                     parallelizer = Parallelizer((yield gatherResults(
                         [spawner.spawnWithConfig(config, To1Driver(), To1Home)
-                         for x in xrange(parallel)]
+                         for _ignore_x in xrange(parallel)]
                     )))
                 log.warn("Processing %d calendar homes in %s" % (total, uidHomes))
 
@@ -576,6 +586,7 @@
         log.warn("Data upgrade encountered errors but will proceed; see error.log for details")
 
 
+
 def normalizeCUAddrs(data, directory, cuaCache):
     """
     Normalize calendar user addresses to urn:uuid: form.
@@ -598,7 +609,7 @@
     def lookupFunction(cuaddr, principalFunction, config):
 
         # Return cached results, if any.
-        if cuaCache.has_key(cuaddr):
+        if cuaddr in cuaCache:
             return cuaCache[cuaddr]
 
         result = normalizationLookup(cuaddr, principalFunction, config)
@@ -617,8 +628,6 @@
 
 @inlineCallbacks
 def upgrade_to_2(config, spawner, parallel, directory):
-    
-    errorOccurred = False
 
     def renameProxyDB():
         #
@@ -626,12 +635,13 @@
         #
         oldFilename = "calendaruserproxy.sqlite"
         newFilename = "proxies.sqlite"
-    
+
         oldDbPath = os.path.join(config.DataRoot, oldFilename)
         newDbPath = os.path.join(config.DataRoot, newFilename)
         if os.path.exists(oldDbPath) and not os.path.exists(newDbPath):
             os.rename(oldDbPath, newDbPath)
 
+
     def flattenHome(calHome):
 
         log.debug("Flattening calendar home: %s" % (calHome,))
@@ -643,19 +653,19 @@
                     # Skip non-directories; these might have been uploaded by a
                     # random DAV client, they can't be calendar collections.
                     continue
-                
+
                 if cal in ("dropbox",):
                     continue
                 if os.path.exists(os.path.join(calPath, db_basename)):
                     continue
-                
+
                 # Commented this out because it is only needed if there are calendars nested inside of regular collections.
                 # Whilst this is technically possible in early versions of the servers the main clients did not support it.
                 # Also, the v1 upgrade does not look at nested calendars for cu-address normalization.
                 # However, we do still need to "ignore" regular collections in the calendar home so what we do is rename them
                 # with a ".collection." prefix.
 #                def scanCollection(collection):
-#                    
+#
 #                    for child in os.listdir(collection):
 #                        childCollection = os.path.join(collection, child)
 #                        if os.path.isdir(childCollection):
@@ -677,15 +687,16 @@
         except Exception, e:
             log.error("Failed to upgrade calendar home %s: %s" % (calHome, e))
             return succeed(False)
-        
+
         return succeed(True)
 
+
     def flattenHomes():
         """
         Make sure calendars inside regular collections are all moved to the top level.
         """
         errorOccurred = False
-    
+
         log.debug("Flattening calendar homes")
 
         docRoot = config.DocumentRoot
@@ -693,7 +704,7 @@
             calRoot = os.path.join(docRoot, "calendars")
             if os.path.exists(calRoot) and os.path.isdir(calRoot):
                 uidHomes = os.path.join(calRoot, "__uids__")
-                if os.path.isdir(uidHomes): 
+                if os.path.isdir(uidHomes):
                     for path1 in os.listdir(uidHomes):
                         uidLevel1 = os.path.join(uidHomes, path1)
                         if not os.path.isdir(uidLevel1):
@@ -716,9 +727,7 @@
     # Move auto-schedule from resourceinfo sqlite to augments:
     yield migrateAutoSchedule(config, directory)
 
-    errorOccurred = flattenHomes()
-
-    if errorOccurred:
+    if flattenHomes():
         raise UpgradeError("Data upgrade failed, see error.log for details")
 
 
@@ -776,6 +785,8 @@
     if os.path.exists(triggerPath):
         os.remove(triggerPath)
 
+
+
 class UpgradeError(RuntimeError):
     """
     Generic upgrade error.
@@ -783,6 +794,7 @@
     pass
 
 
+
 #
 # Utility functions
 #
@@ -805,6 +817,7 @@
     return newHref
 
 
+
 def updateFreeBusySet(value, directory):
 
     try:
@@ -845,6 +858,7 @@
     return None # no update required
 
 
+
 def makeDirsUserGroup(path, uid=-1, gid=-1):
     parts = path.split("/")
     if parts[0] == "": # absolute path
@@ -860,6 +874,7 @@
             os.chown(path, uid, gid)
 
 
+
 def archive(config, srcPath, uid, gid):
     """
     Move srcPath into dataroot/archived, giving the destination a unique
@@ -889,6 +904,7 @@
         os.remove(srcPath)
 
 
+
 DELETECHARS = ''.join(chr(i) for i in xrange(32) if i not in (9, 10, 13))
 def removeIllegalCharacters(data):
     """
@@ -898,7 +914,7 @@
     the data changed.
     """
     beforeLen = len(data)
-    data =  data.translate(None, DELETECHARS)
+    data = data.translate(None, DELETECHARS)
     afterLen = len(data)
     if afterLen != beforeLen:
         return data, True
@@ -906,6 +922,7 @@
         return data, False
 
 
+
 # Deferred
 def migrateFromOD(config, directory):
     #
@@ -927,6 +944,7 @@
     return migrateResources(userService, resourceService)
 
 
+
 @inlineCallbacks
 def migrateAutoSchedule(config, directory):
     # Fetch the autoSchedule assignments from resourceinfo.sqlite and store
@@ -953,6 +971,7 @@
             log.warn("Migrated %d auto-schedule settings" % (len(augmentRecords),))
 
 
+
 class UpgradeFileSystemFormatService(Service, object):
     """
     Upgrade filesystem from previous versions.
@@ -1018,6 +1037,7 @@
         self.store = store
         self.config = config
 
+
     @inlineCallbacks
     def startService(self):
         """
@@ -1087,7 +1107,7 @@
                 totalItems = len(itemsToProcess)
                 ignoreUUIDs = set()
                 for ctr, inboxItem in enumerate(itemsToProcess):
-                    log.info("Processing %d/%d inbox item: %s" % (ctr+1, totalItems, inboxItem,))
+                    log.info("Processing %d/%d inbox item: %s" % (ctr + 1, totalItems, inboxItem,))
                     ignore, uuid, ignore, fileName = inboxItem.rsplit("/", 3)
 
                     if uuid in ignoreUUIDs:
@@ -1156,7 +1176,6 @@
                         else:
                             log.debug("Ignored inbox item - no inbox: %s" % (inboxItem,))
 
-
                         inboxItems.remove(inboxItem)
 
                     finally:

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/base/datastore/subpostgres.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/base/datastore/subpostgres.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/base/datastore/subpostgres.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -1,5 +1,5 @@
 # -*- test-case-name: txdav.base.datastore.test.test_subpostgres -*-
-##
+# #
 # Copyright (c) 2010-2012 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,7 @@
 # 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.
-##
+# #
 
 """
 Run and manage PostgreSQL as a subprocess.
@@ -69,8 +69,9 @@
             if _MAGIC_READY_COOKIE in line:
                 self.svc.ready()
 
+    disconnecting = False
 
-    disconnecting = False
+
     def connectionMade(self):
         self.lineReceiver.makeConnection(self)
 
@@ -163,7 +164,8 @@
                  listenAddresses=[], sharedBuffers=30,
                  maxConnections=20, options=[],
                  testMode=False,
-                 uid=None, gid=None):
+                 uid=None, gid=None,
+                 spawnedDBUser="caldav"):
         """
         Initialize a L{PostgresService} pointed at a data store directory.
 
@@ -190,31 +192,39 @@
         # completed, our stopService( ) examines the delayedShutdown flag.
         # If True, we wait on the shutdownDeferred to fire before proceeding.
         # The deferred gets fired once database init is complete.
-        self.delayedShutdown = False # set to True when in critical code
-        self.shutdownDeferred = None # the actual deferred
+        self.delayedShutdown = False  # set to True when in critical code
+        self.shutdownDeferred = None  # the actual deferred
 
         # Options from config
         self.databaseName = databaseName
         self.logFile = logFile
-        if socketDir:
-            # Unix socket length path limit
-            self.socketDir = CachingFilePath("%s/ccs_postgres_%s/" %
-                (socketDir, md5(dataStoreDirectory.path).hexdigest()))
-            if len(self.socketDir.path) > 64:
-                socketDir = "/tmp"
-                self.socketDir = CachingFilePath("/tmp/ccs_postgres_%s/" %
-                    (md5(dataStoreDirectory.path).hexdigest()))
-            self.host = self.socketDir.path
+        if listenAddresses:
+            self.socketDir = None
+            self.host, self.port = listenAddresses[0].split(":") if ":" in listenAddresses[0] else (listenAddresses[0], None,)
+            self.listenAddresses = [addr.split(":")[0] for addr in listenAddresses]
         else:
-            self.socketDir = None
-            self.host = "localhost"
-        self.listenAddresses = listenAddresses
+            if socketDir:
+                # Unix socket length path limit
+                self.socketDir = CachingFilePath("%s/ccs_postgres_%s/" %
+                    (socketDir, md5(dataStoreDirectory.path).hexdigest()))
+                if len(self.socketDir.path) > 64:
+                    socketDir = "/tmp"
+                    self.socketDir = CachingFilePath("/tmp/ccs_postgres_%s/" %
+                        (md5(dataStoreDirectory.path).hexdigest()))
+                self.host = self.socketDir.path
+                self.port = None
+            else:
+                self.socketDir = None
+                self.host = "localhost"
+                self.port = None
+            self.listenAddresses = []
         self.sharedBuffers = sharedBuffers if not testMode else 16
         self.maxConnections = maxConnections if not testMode else 4
         self.options = options
 
         self.uid = uid
         self.gid = gid
+        self.spawnedDBUser = spawnedDBUser
         self.schema = schema
         self.monitor = None
         self.openConnections = []
@@ -246,14 +256,21 @@
         if databaseName is None:
             databaseName = self.databaseName
 
-        if self.uid is not None:
+        if self.spawnedDBUser:
+            dsn = "%s:dbname=%s:%s" % (self.host, databaseName, self.spawnedDBUser)
+        elif self.uid is not None:
             dsn = "%s:dbname=%s:%s" % (self.host, databaseName,
                 pwd.getpwuid(self.uid).pw_name)
         else:
             dsn = "%s:dbname=%s" % (self.host, databaseName)
-        return DBAPIConnector(pgdb, postgresPreflight, dsn)
 
+        kwargs = {}
+        if self.port:
+            kwargs["host"] = "%s:%s" % (self.host, self.port,)
 
+        return DBAPIConnector(pgdb, postgresPreflight, dsn, **kwargs)
+
+
     def produceConnection(self, label="<unlabeled>", databaseName=None):
         """
         Produce a DB-API 2.0 connection pointed at this database.
@@ -314,17 +331,19 @@
 #        for pipe in self.monitor.transport.pipes.values():
 #            pipe.stopReading()
 #            pipe.stopWriting()
+        pass
 
 
     def unpauseMonitor(self):
         """
         Unpause monitoring.
 
-        @see: L{pauseMonitor} 
+        @see: L{pauseMonitor}
         """
 #        for pipe in self.monitor.transport.pipes.values():
 #            pipe.startReading()
 #            pipe.startWriting()
+        pass
 
 
     def startDatabase(self):
@@ -341,6 +360,8 @@
         )
         if self.socketDir:
             options.append("-k '%s'" % (self.socketDir.path,))
+        if self.port:
+            options.append("-c port=%s" % (self.port,))
         options.append("-c shared_buffers=%d" % (self.sharedBuffers,))
         options.append("-c max_connections=%d" % (self.maxConnections,))
         options.append("-c standard_conforming_strings=on")
@@ -381,7 +402,8 @@
         workingDir = self.dataStoreDirectory.child("working")
         env = self.env = os.environ.copy()
         env.update(PGDATA=clusterDir.path,
-                   PGHOST=self.host)
+                   PGHOST=self.host,
+                   PGUSER=self.spawnedDBUser)
         initdb = which("initdb")[0]
         if self.socketDir:
             if not self.socketDir.isdir():
@@ -401,7 +423,7 @@
             dbInited = Deferred()
             reactor.spawnProcess(
                 CapturingProcessProtocol(dbInited, None),
-                initdb, [initdb, "-E", "UTF8"], env, workingDir.path,
+                initdb, [initdb, "-E", "UTF8", "-U", self.spawnedDBUser], env, workingDir.path,
                 uid=self.uid, gid=self.gid,
             )
             def doCreate(result):

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/base/propertystore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/base/propertystore/sql.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/base/propertystore/sql.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -30,6 +30,8 @@
     Select, Parameter, Update, Insert, TableSyntax, Delete)
 
 from txdav.xml.parser import WebDAVDocument
+from txdav.common.icommondatastore import AllRetriesFailed
+from twext.python.log import LoggingMixIn
 from txdav.common.datastore.sql_tables import schema
 from txdav.base.propertystore.base import (AbstractPropertyStore,
                                            PropertyName, validKey)
@@ -39,7 +41,7 @@
 
 prop = schema.RESOURCE_PROPERTY
 
-class PropertyStore(AbstractPropertyStore):
+class PropertyStore(AbstractPropertyStore, LoggingMixIn):
 
     _cacher = Memcacher("SQL.props", pickle=True, key_normalization=False)
 
@@ -255,7 +257,10 @@
         if hasattr(self, "_notifyCallback") and self._notifyCallback is not None:
             self._notifyCallback()
 
-        self._txn.subtransaction(trySetItem)
+        def justLogIt(f):
+            f.trap(AllRetriesFailed)
+            self.log_error("setting a property failed; probably nothing.")
+        self._txn.subtransaction(trySetItem).addErrback(justLogIt)
 
 
 

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/file.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/file.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/file.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -34,8 +34,6 @@
 
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed, fail
 
-from twisted.python.failure import Failure
-
 from twext.python.vcomponent import VComponent
 from txdav.xml import element as davxml
 from txdav.xml.rfc2518 import ResourceType, GETContentType
@@ -55,7 +53,7 @@
     IndexSchedule as OldInboxIndex
 from txdav.caldav.datastore.util import (
     validateCalendarComponent, dropboxIDFromCalendarObject, CalendarObjectBase,
-    StorageTransportBase
+    StorageTransportBase, AttachmentRetrievalTransport
 )
 
 from txdav.common.datastore.file import (
@@ -735,6 +733,7 @@
     def write(self, data):
         # FIXME: multiple chunks
         self._file.write(data)
+        return super(AttachmentStorageTransport, self).write(data)
 
 
     def loseConnection(self):
@@ -799,12 +798,7 @@
 
 
     def retrieve(self, protocol):
-        # FIXME: makeConnection
-        # FIXME: actually stream
-        # FIMXE: connectionLost
-        protocol.dataReceived(self._path.getContent())
-        # FIXME: ConnectionDone, not NotImplementedError
-        protocol.connectionLost(Failure(NotImplementedError()))
+        return AttachmentRetrievalTransport(self._path).start(protocol)
 
 
     @property

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/sql.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/sql.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -32,9 +32,7 @@
 from twext.python.filepath import CachingFilePath
 
 from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.internet.error import ConnectionLost
 from twisted.python import hashlib
-from twisted.python.failure import Failure
 
 from twistedcaldav import caldavxml, customxml
 from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
@@ -80,6 +78,8 @@
 from pycalendar.duration import PyCalendarDuration
 from pycalendar.timezone import PyCalendarTimezone
 
+from txdav.caldav.datastore.util import AttachmentRetrievalTransport
+
 from zope.interface.declarations import implements
 
 import os
@@ -1485,8 +1485,7 @@
 
 
     def retrieve(self, protocol):
-        protocol.dataReceived(self._path.getContent())
-        protocol.connectionLost(Failure(ConnectionLost()))
+        return AttachmentRetrievalTransport(self._path).start(protocol)
 
 
     _removeStatement = Delete(

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/common.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/common.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -21,7 +21,7 @@
 
 from StringIO import StringIO
 
-from twisted.internet.defer import Deferred, inlineCallbacks, returnValue,\
+from twisted.internet.defer import Deferred, inlineCallbacks, returnValue, \
     maybeDeferred
 from twisted.internet.protocol import Protocol
 from twisted.python import hashlib
@@ -427,7 +427,7 @@
     def test_notificationSyncToken(self):
         """
         L{ICalendar.resourceNamesSinceToken} will return the names of calendar
-        objects changed or deleted since 
+        objects changed or deleted since
         """
         txn = self.transactionUnderTest()
         coll = yield txn.notificationsWithUID("home1")
@@ -574,12 +574,14 @@
         self.assertEquals(calendar.notifierID(), "CalDAV|home1")
         self.assertEquals(calendar.notifierID(label="collection"), "CalDAV|home1/calendar_1")
 
+
     @inlineCallbacks
     def test_nodeNameSuccess(self):
         home = yield self.homeUnderTest()
         name = yield home.nodeName()
         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
@@ -589,7 +591,66 @@
         name = yield home.nodeName()
         self.assertEquals(name, None)
 
+
     @inlineCallbacks
+    def test_calendarHomes(self):
+        """
+        Finding all existing calendar homes.
+        """
+        calendarHomes = (yield self.transactionUnderTest().calendarHomes())
+        self.assertEquals(
+            [home.name() for home in calendarHomes],
+            [
+                "home1",
+                "home_no_splits",
+                "home_splits",
+                "home_splits_shared",
+            ]
+        )
+
+
+    @inlineCallbacks
+    def test_displayNameNone(self):
+        """
+        L{ICalendarHome.calendarWithName} returns C{None} for calendars which
+        do not exist.
+        """
+        home = (yield self.homeUnderTest())
+        calendar = (yield home.calendarWithName("calendar_1"))
+        name = (yield calendar.displayName())
+        self.assertEquals(name, None)
+
+
+    @inlineCallbacks
+    def test_setDisplayName(self):
+        """
+        L{ICalendarHome.calendarWithName} returns C{None} for calendars which
+        do not exist.
+        """
+        home = (yield self.homeUnderTest())
+        calendar = (yield home.calendarWithName("calendar_1"))
+
+        calendar.setDisplayName(u"quux")
+        name = calendar.displayName()
+        self.assertEquals(name, u"quux")
+
+        calendar.setDisplayName(None)
+        name = calendar.displayName()
+        self.assertEquals(name, None)
+
+
+    @inlineCallbacks
+    def test_setDisplayNameBytes(self):
+        """
+        L{ICalendarHome.calendarWithName} returns C{None} for calendars which
+        do not exist.
+        """
+        home = (yield self.homeUnderTest())
+        calendar = (yield home.calendarWithName("calendar_1"))
+        self.assertRaises(ValueError, calendar.setDisplayName, "quux")
+
+
+    @inlineCallbacks
     def test_calendarHomeWithUID_exists(self):
         """
         Finding an existing calendar home by UID results in an object that
@@ -640,6 +701,7 @@
             self.assertProvides(ICalendar, calendar)
             self.assertEquals(calendar.name(), name)
 
+
     @inlineCallbacks
     def test_calendarWithName_exists(self):
         """
@@ -736,7 +798,7 @@
         """
         L{ICalendarHome.createCalendarWithName} raises
         L{CalendarAlreadyExistsError} when the name conflicts with an already-
-        existing 
+        existing
         """
         home = yield self.homeUnderTest()
         for name in home1_calendarNames:
@@ -806,23 +868,25 @@
         result = yield maybeDeferred(calendar.getSupportedComponents)
         self.assertEquals(result, None)
 
+
     @inlineCallbacks
     def test_countComponentTypes(self):
         """
         Test Calendar._countComponentTypes to make sure correct counts are returned.
         """
-        
+
         tests = (
             ("calendar_1", (("VEVENT", 3),)),
             ("calendar_2", (("VEVENT", 3), ("VTODO", 2))),
         )
-        
+
         for calname, results in tests:
             testalendar = yield (yield self.transactionUnderTest().calendarHomeWithUID(
                 "home_splits")).calendarWithName(calname)
             result = yield maybeDeferred(testalendar._countComponentTypes)
             self.assertEquals(result, results)
 
+
     @inlineCallbacks
     def test_calendarObjects(self):
         """
@@ -947,6 +1011,7 @@
             ]
         )
 
+
     @inlineCallbacks
     def test_removeCalendarObjectWithName_exists(self):
         """
@@ -1044,6 +1109,7 @@
         invitedCals = yield cal.asShared()
         self.assertEqual(len(invitedCals), 0)
 
+
     @inlineCallbacks
     def test_unshareSharerSide(self, commit=False):
         """
@@ -1063,6 +1129,7 @@
         invitedCals = yield cal.asShared()
         self.assertEqual(len(invitedCals), 0)
 
+
     @inlineCallbacks
     def test_unshareShareeSide(self, commit=False):
         """
@@ -1082,6 +1149,7 @@
         invitedCals = yield cal.asShared()
         self.assertEqual(len(invitedCals), 0)
 
+
     @inlineCallbacks
     def test_unshareWithInDifferentTransaction(self):
         """
@@ -1130,7 +1198,7 @@
 
         result = (yield home.hasCalendarResourceUIDSomewhereElse("uid2", object, "schedule"))
         self.assertTrue(result)
-        
+
         # FIXME:  do this without legacy calls
         '''
         from twistedcaldav.sharing import SharedCollectionRecord
@@ -1247,7 +1315,6 @@
         self.assertEquals(component.mainType(), "VEVENT")
         self.assertEquals(component.resourceUID(), "uid1")
 
-
     perUserComponent = lambda self: VComponent.fromString("""BEGIN:VCALENDAR
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 VERSION:2.0
@@ -1282,7 +1349,6 @@
 END:VCALENDAR
 """.replace("\n", "\r\n"))
 
-
     asSeenByOwner = lambda self: VComponent.fromString("""BEGIN:VCALENDAR
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 VERSION:2.0
@@ -1302,7 +1368,6 @@
 END:VCALENDAR
 """.replace("\n", "\r\n"))
 
-
     asSeenByOther = lambda self: VComponent.fromString("""BEGIN:VCALENDAR
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 VERSION:2.0
@@ -1420,6 +1485,7 @@
             set(home1_calendarNames)
         )
 
+
     @inlineCallbacks
     def test_loadAllCalendars(self):
         """
@@ -1441,7 +1507,7 @@
             set(c.name() for c in calendars),
             set(home1_calendarNames)
         )
-        
+
         for c in calendars:
             self.assertTrue(c.properties() is not None)
 
@@ -1526,6 +1592,7 @@
             InvalidObjectResourceError,
         )
 
+
     @inlineCallbacks
     def test_setComponent_invalid(self):
         """
@@ -1695,7 +1762,7 @@
             self.assertNotEquals(event1_text, event1_text_withDifferentSubject)
             newComponent = VComponent.fromString(event1_text_withDifferentSubject)
             yield obj.setComponent(newComponent)
-    
+
             # Putting everything into a separate transaction to account for any
             # caching that may take place.
             yield self.commit()
@@ -1704,7 +1771,6 @@
                 propertyContent
             )
 
-
     eventWithDropbox = "\r\n".join("""
 BEGIN:VCALENDAR
 CALSCALE:GREGORIAN
@@ -1813,7 +1879,7 @@
     def test_collectionSyncToken(self):
         """
         L{ICalendar.resourceNamesSinceToken} will return the names of calendar
-        objects changed or deleted since 
+        objects changed or deleted since
         """
         cal = yield self.calendarUnderTest()
         st = yield cal.syncToken()
@@ -2037,7 +2103,7 @@
         that fails with L{QuotaExceeded}.
         """
         home = yield self.homeUnderTest()
-        attachment = yield getit() 
+        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)))
@@ -2080,27 +2146,24 @@
         t.write(sampleData)
         yield t.loseConnection()
         yield self.exceedQuotaTest(get)
+        @inlineCallbacks
         def checkOriginal():
-            catch = StringIO()
-            catch.dataReceived = catch.write
-            lost = []
-            catch.connectionLost = lost.append
-            attachment.retrieve(catch)
+            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 = catch.getvalue()[:60]
+            actual = actual[:60]
             self.assertEquals(actual, expected)
-        checkOriginal()
+        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()
-        checkOriginal()
+        yield checkOriginal()
 
 
-    def test_removeAttachmentWithName(self, refresh=lambda x:x):
+    def test_removeAttachmentWithName(self, refresh=lambda x: x):
         """
         L{ICalendarObject.removeAttachmentWithName} will remove the calendar
         object with the given name.
@@ -2155,7 +2218,7 @@
 
     @inlineCallbacks
     def test_finishedOnCommit(self):
-        """ 
+        """
         Calling L{ITransaction.abort} or L{ITransaction.commit} after
         L{ITransaction.commit} has already been called raises an
         L{AlreadyFinishedError}.
@@ -2237,4 +2300,3 @@
         additionalUIDs.add("home_attachments")
         expectedUIDs = additionalUIDs.union(requiredUIDs)
         self.assertEquals(foundUIDs, expectedUIDs)
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_file.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_file.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_file.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -67,7 +67,7 @@
     calendarPath.parent().makedirs()
     storePath.copyTo(calendarPath)
 
-    # Set year values to current year    
+    # Set year values to current year
     nowYear = PyCalendarDateTime.getToday().getYear()
     for home in calendarPath.child("ho").child("me").children():
         if not home.basename().startswith("."):
@@ -75,7 +75,7 @@
                 if not calendar.basename().startswith("."):
                     for resource in calendar.children():
                         if resource.basename().endswith(".ics"):
-                            resource.setContent(resource.getContent() % {"now":nowYear})
+                            resource.setContent(resource.getContent() % {"now": nowYear})
 
     testID = test.id()
     test.calendarStore = CalendarStore(storeRootPath, test.notifierFactory,
@@ -314,8 +314,8 @@
             self.calendar1.removeCalendarObjectWithName, name
         )
 
+    counter = 0
 
-    counter = 0
     @inlineCallbacks
     def _refresh(self):
         """
@@ -442,7 +442,7 @@
         L{CalendarObject} has instance attributes, C{_path} and C{_calendar},
         which refer to its position in the filesystem and the calendar in which
         it is contained, respectively.
-        """ 
+        """
         self.failUnless(
             isinstance(self.object1._path, FilePath),
             self.object1._path
@@ -482,7 +482,6 @@
         Overridden to be skipped.
         """
 
-
     # TODO: ideally the file store would support all of this sharing stuff.
     test_shareWith.skip = "Not implemented for file store yet."
     test_shareAgainChangesMode = test_shareWith
@@ -503,6 +502,25 @@
 
 
     @inlineCallbacks
+    def test_calendarHomes(self):
+        """
+        Finding all existing calendar homes.
+        """
+        calendarHomes = (yield self.transactionUnderTest().calendarHomes())
+        self.assertEquals(
+            [home.name() for home in calendarHomes],
+            [
+                "home1",
+                "home_attachments",
+                "home_bad",
+                "home_no_splits",
+                "home_splits",
+                "home_splits_shared",
+            ]
+        )
+
+
+    @inlineCallbacks
     def test_calendarObjectsWithDotFile(self):
         """
         Adding a dotfile to the calendar home should not increase the number of

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/util.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/util.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/util.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -45,6 +45,11 @@
     InternalDataStoreError, HomeChildNameAlreadyExistsError
 )
 from txdav.base.datastore.util import normalizeUUIDOrNot
+from twisted.protocols.basic import FileSender
+from twisted.internet.interfaces import ITransport
+from twisted.internet.interfaces import IConsumer
+from twisted.internet.error import ConnectionLost
+from twisted.internet.task import LoopingCall
 
 log = Logger()
 
@@ -219,7 +224,7 @@
                 if exists is None:
                     newattachment = yield outObject.createAttachmentWithName(name)
                     transport = newattachment.store(ctype)
-                    proto =_AttachmentMigrationProto(transport)
+                    proto = _AttachmentMigrationProto(transport)
                     attachment.retrieve(proto)
                     yield proto.done
 
@@ -262,18 +267,18 @@
     # usual suspects.
     contentTypes.update(
         {
-            '.conf':  'text/plain',
-            '.diff':  'text/plain',
-            '.exe':   'application/x-executable',
-            '.flac':  'audio/x-flac',
-            '.java':  'text/plain',
-            '.ogg':   'application/ogg',
-            '.oz':    'text/x-oz',
-            '.swf':   'application/x-shockwave-flash',
-            '.tgz':   'application/x-gtar',
-            '.wml':   'text/vnd.wap.wml',
-            '.xul':   'application/vnd.mozilla.xul+xml',
-            '.py':    'text/plain',
+            '.conf': 'text/plain',
+            '.diff': 'text/plain',
+            '.exe': 'application/x-executable',
+            '.flac': 'audio/x-flac',
+            '.java': 'text/plain',
+            '.ogg': 'application/ogg',
+            '.oz': 'text/x-oz',
+            '.swf': 'application/x-shockwave-flash',
+            '.tgz': 'application/x-gtar',
+            '.wml': 'text/vnd.wap.wml',
+            '.xul': 'application/vnd.mozilla.xul+xml',
+            '.py': 'text/plain',
             '.patch': 'text/plain',
         }
     )
@@ -299,9 +304,15 @@
         self.storeTransport = storeTransport
         self.done = Deferred()
 
+
+    def connectionMade(self):
+        self.storeTransport.registerProducer(self.transport, False)
+
+
     def dataReceived(self, data):
         self.storeTransport.write(data)
 
+
     @inlineCallbacks
     def connectionLost(self, reason):
         try:
@@ -462,14 +473,34 @@
         Create a storage transport with a reference to an L{IAttachment} and a
         L{twext.web2.http_headers.MimeType}.
         """
+        from twisted.internet import reactor
+        self._clock = reactor
         self._attachment = attachment
         self._contentType = contentType
+        self._producer = None
 
         # Make sure we have some kind of contrent-type
         if self._contentType is None:
             self._contentType = http_headers.MimeType.fromString(getType(self._attachment.name(), self.contentTypes))
 
 
+    def write(self, data):
+        """
+        Children must override this to actually write the data, but should
+        upcall this implementation to interact properly with producers.
+        """
+        if self._producer and self._streamingProducer:
+            # XXX this needs to be in a callLater because otherwise
+            # resumeProducing will call write which will call resumeProducing
+            # (etc) forever.
+            self._clock.callLater(0, self._producer.resumeProducing)
+
+
+    def registerProducer(self, producer, streaming):
+        self._producer = producer
+        self._streamingProducer = streaming
+
+
     def getPeer(self):
         return StorageTransportAddress(self._attachment, False)
 
@@ -482,7 +513,84 @@
         return self.write(''.join(seq))
 
 
+    def stopProducing(self):
+        return self.loseConnection()
 
+
+
+class AttachmentRetrievalTransport(FileSender, object):
+    """
+    The transport for a protocol that does L{IAttachment.retrieve}.
+    """
+    implements(ITransport)
+
+    def __init__(self, filePath):
+        from twisted.internet import reactor
+        self.filePath = filePath
+        self.clock = reactor
+
+
+    def start(self, protocol):
+        this = self
+        class Consumer(object):
+            implements(IConsumer)
+            def registerProducer(self, producer, streaming):
+                protocol.makeConnection(producer)
+                this._maybeLoopDelivery()
+            def write(self, data):
+                protocol.dataReceived(data)
+            def unregisterProducer(self):
+                this._done(protocol)
+        self.beginFileTransfer(self.filePath.open(), Consumer())
+
+
+    def _done(self, protocol):
+        if self._deliveryLoop:
+            self._deliveryLoop.stop()
+        protocol.connectionLost(Failure(ConnectionLost()))
+
+
+    def write(self, data):
+        raise NotImplemented("This is a read-only transport.")
+
+
+    def writeSequence(self, datas):
+        self.write("".join(datas))
+
+
+    def loseConnection(self):
+        pass
+
+
+    def getPeer(self):
+        return self
+
+
+    def getHost(self):
+        return self
+
+    _everResumedProducing = False
+
+    def resumeProducing(self):
+        self._everResumedProducing = True
+        super(AttachmentRetrievalTransport, self).resumeProducing()
+
+    _deliveryLoop = None
+
+    def _maybeLoopDelivery(self):
+        """
+        If no consumer was registered (as inferred by the fact that
+        resumeProducing() wasn't called)
+        """
+        if not self._everResumedProducing:
+            # Not registered as a streaming producer.
+            def deliverNextChunk():
+                super(AttachmentRetrievalTransport, self).resumeProducing()
+            self._deliveryLoop = LoopingCall(deliverNextChunk)
+            self._deliveryLoop.start(0.01, True)
+
+
+
 def fixOneCalendarObject(component):
     """
     Correct the properties which may contain a user's directory UUID within a

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/icalendarstore.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/icalendarstore.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -72,6 +72,7 @@
         self.limit = lowerLimit
 
 
+
 class TimeRangeUpperLimit(Exception):
     """
     A request for time-range information too far in the future cannot be satisfied.
@@ -81,11 +82,20 @@
         self.limit = upperLimit
 
 
+
 class ICalendarTransaction(ICommonTransaction):
     """
     Transaction functionality required to be implemented by calendar stores.
     """
 
+    def calendarHomes():
+        """
+        Retrieve each calendar home in the store.
+
+        @return: a L{Deferred} which fires with a list of L{ICalendarHome}.
+        """
+
+
     def calendarHomeWithUID(uid, create=False):
         """
         Retrieve the calendar home for the principal with the given C{uid}.
@@ -115,6 +125,7 @@
         """
 
 
+
 #
 # Interfaces
 #
@@ -134,6 +145,7 @@
         @return: a string.
         """
 
+
     def calendars():
         """
         Retrieve calendars contained in this calendar home.
@@ -141,6 +153,7 @@
         @return: an iterable of L{ICalendar}s.
         """
 
+
     def loadCalendars():
         """
         Pre-load all calendars Depth:1.
@@ -148,6 +161,7 @@
         @return: an iterable of L{ICalendar}s.
         """
 
+
     def calendarWithName(name):
         """
         Retrieve the calendar with the given C{name} contained in this
@@ -185,6 +199,7 @@
             given C{name} already exists.
         """
 
+
     def removeCalendarWithName(name):
         """
         Remove the calendar with the given C{name} from this calendar
@@ -260,7 +275,20 @@
         Change the name of this calendar.
         """
 
+    def displayName():
+        """
+        Get the display name of this calendar.
 
+        @return: a unicode string.
+        """
+
+    def setDisplayName(name):
+        """
+        Set the display name of this calendar.
+
+        @param name: a C{unicode}.
+        """
+
     def ownerCalendarHome():
         """
         Retrieve the calendar home for the owner of this calendar.  Calendars
@@ -275,6 +303,7 @@
         @return: an L{ICalendarHome}.
         """
 
+
     def calendarObjects():
         """
         Retrieve the calendar objects contained in this calendar.
@@ -282,6 +311,7 @@
         @return: an iterable of L{ICalendarObject}s.
         """
 
+
     def calendarObjectWithName(name):
         """
         Retrieve the calendar object with the given C{name} contained
@@ -292,6 +322,7 @@
             object exists.
         """
 
+
     def calendarObjectWithUID(uid):
         """
         Retrieve the calendar object with the given C{uid} contained
@@ -303,6 +334,7 @@
             such calendar object exists.
         """
 
+
     def createCalendarObjectWithName(name, component):
         """
         Create a calendar component with the given C{name} in this
@@ -320,6 +352,7 @@
             a calendar object.
         """
 
+
     def removeCalendarObjectWithName(name):
         """
         Remove the calendar object with the given C{name} from this
@@ -330,6 +363,7 @@
             exists.
         """
 
+
     def removeCalendarObjectWithUID(uid):
         """
         Remove the calendar object with the given C{uid} from this
@@ -340,6 +374,7 @@
             not exist.
         """
 
+
     def syncToken():
         """
         Retrieve the current sync token for this calendar.
@@ -347,6 +382,7 @@
         @return: a string containing a sync token.
         """
 
+
     def calendarObjectsInTimeRange(start, end, timeZone):
         """
         Retrieve all calendar objects in this calendar which have
@@ -359,6 +395,7 @@
         @return: an iterable of L{ICalendarObject}s.
         """
 
+
     def calendarObjectsSinceToken(token):
         """
         Retrieve all calendar objects in this calendar that have
@@ -417,6 +454,8 @@
         """
         # TODO: implement this for the file store.
 
+
+
 class ICalendarObject(IDataStoreObject):
     """
     Calendar object
@@ -431,6 +470,7 @@
         @rtype: L{ICalendar}
         """
 
+
     def setComponent(component):
         """
         Rewrite this calendar object to match the given C{component}.
@@ -443,6 +483,7 @@
             a calendar object.
         """
 
+
     def component():
         """
         Retrieve the calendar component for this calendar object.
@@ -454,6 +495,7 @@
         @return: a C{VCALENDAR} L{VComponent}.
         """
 
+
     def uid():
         """
         Retrieve the UID for this calendar object.
@@ -461,6 +503,7 @@
         @return: a string containing a UID.
         """
 
+
     def componentType():
         """
         Retrieve the iCalendar component type for the main component
@@ -484,6 +527,7 @@
         @return: a URI string.
         """
 
+
     def dropboxID():
         """
         An identifier, unique to the calendar home, that specifies a location
@@ -616,5 +660,3 @@
             that the stream is complete to its C{connectionLost} method.
         @type protocol: L{IProtocol}
         """
-
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/common.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/common.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -234,6 +234,20 @@
 
 
     @inlineCallbacks
+    def test_addressbookHomes(self):
+        """
+        Finding all existing addressbook homes.
+        """
+        addressbookHomes = (yield self.transactionUnderTest().addressbookHomes())
+        self.assertEquals(
+            [home.name() for home in addressbookHomes],
+            [
+                "home1",
+            ]
+        )
+
+
+    @inlineCallbacks
     def test_addressbookHomeWithUID_exists(self):
         """
         Finding an existing addressbook home by UID results in an object that
@@ -664,7 +678,7 @@
             set(c.name() for c in addressbooks),
             set(home1_addressbookNames)
         )
-        
+
         for c in addressbooks:
             self.assertTrue(c.properties() is not None)
 
@@ -910,7 +924,7 @@
             self.assertNotEquals(vcard1_text, vcard1_text_withDifferentNote)
             newComponent = VComponent.fromString(vcard1_text_withDifferentNote)
             yield obj.setComponent(newComponent)
-    
+
             # Putting everything into a separate transaction to account for any
             # caching that may take place.
             yield self.commit()
@@ -979,6 +993,3 @@
         additionalUIDs.add("home_bad")
         expectedUIDs = additionalUIDs.union(requiredUIDs)
         self.assertEquals(foundUIDs, expectedUIDs)
-
-
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/test_file.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/test_file.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/datastore/test/test_file.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -434,7 +434,7 @@
         L{AddressBookObject} has instance attributes, C{_path} and C{_addressbook},
         which refer to its position in the filesystem and the addressbook in which
         it is contained, respectively.
-        """ 
+        """
         self.failUnless(
             isinstance(self.object1._path, FilePath),
             self.object1._path
@@ -445,6 +445,7 @@
         )
 
 
+
 class FileStorageTests(CommonTests, unittest.TestCase):
     """
     File storage tests.
@@ -471,6 +472,21 @@
 
 
     @inlineCallbacks
+    def test_addressbookHomes(self):
+        """
+        Finding all existing addressbook homes.
+        """
+        addressbookHomes = (yield self.transactionUnderTest().addressbookHomes())
+        self.assertEquals(
+            [home.name() for home in addressbookHomes],
+            [
+                "home1",
+                "home_bad",
+            ]
+        )
+
+
+    @inlineCallbacks
     def test_addressbookObjectsWithDotFile(self):
         """
         Adding a dotfile to the addressbook home should not create a new
@@ -489,5 +505,3 @@
         ((yield self.addressbookUnderTest())._path.child("not-a-vcard")
          .createDirectory())
         yield self.test_addressbookObjects()
-
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/iaddressbookstore.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/iaddressbookstore.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/carddav/iaddressbookstore.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -19,7 +19,7 @@
 Address book store interfaces
 """
 
-from txdav.common.icommondatastore import ICommonTransaction,\
+from txdav.common.icommondatastore import ICommonTransaction, \
     IShareableCollection
 from txdav.idav import INotifier
 from txdav.idav import IDataStoreObject
@@ -37,6 +37,14 @@
     Transaction interface that addressbook stores must provide.
     """
 
+    def addressbookHomes():
+        """
+        Retrieve each addressbook home in the store.
+
+        @return: a L{Deferred} which fires with a list of L{ICalendarHome}.
+        """
+
+
     def addressbookHomeWithUID(uid, create=False):
         """
         Retrieve the addressbook home for the principal with the given C{uid}.
@@ -79,6 +87,7 @@
         @return: an iterable of L{IAddressBook}s.
         """
 
+
     def loadAddressbooks():
         """
         Pre-load all addressbooks Depth:1.
@@ -86,6 +95,7 @@
         @return: an iterable of L{IAddressBook}s.
         """
 
+
     def addressbookWithName(name):
         """
         Retrieve the addressbook with the given C{name} contained in this
@@ -96,6 +106,7 @@
             exists.
         """
 
+
     def createAddressBookWithName(name):
         """
         Create an addressbook with the given C{name} in this addressbook
@@ -106,6 +117,7 @@
             given C{name} already exists.
         """
 
+
     def removeAddressBookWithName(name):
         """
         Remove the addressbook with the given C{name} from this addressbook
@@ -117,6 +129,7 @@
         """
 
 
+
 class IAddressBook(INotifier, IShareableCollection, IDataStoreObject):
     """
     AddressBook
@@ -132,6 +145,7 @@
         Change the name of this addressbook.
         """
 
+
     def ownerAddressBookHome():
         """
         Retrieve the addressbook home for the owner of this addressbook.
@@ -141,6 +155,7 @@
         @return: an L{IAddressBookHome}.
         """
 
+
     def addressbookObjects():
         """
         Retrieve the addressbook objects contained in this addressbook.
@@ -148,13 +163,14 @@
         @return: an iterable of L{IAddressBookObject}s.
         """
 
+
     def addressbookObjectWithName(name):
         """
         Retrieve the addressbook object with the given C{name} contained
         in this addressbook.
 
         @param name: a string.
-        
+
         @return: a L{Deferred} that fires with an L{IAddressBookObject} or
             C{None} if no such addressbook object exists.
         """
@@ -169,6 +185,7 @@
             object exists.
         """
 
+
     def createAddressBookObjectWithName(name, component):
         """
         Create an addressbook component with the given C{name} in this
@@ -186,6 +203,7 @@
             an addressbook object.
         """
 
+
     def removeAddressBookObjectWithName(name):
         """
         Remove the addressbook object with the given C{name} from this
@@ -196,6 +214,7 @@
             exists.
         """
 
+
     def removeAddressBookObjectWithUID(uid):
         """
         Remove the addressbook object with the given C{uid} from this
@@ -206,6 +225,7 @@
             not exist.
         """
 
+
     def syncToken():
         """
         Retrieve the current sync token for this addressbook.
@@ -213,6 +233,7 @@
         @return: a string containing a sync token.
         """
 
+
     def addressbookObjectsSinceToken(token):
         """
         Retrieve all addressbook objects in this addressbook that have
@@ -225,6 +246,7 @@
         """
 
 
+
 class IAddressBookObject(IDataStoreObject):
     """
     AddressBook object
@@ -250,6 +272,7 @@
             an addressbook object.
         """
 
+
     def component():
         """
         Retrieve the addressbook component for this addressbook object.
@@ -261,6 +284,7 @@
         @return: a C{VCARD} L{VComponent}.
         """
 
+
     def uid():
         """
         Retrieve the UID for this addressbook object.

Copied: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/common.py (from rev 9988, CalendarServer/trunk/txdav/common/datastore/common.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/common.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/common.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -0,0 +1,55 @@
+# -*- test-case-name: txdav -*-
+##
+# Copyright (c) 2012 Apple Inc. All 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.
+##
+
+"""
+Common functionality that is the same for different data store
+implementations.
+"""
+
+__all__ = [
+]
+
+
+from txdav.xml.element import DisplayName
+from txdav.base.propertystore.base import PropertyName
+
+
+class HomeChildBase(object):
+    """
+    Home child (address book or calendar) common functionality.
+    """
+
+    def displayName(self):
+        name = self.properties().get(PropertyName.fromElement(DisplayName), None)
+        if name is None:
+            return None
+        else:
+            return name.toString()
+
+
+    def setDisplayName(self, name):
+        if name is None:
+            del self.properties()[PropertyName.fromElement(DisplayName)]
+        else:
+            if not isinstance(name, unicode):
+                raise ValueError("Display name must be unicode: %r" % (name,))
+
+            self.properties()[
+                PropertyName.fromElement(DisplayName)
+            ] = DisplayName.fromString(name)
+
+        return None

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/file.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/file.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -24,7 +24,7 @@
 from txdav.xml.rfc2518 import ResourceType, GETContentType, HRef
 from txdav.xml.rfc5842 import ResourceID
 from twext.web2.http_headers import generateContentType, MimeType
-from twext.web2.dav.resource import TwistedGETContentMD5,\
+from twext.web2.dav.resource import TwistedGETContentMD5, \
     TwistedQuotaUsedProperty
 
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue
@@ -38,6 +38,7 @@
 from twistedcaldav.sharing import SharedCollectionsDatabase
 from txdav.caldav.icalendarstore import ICalendarStore, BIND_OWN
 
+from txdav.common.datastore.common import HomeChildBase
 from txdav.common.icommondatastore import HomeChildNameNotAllowedError, \
     HomeChildNameAlreadyExistsError, NoSuchHomeChildError, \
     InternalDataStoreError, ObjectResourceNameNotAllowedError, \
@@ -138,6 +139,7 @@
         self._migrating = state
         self._enableNotifications = not state
 
+
     def setUpgrading(self, state):
         """
         Set the "upgrading" state
@@ -178,6 +180,7 @@
         return self._homesOfType(EADDRESSBOOKTYPE)
 
 
+
 class CommonStoreTransaction(DataStoreTransaction):
     """
     In-memory implementation of
@@ -225,14 +228,45 @@
         CommonStoreTransaction._homeClass[EADDRESSBOOKTYPE] = AddressBookHome
 
 
-    @memoizedKey('uid', '_calendarHomes')
+    def calendarHomes(self):
+        return self.homes(ECALENDARTYPE)
+
+
     def calendarHomeWithUID(self, uid, create=False):
         return self.homeWithUID(ECALENDARTYPE, uid, create=create)
 
-    @memoizedKey("uid", "_addressbookHomes")
+
+    def addressbookHomes(self):
+        return self.homes(EADDRESSBOOKTYPE)
+
+
     def addressbookHomeWithUID(self, uid, create=False):
         return self.homeWithUID(EADDRESSBOOKTYPE, uid, create=create)
 
+
+    def _determineMemo(self, storeType, uid, create=False):
+        """
+        Determine the memo dictionary to use for homeWithUID.
+        """
+        if storeType == ECALENDARTYPE:
+            return self._calendarHomes
+        else:
+            return self._addressbookHomes
+
+
+    def homes(self, storeType):
+        """
+        Load all calendar or addressbook homes.
+        """
+        uids = self._homeClass[storeType].listHomes(self)
+        for uid in uids:
+            self.homeWithUID(storeType, uid, create=False)
+
+        # Return the memoized list directly
+        returnValue([kv[1] for kv in sorted(self._determineMemo(storeType, None).items(), key=lambda x: x[0])])
+
+
+    @memoizedKey("uid", _determineMemo)
     def homeWithUID(self, storeType, uid, create=False):
         if uid.startswith("."):
             return None
@@ -242,6 +276,7 @@
 
         return self._homeClass[storeType].homeWithUID(self, uid, create, storeType == ECALENDARTYPE)
 
+
     @memoizedKey("uid", "_notificationHomes")
     def notificationsWithUID(self, uid, home=None):
 
@@ -254,27 +289,35 @@
     def addAPNSubscription(self, token, key, timestamp, subscriber, userAgent, ipAddr):
         return NotImplementedError
 
+
     def removeAPNSubscription(self, token, key):
         return NotImplementedError
 
+
     def purgeOldAPNSubscriptions(self, purgeSeconds):
         return NotImplementedError
 
+
     def apnSubscriptionsByToken(self, token):
         return NotImplementedError
 
+
     def apnSubscriptionsByKey(self, key):
         return NotImplementedError
 
+
     def apnSubscriptionsBySubscriber(self, guid):
         return NotImplementedError
 
+
     def isNotifiedAlready(self, obj):
         return obj in self._notifiedAlready
 
+
     def notificationAddedForObject(self, obj):
         self._notifiedAlready.add(obj)
 
+
     def isBumpedAlready(self, obj):
         """
         Indicates whether or not bumpAddedForObject has already been
@@ -283,6 +326,7 @@
         """
         return obj in self._bumpedAlready
 
+
     def bumpAddedForObject(self, obj):
         """
         Records the fact that a bumpModified( ) call has already been
@@ -331,6 +375,31 @@
 
 
     @classmethod
+    def listHomes(cls, txn):
+        """
+        Retrieve the owner UIDs of all existing homes.
+
+        @return: an iterable of C{str}s.
+        """
+        results = []
+        top = txn._dataStore._path.child(cls._topPath)
+        if top.exists() and top.isdir() and top.child(UIDPATH).exists():
+            for firstPrefix in top.child(UIDPATH).children():
+                if not isValidName(firstPrefix.basename()):
+                    continue
+                for secondPrefix in firstPrefix.children():
+                    if not isValidName(secondPrefix.basename()):
+                        continue
+                    for actualHome in secondPrefix.children():
+                        uid = actualHome.basename()
+                        if not isValidName(uid):
+                            continue
+                        results.append(uid)
+
+        return results
+
+
+    @classmethod
     def homeWithUID(cls, txn, uid, create=False, withNotifications=False):
 
         assert len(uid) >= 4
@@ -390,6 +459,7 @@
 
         return home
 
+
     def __repr__(self):
         return "<%s: %s>" % (self.__class__.__name__, self._path)
 
@@ -401,6 +471,7 @@
     def transaction(self):
         return self._transaction
 
+
     def retrieveOldShares(self):
         """
         Retrieve the old Index object.
@@ -433,7 +504,7 @@
         ) | set(
             name
             for name in self._path.listdir()
-            if not name.startswith(".") and 
+            if not name.startswith(".") and
                 self._path.child(name).isdir() and
                 name not in self._removedChildren
         ))
@@ -515,6 +586,7 @@
         self.notifyChanged()
         return c
 
+
     @writeOperation
     def removeChildWithName(self, name):
         if name.startswith(".") or name in self._removedChildren:
@@ -532,13 +604,14 @@
             else:
                 self._removedChildren.add(name)
 
+
     @inlineCallbacks
     def syncToken(self):
-        
+
         maxrev = 0
         for child in self.children():
             maxrev = max(int((yield child.syncToken()).split("_")[1]), maxrev)
-            
+
         try:
             urnuuid = str(self.properties()[PropertyName.fromElement(ResourceID)].children[0])
         except KeyError:
@@ -564,6 +637,7 @@
         self._transaction.addOperation(props.flush, "flush home properties")
         return props
 
+
     def objectResourcesWithUID(self, uid, ignore_children=()):
         """
         Return all child object resources with the specified UID, ignoring any in the
@@ -578,6 +652,7 @@
                 results.append(object)
         return results
 
+
     def quotaUsedBytes(self):
 
         try:
@@ -585,31 +660,34 @@
         except KeyError:
             return 0
 
+
     def adjustQuotaUsedBytes(self, delta):
         """
         Adjust quota used. We need to get a lock on the row first so that the adjustment
         is done atomically.
         """
-        
+
         old_used = self.quotaUsedBytes()
         new_used = old_used + delta
         if new_used < 0:
             self.log_error("Fixing quota adjusted below zero to %s by change amount %s" % (new_used, delta,))
             new_used = 0
         self.properties()[PropertyName.fromElement(TwistedQuotaUsedProperty)] = TwistedQuotaUsedProperty(str(new_used))
-            
 
+
     def addNotifier(self, notifier):
         if self._notifiers is None:
             self._notifiers = ()
         self._notifiers += (notifier,)
- 
+
+
     def notifierID(self, label="default"):
         if self._notifiers:
             return self._notifiers[0].getID(label)
         else:
             return None
 
+
     @inlineCallbacks
     def nodeName(self, label="default"):
         if self._notifiers:
@@ -620,6 +698,7 @@
         else:
             returnValue(None)
 
+
     def notifyChanged(self):
         """
         Trigger a notification of a change
@@ -632,7 +711,8 @@
             self._transaction.notificationAddedForObject(self)
 
 
-class CommonHomeChild(FileMetaDataMixin, LoggingMixIn, FancyEqMixin):
+
+class CommonHomeChild(FileMetaDataMixin, LoggingMixIn, FancyEqMixin, HomeChildBase):
     """
     Common ancestor class of AddressBooks and Calendars.
     """
@@ -683,6 +763,7 @@
     def objectWithName(cls, home, name, owned):
         return cls(name, home, owned) if home._path.child(name).isdir() else None
 
+
     @property
     def _path(self):
         return self._home._path.child(self._name)
@@ -691,12 +772,14 @@
     def resourceType(self):
         return NotImplementedError
 
+
     def retrieveOldIndex(self):
         """
         Retrieve the old Index object.
         """
         return self._index._oldIndex
 
+
     def retrieveOldInvites(self):
         """
         Retrieve the old Invites DB object.
@@ -721,6 +804,7 @@
         """
         return BIND_OWN
 
+
     def owned(self):
         return self._owned
 
@@ -742,6 +826,7 @@
 
         self.notifyChanged()
 
+
     @writeOperation
     def remove(self):
 
@@ -855,7 +940,7 @@
         rname = self.retrieveOldIndex().resourceNameForUID(uid)
         if rname and rname not in self._removedObjectResources:
             return self.objectResourceWithName(rname)
-        
+
         return None
 
 
@@ -863,7 +948,7 @@
     def createObjectResourceWithName(self, name, component, metadata=None):
         """
         Create a new resource with component data and optional metadata. We create the
-        python object using the metadata then create the actual store object with setComponent. 
+        python object using the metadata then create the actual store object with setComponent.
         """
         if name.startswith("."):
             raise ObjectResourceNameNotAllowedError(name)
@@ -934,6 +1019,7 @@
         """
         return True
 
+
     # FIXME: property writes should be a write operation
     @cached
     def properties(self):
@@ -957,17 +1043,20 @@
         """
         pass
 
+
     def addNotifier(self, notifier):
         if self._notifiers is None:
             self._notifiers = ()
         self._notifiers += (notifier,)
- 
+
+
     def notifierID(self, label="default"):
         if self._notifiers:
             return self._notifiers[0].getID(label)
         else:
             return None
 
+
     @inlineCallbacks
     def nodeName(self, label="default"):
         if self._notifiers:
@@ -978,6 +1067,7 @@
         else:
             returnValue(None)
 
+
     def notifyChanged(self):
         """
         Trigger a notification of a change
@@ -990,6 +1080,7 @@
             self._transaction.notificationAddedForObject(self)
 
 
+
 class CommonObjectResource(FileMetaDataMixin, LoggingMixIn, FancyEqMixin):
     """
     @ivar _path: The path of the file on disk
@@ -1017,9 +1108,11 @@
     def __repr__(self):
         return "<%s: %s>" % (self.__class__.__name__, self._path.path)
 
+
     def transaction(self):
         return self._transaction
 
+
     @writeOperation
     def setComponent(self, component, inserting=False):
         raise NotImplementedError
@@ -1036,6 +1129,7 @@
     def uid(self):
         raise NotImplementedError
 
+
     @cached
     def properties(self):
         home = self._parentCollection._home
@@ -1049,6 +1143,7 @@
         self._transaction.addOperation(props.flush, "object properties flush")
         return props
 
+
     def initPropertyStore(self, props):
         """
         A hook for subclasses to override in order to set up their property
@@ -1058,6 +1153,8 @@
         """
         pass
 
+
+
 class CommonStubResource(object):
     """
     Just enough resource to keep the collection sql DB classes going.
@@ -1066,6 +1163,7 @@
         self.resource = resource
         self.fp = self.resource._path
 
+
     def bumpSyncToken(self, reset=False):
         # FIXME: needs direct tests
         return self.resource._updateSyncToken(reset)
@@ -1076,6 +1174,7 @@
         self.bumpSyncToken(True)
 
 
+
 class NotificationCollection(CommonHomeChild):
     """
     File-based implementation of L{INotificationCollection}.
@@ -1100,6 +1199,7 @@
         self._invites = None
         self._objectResourceClass = NotificationObject
 
+
     @classmethod
     def notificationsFromHome(cls, txn, home):
 
@@ -1143,6 +1243,7 @@
         props[PropertyName(*ResourceType.qname())] = c.resourceType()
         return c
 
+
     def resourceType(self):
         return ResourceType.notification #@UndefinedVariable
 
@@ -1155,6 +1256,7 @@
         name = uid + ".xml"
         return self.notificationObjectWithName(name)
 
+
     def writeNotificationObject(self, uid, xmltype, xmldata):
         name = uid + ".xml"
         if name.startswith("."):
@@ -1166,9 +1268,10 @@
 
         # Update database
         self.retrieveOldIndex().addOrUpdateRecord(NotificationRecord(uid, name, xmltype.name))
-        
+
         self.notifyChanged()
 
+
     @writeOperation
     def removeNotificationObjectWithName(self, name):
         if name.startswith("."):
@@ -1185,17 +1288,19 @@
                 return lambda: None
             self._transaction.addOperation(do, "remove object resource object %r" %
                                            (name,))
-        
+
             self.notifyChanged()
         else:
             raise NoSuchObjectResourceError(name)
 
+
     @writeOperation
     def removeNotificationObjectWithUID(self, uid):
         name = uid + ".xml"
         self.removeNotificationObjectWithName(name)
 
 
+
 class NotificationObject(CommonObjectResource):
     """
     """
@@ -1205,6 +1310,7 @@
         super(NotificationObject, self).__init__(name, notifications)
         self._uid = name[:-4]
 
+
     def notificationCollection(self):
         return self._parentCollection
 
@@ -1215,6 +1321,7 @@
             return int(reactor.seconds())
         return super(NotificationObject, self).created()
 
+
     def modified(self):
         if not self._path.exists():
             from twisted.internet import reactor
@@ -1260,11 +1367,10 @@
         self.properties().update(self.properties())
 
         props = self.properties()
-        props[PropertyName(*GETContentType.qname())] = GETContentType.fromString(generateContentType(MimeType("text", "xml", params={"charset":"utf-8"})))
+        props[PropertyName(*GETContentType.qname())] = GETContentType.fromString(generateContentType(MimeType("text", "xml", params={"charset": "utf-8"})))
         props[PropertyName.fromElement(NotificationType)] = NotificationType(xmltype)
         props[PropertyName.fromElement(TwistedGETContentMD5)] = TwistedGETContentMD5.fromString(md5)
 
-
         # FIXME: the property store's flush() method may already have been
         # added to the transaction, but we need to add it again to make sure it
         # happens _after_ the new file has been written.  we may end up doing
@@ -1272,7 +1378,6 @@
         # manipulation methods won't work.
         self._transaction.addOperation(self.properties().flush, "post-update property flush")
 
-
     _xmldata = None
 
     def xmldata(self):
@@ -1297,10 +1402,12 @@
     def uid(self):
         return self._uid
 
+
     def xmlType(self):
         # NB This is the NotificationType property element
         return self.properties()[PropertyName.fromElement(NotificationType)]
 
+
     def initPropertyStore(self, props):
         # Setup peruser special properties
         props.setSpecialProperties(
@@ -1312,6 +1419,7 @@
         )
 
 
+
 class NotificationIndex(object):
     #
     # OK, here's where we get ugly.
@@ -1321,4 +1429,3 @@
         self.notificationCollection = notificationCollection
         stubResource = CommonStubResource(notificationCollection)
         self._oldIndex = OldNotificationIndex(stubResource)
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -31,7 +31,8 @@
 
 from twext.python.log import Logger, LoggingMixIn
 from twisted.python.log import msg as log_msg, err as log_err
-from txdav.xml.rfc2518 import ResourceType
+
+from txdav.xml.element import ResourceType
 from txdav.xml.parser import WebDAVDocument
 from twext.web2.http_headers import MimeType
 
@@ -53,6 +54,7 @@
 
 from txdav.carddav.iaddressbookstore import IAddressBookTransaction
 
+from txdav.common.datastore.common import HomeChildBase
 from txdav.common.datastore.sql_tables import schema
 from txdav.common.datastore.sql_tables import _BIND_MODE_OWN, \
     _BIND_STATUS_ACCEPTED, _BIND_STATUS_DECLINED, \
@@ -463,10 +465,18 @@
         raise RuntimeError("Database key %s cannot be determined." % (key,))
 
 
+    def calendarHomes(self):
+        return self.homes(ECALENDARTYPE)
+
+
     def calendarHomeWithUID(self, uid, create=False):
         return self.homeWithUID(ECALENDARTYPE, uid, create=create)
 
 
+    def addressbookHomes(self):
+        return self.homes(EADDRESSBOOKTYPE)
+
+
     def addressbookHomeWithUID(self, uid, create=False):
         return self.homeWithUID(EADDRESSBOOKTYPE, uid, create=create)
 
@@ -481,6 +491,21 @@
             return self._addressbookHomes
 
 
+    @inlineCallbacks
+    def homes(self, storeType):
+        """
+        Load all calendar or addressbook homes.
+        """
+
+        # Get all UIDs and load them - this will memoize all existing ones
+        uids = (yield self._homeClass[storeType].listHomes(self))
+        for uid in uids:
+            yield self.homeWithUID(storeType, uid, create=False)
+
+        # Return the memoized list directly
+        returnValue([kv[1] for kv in sorted(self._determineMemo(storeType, None).items(), key=lambda x: x[0])])
+
+
     @memoizedKey("uid", _determineMemo)
     def homeWithUID(self, storeType, uid, create=False):
         if storeType not in (ECALENDARTYPE, EADDRESSBOOKTYPE):
@@ -726,6 +751,13 @@
         block = self._sqlTxn.commandBlock()
         sp = self._savepoint()
         failuresToMaybeLog = []
+        def end():
+            block.end()
+            for f in failuresToMaybeLog:
+                # TODO: direct tests, to make sure error logging
+                # happens correctly in all cases.
+                log.err(f)
+            raise AllRetriesFailed()
         triesLeft = retries
         try:
             while True:
@@ -733,8 +765,9 @@
                 try:
                     result = yield thunk(block)
                 except:
+                    f = Failure()
                     if not failureOK:
-                        failuresToMaybeLog.append(Failure())
+                        failuresToMaybeLog.append(f)
                     yield sp.rollback(block)
                     if triesLeft:
                         triesLeft -= 1
@@ -749,12 +782,7 @@
                         block = newBlock
                         sp = self._savepoint()
                     else:
-                        block.end()
-                        for f in failuresToMaybeLog:
-                            # TODO: direct tests, to make sure error logging
-                            # happens correctly in all cases.
-                            log.err(f)
-                        raise AllRetriesFailed()
+                        end()
                 else:
                     yield sp.release(block)
                     block.end()
@@ -765,9 +793,10 @@
             # and only that case - acquire() or release() or commandBlock() may
             # raise an AlreadyFinishedError (either synchronously, or in the
             # case of the first two, possibly asynchronously as well).  We can
-            # safely ignore this, because it can't have any real effect; our
-            # caller shouldn't be paying attention anyway.
-            block.end()
+            # safely ignore this error, because it can't have any effect on what
+            # gets written; our caller will just get told that it failed in a
+            # way they have to be prepared for anyway.
+            end()
 
 
     @inlineCallbacks
@@ -1101,6 +1130,22 @@
 
     @classmethod
     @inlineCallbacks
+    def listHomes(cls, txn):
+        """
+        Retrieve the owner UIDs of all existing homes.
+
+        @return: an iterable of C{str}s.
+        """
+        rows = yield Select(
+            [cls._homeSchema.OWNER_UID],
+            From=cls._homeSchema,
+        ).on(txn)
+        rids = [row[0] for row in rows]
+        returnValue(rids)
+
+
+    @classmethod
+    @inlineCallbacks
     def homeWithUID(cls, txn, uid, create=False):
         if txn._notifierFactory:
             notifiers = (txn._notifierFactory.newNotifier(
@@ -1993,7 +2038,7 @@
 
 
 
-class CommonHomeChild(LoggingMixIn, FancyEqMixin, _SharedSyncLogic):
+class CommonHomeChild(LoggingMixIn, FancyEqMixin, _SharedSyncLogic, HomeChildBase):
     """
     Common ancestor class of AddressBooks and Calendars.
     """

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_legacy.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_legacy.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_legacy.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -39,7 +39,7 @@
     ReservationError, NoSuchObjectResourceError
 
 from txdav.common.datastore.sql_tables import schema
-from twext.enterprise.dal.syntax import Parameter, Select 
+from twext.enterprise.dal.syntax import Parameter, Select
 from twext.python.clsprop import classproperty
 from twext.python.log import Logger, LoggingMixIn
 
@@ -226,7 +226,7 @@
 
         # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
         if self.calendarid:
-            
+
             test = expression.isExpression("CALENDAR_OBJECT.CALENDAR_RESOURCE_ID", str(self.calendarid), True)
 
             # Since timerange expression already have the calendar resource-id test in them, do not
@@ -236,10 +236,10 @@
             # Top-level timerange expression already has calendar resource-id restriction in it
             if isinstance(self.expression, expression.timerangeExpression):
                 pass
-            
+
             # Top-level OR - check each component
             elif isinstance(self.expression, expression.orExpression):
-                
+
                 def _hasTopLevelTimerange(testexpr):
                     if isinstance(testexpr, expression.timerangeExpression):
                         return True
@@ -247,7 +247,7 @@
                         return any([isinstance(expr, expression.timerangeExpression) for expr in testexpr.expressions])
                     else:
                         return False
-                        
+
                 hasTimerange = any([_hasTopLevelTimerange(expr) for expr in self.expression.expressions])
 
                 if hasTimerange:
@@ -255,16 +255,16 @@
                     pass
                 else:
                     # AND the whole thing with calendarid
-                    self.expression = test.andWith(self.expression)    
+                    self.expression = test.andWith(self.expression)
 
-            
+
             # Top-level AND - only add additional expression if timerange not present
             elif isinstance(self.expression, expression.andExpression):
                 hasTimerange = any([isinstance(expr, expression.timerangeExpression) for expr in self.expression.expressions])
                 if not hasTimerange:
                     # AND the whole thing
-                    self.expression = test.andWith(self.expression)    
-            
+                    self.expression = test.andWith(self.expression)
+
             # Just AND the entire thing
             else:
                 self.expression = test.andWith(self.expression)
@@ -472,10 +472,10 @@
         with a longer expansion.
         """
         obj = yield self.calendar.calendarObjectWithName(name)
-        
+
         # Use a new transaction to do this update quickly without locking the row for too long. However, the original
         # transaction may have the row locked, so use wait=False and if that fails, fall back to using the original txn. 
-        
+
         newTxn = obj.transaction().store().newTransaction()
         try:
             yield obj.lock(wait=False, txn=newTxn)
@@ -494,7 +494,7 @@
             else:
                 # We repeat this check because the resource may have been re-expanded by someone else
                 rmin, rmax = (yield obj.recurrenceMinMax(txn=newTxn))
-                
+
                 # If the resource is not fully expanded, see if within the required range or not.
                 # Note that expand_start could be None if no lower limit is applied, but expand_end will
                 # never be None
@@ -585,7 +585,7 @@
                     if minDate < truncateLowerLimit:
                         raise TimeRangeLowerLimit(truncateLowerLimit)
 
-                        
+
                 if maxDate is not None or minDate is not None:
                     yield self.testAndUpdateIndex(minDate, maxDate)
 
@@ -739,10 +739,10 @@
 
         # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
         if self.calendarid:
-            
+
             # AND the whole thing
             test = expression.isExpression("ADDRESSBOOK_OBJECT.ADDRESSBOOK_RESOURCE_ID", str(self.calendarid), True)
-            self.expression = test.andWith(self.expression)    
+            self.expression = test.andWith(self.expression)
 
         # Generate ' where ...' partial statement
         self.sout.write(self.WHERE)

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/test/test_sql.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/test/test_sql.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -24,6 +24,7 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.task import Clock
 from twisted.trial.unittest import TestCase
+from twisted.internet.defer import Deferred
 
 from txdav.common.datastore.sql import log, CommonStoreTransactionMonitor,\
     CommonHome, CommonHomeChild, ECALENDARTYPE
@@ -227,7 +228,8 @@
     @inlineCallbacks
     def test_subtransactionFailSomeRetries(self):
         """
-        txn.subtransaction runs loop three times when all fail and two retries requested.
+        txn.subtransaction runs loop three times when all fail and two retries
+        requested.
         """
         
         txn = self.transactionUnderTest()
@@ -251,7 +253,35 @@
             self.fail("AllRetriesFailed not raised")
         self.assertEqual(ctr[0], 3)
 
+
     @inlineCallbacks
+    def test_subtransactionAbortOuterTransaction(self):
+        """
+        If an outer transaction that is holding a subtransaction open is
+        aborted, then the L{Deferred} returned by L{subtransaction} raises
+        L{AllRetriesFailed}.
+        """
+        txn = self.transactionUnderTest()
+        cs = schema.CALENDARSERVER
+        waitAMoment = Deferred()
+        @inlineCallbacks
+        def later(subtxn):
+            yield waitAMoment
+            value = yield Select([cs.VALUE], From=cs).on(subtxn)
+            returnValue(value)
+        started = txn.subtransaction(later)
+        txn.abort()
+        waitAMoment.callback(True)
+        try:
+            result = yield started
+        except AllRetriesFailed:
+            pass
+        else:
+            self.fail("AllRetriesFailed not raised, %r returned instead" %
+                      (result,))
+
+
+    @inlineCallbacks
     def test_changeRevision(self):
         """
         CommonHomeChild._changeRevision actions.

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/xml/__init__.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/xml/__init__.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/xml/__init__.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -52,4 +52,6 @@
 import txdav.xml.rfc6578
 import txdav.xml.extensions
 
+# FIXME: add symbols to __all__
+
 txdav # Shhh pyflakes

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/xml/base.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/xml/base.py	2012-10-26 20:14:36 UTC (rev 9990)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/xml/base.py	2012-10-26 21:24:44 UTC (rev 9991)
@@ -589,15 +589,24 @@
     def fromString(clazz, string):
         if string is None:
             return clazz()
-        elif isinstance(string, (str, unicode)):
+        elif isinstance(string, (unicode, str)):
             return clazz(PCDATAElement(string))
         else:
             return clazz(PCDATAElement(str(string)))
 
     allowed_children = { PCDATAElement: (0, None) }
 
+    def toString(self):
+        """
+        @return: a unicode string containing the text in this element.
+        """
+        return self.__str__().decode("utf-8")
+
     def __str__(self):
-        return "".join([c.data for c in self.children])
+        """
+        @return: a byte string containing the text in this element.
+        """
+        return b"".join([c.data for c in self.children])
 
     def __repr__(self):
         content = str(self)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121026/315da48c/attachment-0001.html>


More information about the calendarserver-changes mailing list