[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 loo