[CalendarServer-changes] [9984] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Oct 25 08:20:28 PDT 2012


Revision: 9984
          http://trac.calendarserver.org//changeset/9984
Author:   cdaboo at apple.com
Date:     2012-10-25 08:20:28 -0700 (Thu, 25 Oct 2012)
Log Message:
-----------
Merge DKIM iSchedule work to trunk.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/calendarserver/tap/test/test_util.py
    CalendarServer/trunk/calendarserver/tap/util.py
    CalendarServer/trunk/conf/caldavd-apple.plist
    CalendarServer/trunk/conf/caldavd-partitioning-primary.plist
    CalendarServer/trunk/conf/caldavd-partitioning-secondary.plist
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/conf/caldavd.plist
    CalendarServer/trunk/twisted/plugins/caldav.py
    CalendarServer/trunk/twistedcaldav/__init__.py
    CalendarServer/trunk/twistedcaldav/directory/directory.py
    CalendarServer/trunk/twistedcaldav/freebusyurl.py
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py
    CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py
    CalendarServer/trunk/twistedcaldav/scheduling/delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
    CalendarServer/trunk/twistedcaldav/scheduling/itip.py
    CalendarServer/trunk/twistedcaldav/scheduling/processing.py
    CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
    CalendarServer/trunk/twistedcaldav/scheduling/utils.py
    CalendarServer/trunk/twistedcaldav/simpleresource.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/twistedcaldav/test/test_upgrade.py
    CalendarServer/trunk/twistedcaldav/timezonexml.py
    CalendarServer/trunk/twistedcaldav/upgrade.py
    CalendarServer/trunk/txdav/base/datastore/subpostgres.py
    CalendarServer/trunk/txdav/caldav/datastore/util.py

Added Paths:
-----------
    CalendarServer/trunk/bin/calendarserver_dkimtool
    CalendarServer/trunk/calendarserver/tools/dkimtool.py
    CalendarServer/trunk/conf/localservers-test.xml
    CalendarServer/trunk/conf/localservers.xml
    CalendarServer/trunk/conf/remoteservers-test.xml
    CalendarServer/trunk/conf/remoteservers.xml
    CalendarServer/trunk/doc/Admin/iSchedule.txt
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/scheduler.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/mailgateway.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/scheduler.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/good_reply
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.mo
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.mo
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_mailgateway.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/dkim.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/localservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/scheduler.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.two.zones
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_dkim.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_localservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_utils.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py

Removed Paths:
-------------
    CalendarServer/trunk/conf/servers-test.xml
    CalendarServer/trunk/conf/servers.xml
    CalendarServer/trunk/conf/servertoserver-test.xml
    CalendarServer/trunk/conf/servertoserver.xml
    CalendarServer/trunk/twistedcaldav/mail.py
    CalendarServer/trunk/twistedcaldav/schedule.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/scheduler.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/mailgateway.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/scheduler.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/good_reply
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.mo
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.mo
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_mailgateway.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/dkim.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/localservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/scheduler.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.two.zones
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_dkim.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_localservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_utils.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_caldav.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_imip.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_ischeduleservers.py
    CalendarServer/trunk/twistedcaldav/servers.py
    CalendarServer/trunk/twistedcaldav/test/data/mail/
    CalendarServer/trunk/twistedcaldav/test/test_mail.py
    CalendarServer/trunk/twistedcaldav/test/test_schedule.py
    CalendarServer/trunk/twistedcaldav/test/test_servers.py

Property Changed:
----------------
    CalendarServer/trunk/


Property changes on: CalendarServer/trunk
___________________________________________________________________
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/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/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

Copied: CalendarServer/trunk/bin/calendarserver_dkimtool (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/bin/calendarserver_dkimtool)
===================================================================
--- CalendarServer/trunk/bin/calendarserver_dkimtool	                        (rev 0)
+++ CalendarServer/trunk/bin/calendarserver_dkimtool	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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()

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -70,7 +70,7 @@
 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
@@ -208,6 +208,8 @@
         self.transport.write("%s\r\n" % (stats,))
         self.transport.loseConnection()
 
+
+
 class CalDAVStatisticsServer (Factory):
 
     protocol = CalDAVStatisticsProtocol
@@ -216,6 +218,7 @@
         self.logger = logObserver
 
 
+
 class ErrorLoggingMultiService(MultiService, object):
     """ Registers a rotating file logger for error logging, if
         config.ErrorLogEnabled is True. """
@@ -236,6 +239,7 @@
             app.setComponent(ILogObserver, errorLogObserver)
 
 
+
 class CalDAVService (ErrorLoggingMultiService):
 
     # The ConnectionService is a MultiService which bundles all the connection
@@ -247,10 +251,12 @@
         self.logObserver = logObserver # accesslog observer
         MultiService.__init__(self)
 
+
     def privilegedStartService(self):
         MultiService.privilegedStartService(self)
         self.logObserver.start()
 
+
     @inlineCallbacks
     def stopService(self):
         """
@@ -265,6 +271,7 @@
         self.logObserver.stop()
 
 
+
 class CalDAVOptions (Options, LoggingMixIn):
     optParameters = [[
         "config", "f", DEFAULT_CONFIG_FILE, "Path to configuration file."
@@ -329,6 +336,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 +357,7 @@
 
     opt_o = opt_option
 
+
     def postOptions(self):
         try:
             self.loadConfiguration()
@@ -357,6 +366,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 +377,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 +409,6 @@
 
         self.parent["pidfile"] = config.PIDFile
 
-
         #
         # Verify that server root actually exists
         #
@@ -527,7 +538,6 @@
             )
             self.monitor.addProcessObject(process, PARENT_ENVIRONMENT)
 
-
         if (
             config.Notifications.Enabled and
             config.Notifications.InternalNotificationHost == "localhost"
@@ -595,6 +605,7 @@
                                env=PARENT_ENVIRONMENT)
 
 
+
 class ReExecService(MultiService, LoggingMixIn):
     """
     A MultiService which catches SIGHUP and re-exec's the process.
@@ -612,6 +623,7 @@
         self.reactor = reactor
         MultiService.__init__(self)
 
+
     def reExec(self):
         """
         Removes pidfile, registers an exec to happen after shutdown, then
@@ -627,18 +639,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 +729,6 @@
                 else:
                     return "%s: %s" % (frame.f_code.co_name, frame.f_lineno)
 
-
             return service
 
 
@@ -949,7 +964,6 @@
                         inherit=False
                     ).setServiceParent(connectionService)
 
-
         # Change log level back to what it was before
         setLogLevelForNamespace(None, oldLogLevel)
         return service
@@ -1176,7 +1190,6 @@
         else:
             uid = os.getuid()
 
-
         controlSocket = ControlSocket()
         controlSocket.addFactory(_LOG_ROUTE, logger)
         if config.ControlSocket:
@@ -1233,7 +1246,6 @@
             config.MultiProcess.ProcessCount = processCount
             self.log_info("Configuring %d processes." % (processCount,))
 
-
         # Open the socket(s) to be inherited by the slaves
         inheritFDs = []
         inheritSSLFDs = []
@@ -1372,7 +1384,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):
@@ -1640,6 +1652,7 @@
         for name in self.processes:
             self.startProcess(name)
 
+
     def stopService(self):
         """
         Return a deferred that fires when all child processes have ended.
@@ -1763,7 +1776,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:
@@ -1778,15 +1791,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())
 
@@ -1797,7 +1810,6 @@
             childFDs=childFDs
         )
 
-
     _pendingStarts = 0
 
     def startProcess(self, name):
@@ -1828,12 +1840,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 + ')'
@@ -1859,6 +1871,7 @@
         """
         Ignore this IProtocol method, since I don't need a transport.
         """
+        pass
 
 
     def dataReceived(self, data):
@@ -1894,8 +1907,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
@@ -1906,16 +1919,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.

Modified: CalendarServer/trunk/calendarserver/tap/test/test_util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/test/test_util.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/calendarserver/tap/test/test_util.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -51,6 +51,8 @@
                 computeProcessCount(min, perCPU, perGB, cpuCount=cpu, memSize=mem)
             )
 
+
+
 class UtilTestCase(TestCase):
 
     def test_directoryFromConfig(self):
@@ -72,20 +74,26 @@
     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):
@@ -104,7 +112,7 @@
 
         processes = []
         protocols = {}
-        for pid, (name, resident, virtual) in data.iteritems():
+        for pid, (name, _ignore_resident, _ignore_virtual) in data.iteritems():
             protocols[name] = StubProtocol(StubProcess(pid))
             processes.append(name)
         processMonitor = StubProcessMonitor(processes, protocols)

Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/util.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/calendarserver/tap/util.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -61,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
@@ -289,7 +290,7 @@
     directories = []
 
     directoryClass = namedClass(config.DirectoryService.type)
-    principalResourceClass       = DirectoryPrincipalProvisioningResource
+    principalResourceClass = DirectoryPrincipalProvisioningResource
 
     log.info("Configuring directory service of type: %s"
         % (config.DirectoryService.type,))
@@ -351,6 +352,7 @@
     return directory
 
 
+
 def getRootResource(config, newStore, resources=None):
     """
     Set up directory service and resource hierarchy based on config.
@@ -376,16 +378,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)
 
@@ -462,12 +464,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
 
@@ -502,10 +502,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,))
 
@@ -514,7 +514,6 @@
         principalCollections=(principalCollection,),
     )
 
-
     root.putChild("principals", principalCollection)
     if config.EnableCalDAV:
         root.putChild("calendars", calendarCollection)
@@ -537,6 +536,7 @@
             (config.EnableCalDAV, "caldav", "/",),
             (config.EnableCardDAV, "carddav", "/",),
             (config.TimezoneService.Enabled, "timezone", "/stdtimezones",),
+            (config.Scheduling.iSchedule.Enabled, "ischedule", "/ischedule"),
         ):
             if enabled:
                 if config.EnableSSL:
@@ -581,14 +581,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,))
@@ -599,6 +601,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
     #
@@ -640,10 +654,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:
 
@@ -683,6 +696,7 @@
     return logWrapper
 
 
+
 def getDBPool(config):
     """
     Inspect configuration to determine what database connection pool
@@ -759,8 +773,6 @@
 
 
 
-
-
 class FakeRequest(object):
 
     def __init__(self, rootResource, method, path, uri='/'):
@@ -772,6 +784,7 @@
         self._urlsByResource = {}
         self.headers = Headers()
 
+
     @inlineCallbacks
     def _getChild(self, resource, segments):
         if not segments:
@@ -780,6 +793,7 @@
         child, remaining = (yield resource.locateChild(self, segments))
         returnValue((yield self._getChild(child, remaining)))
 
+
     @inlineCallbacks
     def locateResource(self, url):
         url = url.strip("/")
@@ -789,6 +803,7 @@
             self._rememberResource(resource, url)
         returnValue(resource)
 
+
     @inlineCallbacks
     def locateChildResource(self, parent, childName):
         if parent is None or childName is None:
@@ -803,17 +818,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:
@@ -822,10 +840,12 @@
             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,
@@ -841,6 +861,7 @@
     return memoryInfo.rss if residentOnly else memoryInfo.vms
 
 
+
 class MemoryLimitService(Service, object):
     """
     A service which when paired with a DelayedStartupProcessMonitor will periodically
@@ -871,6 +892,7 @@
         # Unit tests can swap out _memoryForPID
         self._memoryForPID = memoryForPID
 
+
     def startService(self):
         """
         Start scheduling the memory checks
@@ -878,6 +900,7 @@
         super(MemoryLimitService, self).startService()
         self._delayedCall = self._reactor.callLater(self._seconds, self.checkMemory)
 
+
     def stopService(self):
         """
         Stop checking memory
@@ -887,6 +910,7 @@
             self._delayedCall.cancel()
             self._delayedCall = None
 
+
     def checkMemory(self):
         """
         Stop any processes monitored by our paired processMonitor whose resident

Copied: CalendarServer/trunk/calendarserver/tools/dkimtool.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/calendarserver/tools/dkimtool.py)
===================================================================
--- CalendarServer/trunk/calendarserver/tools/dkimtool.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/tools/dkimtool.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-apple.plist	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/conf/caldavd-apple.plist	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/caldavd-partitioning-primary.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-partitioning-primary.plist	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/conf/caldavd-partitioning-primary.plist	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/caldavd-partitioning-secondary.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-partitioning-secondary.plist	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/conf/caldavd-partitioning-secondary.plist	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/conf/caldavd.plist	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/localservers-test.xml (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/conf/localservers-test.xml)
===================================================================
--- CalendarServer/trunk/conf/localservers-test.xml	                        (rev 0)
+++ CalendarServer/trunk/conf/localservers-test.xml	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/localservers.xml (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/conf/localservers.xml)
===================================================================
--- CalendarServer/trunk/conf/localservers.xml	                        (rev 0)
+++ CalendarServer/trunk/conf/localservers.xml	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/remoteservers-test.xml (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/conf/remoteservers-test.xml)
===================================================================
--- CalendarServer/trunk/conf/remoteservers-test.xml	                        (rev 0)
+++ CalendarServer/trunk/conf/remoteservers-test.xml	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/remoteservers.xml (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/conf/remoteservers.xml)
===================================================================
--- CalendarServer/trunk/conf/remoteservers.xml	                        (rev 0)
+++ CalendarServer/trunk/conf/remoteservers.xml	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/servers-test.xml
===================================================================
--- CalendarServer/trunk/conf/servers-test.xml	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/conf/servers-test.xml	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/servers.xml
===================================================================
--- CalendarServer/trunk/conf/servers.xml	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/conf/servers.xml	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/servertoserver-test.xml
===================================================================
--- CalendarServer/trunk/conf/servertoserver-test.xml	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/conf/servertoserver-test.xml	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/conf/servertoserver.xml
===================================================================
--- CalendarServer/trunk/conf/servertoserver.xml	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/conf/servertoserver.xml	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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>

Copied: CalendarServer/trunk/doc/Admin/iSchedule.txt (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/doc/Admin/iSchedule.txt)
===================================================================
--- CalendarServer/trunk/doc/Admin/iSchedule.txt	                        (rev 0)
+++ CalendarServer/trunk/doc/Admin/iSchedule.txt	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twisted/plugins/caldav.py
===================================================================
--- CalendarServer/trunk/twisted/plugins/caldav.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twisted/plugins/caldav.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/__init__.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/freebusyurl.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/freebusyurl.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/freebusyurl.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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.

Deleted: CalendarServer/trunk/twistedcaldav/mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/mail.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/mail.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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_warn("IMAP factory connection failed")
-        self.retry(connector)

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -534,6 +534,7 @@
         res = (yield self._hasGlobalProperty(property, request))
         returnValue(res)
 
+
     def _hasSharedProperty(self, qname, request):
 
         # Always have default alarms on shared calendars
@@ -548,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.
@@ -602,6 +604,7 @@
         p = self.deadProperties().get(qname)
         return p
 
+
     @inlineCallbacks
     def _readGlobalProperty(self, qname, property, request):
 
@@ -1017,7 +1020,8 @@
         """
         See L{ICalDAVResource.isSpecialCollection}.
         """
-        if not self.isCollection(): return False
+        if not self.isCollection():
+            return False
 
         try:
             resourcetype = self.resourceType()
@@ -2135,6 +2139,7 @@
 
         return props
 
+
     def url(self):
         return joinURL(self.parent.url(), self.name, "/")
 
@@ -2584,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:
@@ -2900,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/trunk/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/schedule.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/schedule.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/addressmapping.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/__init__.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/__init__.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/delivery.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/delivery.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/delivery.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/delivery.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/resource.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/resource.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/resource.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/resource.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/resource.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/resource.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/scheduler.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/scheduler.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/scheduler.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/scheduler.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/scheduler.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/scheduler.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/test/__init__.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/test/__init__.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/test/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/test/test_delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/test/test_delivery.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/test/test_delivery.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/test/test_delivery.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_delivery.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/test/test_resource.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_resource.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav/test/test_resource.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/caldav/test/test_resource.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_resource.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav/test/test_resource.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/cuaddress.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/delivery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/delivery.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/imip/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/__init__.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/__init__.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/delivery.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/delivery.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/delivery.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/delivery.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/mailgateway.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/mailgateway.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/mailgateway.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/mailgateway.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/mailgateway.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/mailgateway.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/mailgateway.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/resource.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/resource.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/resource.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/resource.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/resource.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/resource.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/scheduler.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/scheduler.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/scheduler.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/scheduler.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/scheduler.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/scheduler.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/__init__.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/__init__.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_ics	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_no_original	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/dsn_failure_with_ics	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/good_reply
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/good_reply	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/good_reply	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/good_reply (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/good_reply)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/good_reply	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/good_reply	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.mo
===================================================================
(Binary files differ)

Copied: CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.mo (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.mo)
===================================================================
(Binary files differ)

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/en/LC_MESSAGES/calendarserver.po	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.mo
===================================================================
(Binary files differ)

Copied: CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.mo (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.mo)
===================================================================
(Binary files differ)

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/locales/pig/LC_MESSAGES/calendarserver.po	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attachment	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_attendee	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/data/reply_missing_organizer	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/test_delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/test_delivery.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/test_delivery.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/test_delivery.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_delivery.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/test_mailgateway.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/test_mailgateway.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_mailgateway.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip/test/test_mailgateway.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/imip/test/test_mailgateway.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_mailgateway.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_mailgateway.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/imip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -31,7 +31,7 @@
     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
 
@@ -1149,7 +1149,7 @@
         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)

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

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

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

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/remoteservers.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,227 +0,0 @@
-##
-# Copyright (c) 2006-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.python.filepath import CachingFilePath as FilePath
-
-from twext.python.log import Logger
-
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav import xmlutil
-
-"""
-XML based iSchedule configuration file handling. This is for handling of remote servers. The localservers.py module
-handles servers that are local (partitioned or podded).
-"""
-
-__all__ = [
-    "IScheduleServers",
-]
-
-log = Logger()
-
-
-
-class IScheduleServers(object):
-
-    _fileInfo = None
-    _xmlFile = None
-    _servers = None
-    _domainMap = None
-
-    def __init__(self):
-
-        self._loadConfig()
-
-
-    def _loadConfig(self):
-        if config.Scheduling.iSchedule.RemoteServers:
-            if IScheduleServers._servers is None:
-                IScheduleServers._xmlFile = FilePath(
-                    fullServerPath(
-                        config.ConfigRoot,
-                        config.Scheduling.iSchedule.RemoteServers,
-                    )
-                )
-            if IScheduleServers._xmlFile.exists():
-                IScheduleServers._xmlFile.restat()
-                fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
-                if fileInfo != IScheduleServers._fileInfo:
-                    parser = IScheduleServersParser(IScheduleServers._xmlFile)
-                    IScheduleServers._servers = parser.servers
-                    self._mapDomains()
-                    IScheduleServers._fileInfo = fileInfo
-            else:
-                IScheduleServers._servers = ()
-                IScheduleServers._domainMap = {}
-
-        else:
-            IScheduleServers._servers = ()
-            IScheduleServers._domainMap = {}
-
-
-    def _mapDomains(self):
-        IScheduleServers._domainMap = {}
-        for server in IScheduleServers._servers:
-            for domain in server.domains:
-                IScheduleServers._domainMap[domain] = server
-
-
-    def mapDomain(self, domain):
-        """
-        Map a calendar user address domain to a suitable server that can
-        handle server-to-server requests for that user.
-        """
-        return IScheduleServers._domainMap.get(domain)
-
-ELEMENT_SERVERS = "servers"
-ELEMENT_SERVER = "server"
-ELEMENT_URI = "uri"
-ELEMENT_AUTHENTICATION = "authentication"
-ATTRIBUTE_TYPE = "type"
-ATTRIBUTE_BASICAUTH = "basic"
-ELEMENT_USER = "user"
-ELEMENT_PASSWORD = "password"
-ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
-ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
-ELEMENT_DOMAINS = "domains"
-ELEMENT_DOMAIN = "domain"
-ELEMENT_CLIENT_HOSTS = "hosts"
-ELEMENT_HOST = "host"
-
-
-
-class IScheduleServersParser(object):
-    """
-    Server-to-server configuration file parser.
-    """
-    def __repr__(self):
-        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
-
-
-    def __init__(self, xmlFile):
-
-        self.servers = []
-
-        # Read in XML
-        _ignore_etree, servers_node = xmlutil.readXML(xmlFile.path, ELEMENT_SERVERS)
-        self._parseXML(servers_node)
-
-
-    def _parseXML(self, node):
-        """
-        Parse the XML root node from the server-to-server configuration document.
-        @param node: the L{Node} to parse.
-        """
-
-        for child in node.getchildren():
-            if child.tag == ELEMENT_SERVER:
-                self.servers.append(IScheduleServerRecord())
-                self.servers[-1].parseXML(child)
-
-
-
-class IScheduleServerRecord (object):
-    """
-    Contains server-to-server details.
-    """
-    def __init__(self, uri=None):
-        """
-        @param recordType: record type for directory entry.
-        """
-        self.uri = ""
-        self.authentication = None
-        self.allow_from = False
-        self.allow_to = True
-        self.domains = []
-        self.client_hosts = []
-        self.unNormalizeAddresses = True
-        self.moreHeaders = []
-
-        if uri:
-            self.uri = uri
-            self._parseDetails()
-
-
-    def details(self):
-        return (self.ssl, self.host, self.port, self.path,)
-
-
-    def redirect(self, location):
-        """
-        Permanent redirect for the lifetime of this record.
-        """
-        self.uri = location
-        self._parseDetails()
-
-
-    def parseXML(self, node):
-        for child in node.getchildren():
-            if child.tag == ELEMENT_URI:
-                self.uri = child.text
-            elif child.tag == ELEMENT_AUTHENTICATION:
-                self._parseAuthentication(child)
-            elif child.tag == ELEMENT_ALLOW_REQUESTS_FROM:
-                self.allow_from = True
-            elif child.tag == ELEMENT_ALLOW_REQUESTS_TO:
-                self.allow_to = True
-            elif child.tag == ELEMENT_DOMAINS:
-                self._parseList(child, ELEMENT_DOMAIN, self.domains)
-            elif child.tag == ELEMENT_CLIENT_HOSTS:
-                self._parseList(child, ELEMENT_HOST, self.client_hosts)
-            else:
-                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child.tag,))
-
-        self._parseDetails()
-
-
-    def _parseList(self, node, element_name, appendto):
-        for child in node.getchildren():
-            if child.tag == element_name:
-                appendto.append(child.text)
-
-
-    def _parseAuthentication(self, node):
-        if node.get(ATTRIBUTE_TYPE) != ATTRIBUTE_BASICAUTH:
-            return
-
-        for child in node.getchildren():
-            if child.tag == ELEMENT_USER:
-                user = child.text
-            elif child.tag == ELEMENT_PASSWORD:
-                password = child.text
-
-        self.authentication = ("basic", user, password,)
-
-
-    def _parseDetails(self):
-        # Extract scheme, host, port and path
-        if self.uri.startswith("http://"):
-            self.ssl = False
-            rest = self.uri[7:]
-        elif self.uri.startswith("https://"):
-            self.ssl = True
-            rest = self.uri[8:]
-
-        splits = rest.split("/", 1)
-        hostport = splits[0].split(":")
-        self.host = hostport[0]
-        if len(hostport) > 1:
-            self.port = int(hostport[1])
-        else:
-            self.port = {False: 80, True: 443}[self.ssl]
-        self.path = "/"
-        if len(splits) > 1:
-            self.path += splits[1]

Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/remoteservers.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,227 @@
+##
+# Copyright (c) 2006-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.python.filepath import CachingFilePath as FilePath
+
+from twext.python.log import Logger
+
+from twistedcaldav.config import config, fullServerPath
+from twistedcaldav import xmlutil
+
+"""
+XML based iSchedule configuration file handling. This is for handling of remote servers. The localservers.py module
+handles servers that are local (partitioned or podded).
+"""
+
+__all__ = [
+    "IScheduleServers",
+]
+
+log = Logger()
+
+
+
+class IScheduleServers(object):
+
+    _fileInfo = None
+    _xmlFile = None
+    _servers = None
+    _domainMap = None
+
+    def __init__(self):
+
+        self._loadConfig()
+
+
+    def _loadConfig(self):
+        if config.Scheduling.iSchedule.RemoteServers:
+            if IScheduleServers._servers is None:
+                IScheduleServers._xmlFile = FilePath(
+                    fullServerPath(
+                        config.ConfigRoot,
+                        config.Scheduling.iSchedule.RemoteServers,
+                    )
+                )
+            if IScheduleServers._xmlFile.exists():
+                IScheduleServers._xmlFile.restat()
+                fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
+                if fileInfo != IScheduleServers._fileInfo:
+                    parser = IScheduleServersParser(IScheduleServers._xmlFile)
+                    IScheduleServers._servers = parser.servers
+                    self._mapDomains()
+                    IScheduleServers._fileInfo = fileInfo
+            else:
+                IScheduleServers._servers = ()
+                IScheduleServers._domainMap = {}
+
+        else:
+            IScheduleServers._servers = ()
+            IScheduleServers._domainMap = {}
+
+
+    def _mapDomains(self):
+        IScheduleServers._domainMap = {}
+        for server in IScheduleServers._servers:
+            for domain in server.domains:
+                IScheduleServers._domainMap[domain] = server
+
+
+    def mapDomain(self, domain):
+        """
+        Map a calendar user address domain to a suitable server that can
+        handle server-to-server requests for that user.
+        """
+        return IScheduleServers._domainMap.get(domain)
+
+ELEMENT_SERVERS = "servers"
+ELEMENT_SERVER = "server"
+ELEMENT_URI = "uri"
+ELEMENT_AUTHENTICATION = "authentication"
+ATTRIBUTE_TYPE = "type"
+ATTRIBUTE_BASICAUTH = "basic"
+ELEMENT_USER = "user"
+ELEMENT_PASSWORD = "password"
+ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
+ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
+ELEMENT_DOMAINS = "domains"
+ELEMENT_DOMAIN = "domain"
+ELEMENT_CLIENT_HOSTS = "hosts"
+ELEMENT_HOST = "host"
+
+
+
+class IScheduleServersParser(object):
+    """
+    Server-to-server configuration file parser.
+    """
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+
+    def __init__(self, xmlFile):
+
+        self.servers = []
+
+        # Read in XML
+        _ignore_etree, servers_node = xmlutil.readXML(xmlFile.path, ELEMENT_SERVERS)
+        self._parseXML(servers_node)
+
+
+    def _parseXML(self, node):
+        """
+        Parse the XML root node from the server-to-server configuration document.
+        @param node: the L{Node} to parse.
+        """
+
+        for child in node.getchildren():
+            if child.tag == ELEMENT_SERVER:
+                self.servers.append(IScheduleServerRecord())
+                self.servers[-1].parseXML(child)
+
+
+
+class IScheduleServerRecord (object):
+    """
+    Contains server-to-server details.
+    """
+    def __init__(self, uri=None):
+        """
+        @param recordType: record type for directory entry.
+        """
+        self.uri = ""
+        self.authentication = None
+        self.allow_from = False
+        self.allow_to = True
+        self.domains = []
+        self.client_hosts = []
+        self.unNormalizeAddresses = True
+        self.moreHeaders = []
+
+        if uri:
+            self.uri = uri
+            self._parseDetails()
+
+
+    def details(self):
+        return (self.ssl, self.host, self.port, self.path,)
+
+
+    def redirect(self, location):
+        """
+        Permanent redirect for the lifetime of this record.
+        """
+        self.uri = location
+        self._parseDetails()
+
+
+    def parseXML(self, node):
+        for child in node.getchildren():
+            if child.tag == ELEMENT_URI:
+                self.uri = child.text
+            elif child.tag == ELEMENT_AUTHENTICATION:
+                self._parseAuthentication(child)
+            elif child.tag == ELEMENT_ALLOW_REQUESTS_FROM:
+                self.allow_from = True
+            elif child.tag == ELEMENT_ALLOW_REQUESTS_TO:
+                self.allow_to = True
+            elif child.tag == ELEMENT_DOMAINS:
+                self._parseList(child, ELEMENT_DOMAIN, self.domains)
+            elif child.tag == ELEMENT_CLIENT_HOSTS:
+                self._parseList(child, ELEMENT_HOST, self.client_hosts)
+            else:
+                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child.tag,))
+
+        self._parseDetails()
+
+
+    def _parseList(self, node, element_name, appendto):
+        for child in node.getchildren():
+            if child.tag == element_name:
+                appendto.append(child.text)
+
+
+    def _parseAuthentication(self, node):
+        if node.get(ATTRIBUTE_TYPE) != ATTRIBUTE_BASICAUTH:
+            return
+
+        for child in node.getchildren():
+            if child.tag == ELEMENT_USER:
+                user = child.text
+            elif child.tag == ELEMENT_PASSWORD:
+                password = child.text
+
+        self.authentication = ("basic", user, password,)
+
+
+    def _parseDetails(self):
+        # Extract scheme, host, port and path
+        if self.uri.startswith("http://"):
+            self.ssl = False
+            rest = self.uri[7:]
+        elif self.uri.startswith("https://"):
+            self.ssl = True
+            rest = self.uri[8:]
+
+        splits = rest.split("/", 1)
+        hostport = splits[0].split(":")
+        self.host = hostport[0]
+        if len(hostport) > 1:
+            self.port = int(hostport[1])
+        else:
+            self.port = {False: 80, True: 443}[self.ssl]
+        self.path = "/"
+        if len(splits) > 1:
+            self.path += splits[1]

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,245 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from pycalendar.datetime import PyCalendarDateTime
-from pycalendar.timezone import PyCalendarTimezone
-from twext.web2 import responsecode
-from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.http import Response, HTTPError, StatusResponse, XMLResponse
-from twext.web2.http_headers import MimeType
-from twisted.internet.defer import succeed, returnValue, inlineCallbacks
-from twistedcaldav import caldavxml
-from twistedcaldav.config import config
-from twistedcaldav.extensions import DAVResource, \
-    DAVResourceWithoutChildrenMixin
-from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
-from twistedcaldav.scheduling.caldav.resource import deliverSchedulePrivilegeSet
-from twistedcaldav.scheduling.ischedule.scheduler import IScheduleScheduler
-from txdav.xml import element as davxml
-import twistedcaldav.scheduling.ischedule.xml  as ischedulexml
-
-__all__ = [
-    "IScheduleInboxResource",
-]
-
-class IScheduleInboxResource (ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
-    """
-    iSchedule Inbox resource.
-
-    Extends L{DAVResource} to provide iSchedule inbox functionality.
-    """
-
-    def __init__(self, parent, store):
-        """
-        @param parent: the parent resource of this one.
-        """
-        assert parent is not None
-
-        DAVResource.__init__(self, principalCollections=parent.principalCollections())
-
-        self.parent = parent
-        self._newStore = store
-
-
-    def deadProperties(self):
-        if not hasattr(self, "_dead_properties"):
-            self._dead_properties = NonePropertyStore(self)
-        return self._dead_properties
-
-
-    def etag(self):
-        return succeed(None)
-
-
-    def checkPreconditions(self, request):
-        return None
-
-
-    def resourceType(self):
-        return davxml.ResourceType.ischeduleinbox
-
-
-    def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8")
-
-
-    def isCollection(self):
-        return False
-
-
-    def isCalendarCollection(self):
-        return False
-
-
-    def isPseudoCalendarCollection(self):
-        return False
-
-
-    def principalForCalendarUserAddress(self, address):
-        for principalCollection in self.principalCollections():
-            principal = principalCollection.principalForCalendarUserAddress(address)
-            if principal is not None:
-                return principal
-        return None
-
-
-    def render(self, request):
-        output = """<html>
-<head>
-<title>Server To Server Inbox Resource</title>
-</head>
-<body>
-<h1>Server To Server Inbox Resource.</h1>
-</body
-</html>"""
-
-        response = Response(200, {}, output)
-        response.headers.setHeader("content-type", MimeType("text", "html"))
-        return response
-
-
-    def http_GET(self, request):
-        """
-        The iSchedule GET method.
-        """
-
-        if not request.args:
-            # Do normal GET behavior
-            return self.render(request)
-
-        action = request.args.get("action", ("",))
-        if len(action) != 1:
-            raise HTTPError(StatusResponse(
-                responsecode.BAD_REQUEST,
-                "Invalid action parameter",
-            ))
-        action = action[0]
-
-        action = {
-            "capabilities"  : self.doCapabilities,
-        }.get(action, None)
-
-        if action is None:
-            raise HTTPError(StatusResponse(
-                responsecode.BAD_REQUEST,
-                "Unknown action action parameter",
-            ))
-
-        return action(request)
-
-
-    def doCapabilities(self, request):
-        """
-        Return a list of all timezones known to the server.
-        """
-
-        # Determine min/max date-time for iSchedule
-        now = PyCalendarDateTime.getNowUTC()
-        minDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
-        minDateTime.offsetYear(-1)
-        maxDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
-        maxDateTime.offsetYear(10)
-
-        result = ischedulexml.QueryResult(
-
-            ischedulexml.Capabilities(
-                ischedulexml.Versions(
-                    ischedulexml.Version.fromString("1.0"),
-                ),
-                ischedulexml.SchedulingMessages(
-                    ischedulexml.Component(
-                        ischedulexml.Method(name="REQUEST"),
-                        ischedulexml.Method(name="CANCEL"),
-                        ischedulexml.Method(name="REPLY"),
-                        name="VEVENT"
-                    ),
-                    ischedulexml.Component(
-                        ischedulexml.Method(name="REQUEST"),
-                        ischedulexml.Method(name="CANCEL"),
-                        ischedulexml.Method(name="REPLY"),
-                        name="VTODO"
-                    ),
-                    ischedulexml.Component(
-                        ischedulexml.Method(name="REQUEST"),
-                        name="VFREEBUSY"
-                    ),
-                ),
-                ischedulexml.CalendarDataTypes(
-                    ischedulexml.CalendarDataType(**{
-                            "content-type": "text/calendar",
-                            "version": "2.0",
-                    }),
-                ),
-                ischedulexml.Attachments(
-                    ischedulexml.External(),
-                ),
-                ischedulexml.MaxContentLength.fromString(config.MaxResourceSize),
-                ischedulexml.MinDateTime.fromString(minDateTime.getText()),
-                ischedulexml.MaxDateTime.fromString(maxDateTime.getText()),
-                ischedulexml.MaxInstances.fromString(config.MaxAllowedInstances),
-                ischedulexml.MaxRecipients.fromString(config.MaxAttendeesPerInstance),
-                ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")),
-            ),
-        )
-        return XMLResponse(responsecode.OK, result)
-
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The server-to-server POST method.
-        """
-
-        # This is a server-to-server scheduling operation.
-        scheduler = IScheduleScheduler(request, self)
-
-        # Need a transaction to work with
-        txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
-        request._newStoreTransaction = txn
-
-        # Do the POST processing treating this as a non-local schedule
-        try:
-            result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
-        except Exception, e:
-            yield txn.abort()
-            raise e
-        else:
-            yield txn.commit()
-        returnValue(result.response())
-
-    ##
-    # ACL
-    ##
-
-
-    def supportedPrivileges(self, request):
-        return succeed(deliverSchedulePrivilegeSet)
-
-
-    def defaultAccessControlList(self):
-        privs = (
-            davxml.Privilege(davxml.Read()),
-            davxml.Privilege(caldavxml.ScheduleDeliver()),
-        )
-
-        return davxml.ACL(
-            # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(*privs),
-                davxml.Protected(),
-            ),
-        )

Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,245 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.timezone import PyCalendarTimezone
+from twext.web2 import responsecode
+from twext.web2.dav.noneprops import NonePropertyStore
+from twext.web2.http import Response, HTTPError, StatusResponse, XMLResponse
+from twext.web2.http_headers import MimeType
+from twisted.internet.defer import succeed, returnValue, inlineCallbacks
+from twistedcaldav import caldavxml
+from twistedcaldav.config import config
+from twistedcaldav.extensions import DAVResource, \
+    DAVResourceWithoutChildrenMixin
+from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
+from twistedcaldav.scheduling.caldav.resource import deliverSchedulePrivilegeSet
+from twistedcaldav.scheduling.ischedule.scheduler import IScheduleScheduler
+from txdav.xml import element as davxml
+import twistedcaldav.scheduling.ischedule.xml  as ischedulexml
+
+__all__ = [
+    "IScheduleInboxResource",
+]
+
+class IScheduleInboxResource (ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
+    """
+    iSchedule Inbox resource.
+
+    Extends L{DAVResource} to provide iSchedule inbox functionality.
+    """
+
+    def __init__(self, parent, store):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
+
+        DAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+        self.parent = parent
+        self._newStore = store
+
+
+    def deadProperties(self):
+        if not hasattr(self, "_dead_properties"):
+            self._dead_properties = NonePropertyStore(self)
+        return self._dead_properties
+
+
+    def etag(self):
+        return succeed(None)
+
+
+    def checkPreconditions(self, request):
+        return None
+
+
+    def resourceType(self):
+        return davxml.ResourceType.ischeduleinbox
+
+
+    def contentType(self):
+        return MimeType.fromString("text/html; charset=utf-8")
+
+
+    def isCollection(self):
+        return False
+
+
+    def isCalendarCollection(self):
+        return False
+
+
+    def isPseudoCalendarCollection(self):
+        return False
+
+
+    def principalForCalendarUserAddress(self, address):
+        for principalCollection in self.principalCollections():
+            principal = principalCollection.principalForCalendarUserAddress(address)
+            if principal is not None:
+                return principal
+        return None
+
+
+    def render(self, request):
+        output = """<html>
+<head>
+<title>Server To Server Inbox Resource</title>
+</head>
+<body>
+<h1>Server To Server Inbox Resource.</h1>
+</body
+</html>"""
+
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
+
+
+    def http_GET(self, request):
+        """
+        The iSchedule GET method.
+        """
+
+        if not request.args:
+            # Do normal GET behavior
+            return self.render(request)
+
+        action = request.args.get("action", ("",))
+        if len(action) != 1:
+            raise HTTPError(StatusResponse(
+                responsecode.BAD_REQUEST,
+                "Invalid action parameter",
+            ))
+        action = action[0]
+
+        action = {
+            "capabilities"  : self.doCapabilities,
+        }.get(action, None)
+
+        if action is None:
+            raise HTTPError(StatusResponse(
+                responsecode.BAD_REQUEST,
+                "Unknown action action parameter",
+            ))
+
+        return action(request)
+
+
+    def doCapabilities(self, request):
+        """
+        Return a list of all timezones known to the server.
+        """
+
+        # Determine min/max date-time for iSchedule
+        now = PyCalendarDateTime.getNowUTC()
+        minDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
+        minDateTime.offsetYear(-1)
+        maxDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
+        maxDateTime.offsetYear(10)
+
+        result = ischedulexml.QueryResult(
+
+            ischedulexml.Capabilities(
+                ischedulexml.Versions(
+                    ischedulexml.Version.fromString("1.0"),
+                ),
+                ischedulexml.SchedulingMessages(
+                    ischedulexml.Component(
+                        ischedulexml.Method(name="REQUEST"),
+                        ischedulexml.Method(name="CANCEL"),
+                        ischedulexml.Method(name="REPLY"),
+                        name="VEVENT"
+                    ),
+                    ischedulexml.Component(
+                        ischedulexml.Method(name="REQUEST"),
+                        ischedulexml.Method(name="CANCEL"),
+                        ischedulexml.Method(name="REPLY"),
+                        name="VTODO"
+                    ),
+                    ischedulexml.Component(
+                        ischedulexml.Method(name="REQUEST"),
+                        name="VFREEBUSY"
+                    ),
+                ),
+                ischedulexml.CalendarDataTypes(
+                    ischedulexml.CalendarDataType(**{
+                            "content-type": "text/calendar",
+                            "version": "2.0",
+                    }),
+                ),
+                ischedulexml.Attachments(
+                    ischedulexml.External(),
+                ),
+                ischedulexml.MaxContentLength.fromString(config.MaxResourceSize),
+                ischedulexml.MinDateTime.fromString(minDateTime.getText()),
+                ischedulexml.MaxDateTime.fromString(maxDateTime.getText()),
+                ischedulexml.MaxInstances.fromString(config.MaxAllowedInstances),
+                ischedulexml.MaxRecipients.fromString(config.MaxAttendeesPerInstance),
+                ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")),
+            ),
+        )
+        return XMLResponse(responsecode.OK, result)
+
+
+    @inlineCallbacks
+    def http_POST(self, request):
+        """
+        The server-to-server POST method.
+        """
+
+        # This is a server-to-server scheduling operation.
+        scheduler = IScheduleScheduler(request, self)
+
+        # Need a transaction to work with
+        txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
+        request._newStoreTransaction = txn
+
+        # Do the POST processing treating this as a non-local schedule
+        try:
+            result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
+        except Exception, e:
+            yield txn.abort()
+            raise e
+        else:
+            yield txn.commit()
+        returnValue(result.response())
+
+    ##
+    # ACL
+    ##
+
+
+    def supportedPrivileges(self, request):
+        return succeed(deliverSchedulePrivilegeSet)
+
+
+    def defaultAccessControlList(self):
+        privs = (
+            davxml.Privilege(davxml.Read()),
+            davxml.Privilege(caldavxml.ScheduleDeliver()),
+        )
+
+        return davxml.ACL(
+            # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
+            davxml.ACE(
+                davxml.Principal(davxml.All()),
+                davxml.Grant(*privs),
+                davxml.Protected(),
+            ),
+        )

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

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

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -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/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/data/db.example.com	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com	2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,19 +0,0 @@
-example.com.		  10800 IN SOA	ns.example.com. 	admin.example.com. (
-                                                        2012090810 ; serial
-                                                        3600       ; refresh (1 hour)
-                                                        900        ; retry (15 minutes)
-                                                        1209600    ; expire (2 weeks)
-                                                        86400      ; minimum (1 day)
-							)
-									10800 IN NS		ns.example.com.
-									10800 IN A		127.0.0.1
-ns.example.com.						10800 IN A		127.0.0.1
-
-_caldavs._tcp.example.com.			10800 IN SRV	0	0	8443	example.com.
-_ischedules._tcp.example.com.		10800 IN SRV	0	0	8443	example.com.
-
-_ischedule._domainkey.example.com.	10800 IN TXT	"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjUfDqd8ICAL0dyq2KdjKN6LS8O/Y4yMxOxgATqtSIMi7baKXEs1w5Wj9efOC2nU+aqyhP2/J6AzfFJfSB+GV5gcIT+LAC4btJKPGjPUyXcQFJV4a73y0jIgCTBzWxdaP6qD9P9rzYlvMPcdrrKiKoAOtI3JZqAAdZudOmGlc4QQIDAQAB"
-_revoked._domainkey.example.com.	10800 IN TXT	"v=DKIM1; p="
-
-_domainkey._tcp.example.com.		10800 IN SRV	0	0	8443	key.example.com.
-_domainkey._tcp.www.example.com.	10800 IN SRV	0	0	80		key.example.com.

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

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

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

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_delivery.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,62 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import twistedcaldav.test.util
-from twisted.internet.defer import inlineCallbacks
-from twisted.python.modules import getModule
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule import utils
-from twisted.names import client
-from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
-
-class CalDAV (twistedcaldav.test.util.TestCase):
-    """
-    twistedcaldav.scheduling.caldav tests
-    """
-
-    def tearDown(self):
-        """
-        By setting the resolver to None, it will be recreated next time a name
-        lookup is done.
-        """
-        client.theResolver = None
-        utils.DebugResolver = None
-
-
-    @inlineCallbacks
-    def test_matchCalendarUserAddress(self):
-        """
-        Make sure we do an exact comparison on EmailDomain
-        """
-
-        self.patch(config.Scheduling.iSchedule, "RemoteServers", "")
-
-        # Only mailtos:
-        result = yield ScheduleViaISchedule.matchCalendarUserAddress("http://example.com/principal/user")
-        self.assertFalse(result)
-
-        # Need to setup a fake resolver
-        module = getModule(__name__)
-        dataPath = module.filePath.sibling("data")
-        bindPath = dataPath.child("db.example.com")
-        self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
-        utils.DebugResolver = None
-        utils._initResolver()
-
-        result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.com")
-        self.assertTrue(result)
-        result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.org")
-        self.assertFalse(result)

Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_delivery.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,62 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import twistedcaldav.test.util
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.modules import getModule
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule import utils
+from twisted.names import client
+from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
+
+class CalDAV (twistedcaldav.test.util.TestCase):
+    """
+    twistedcaldav.scheduling.caldav tests
+    """
+
+    def tearDown(self):
+        """
+        By setting the resolver to None, it will be recreated next time a name
+        lookup is done.
+        """
+        client.theResolver = None
+        utils.DebugResolver = None
+
+
+    @inlineCallbacks
+    def test_matchCalendarUserAddress(self):
+        """
+        Make sure we do an exact comparison on EmailDomain
+        """
+
+        self.patch(config.Scheduling.iSchedule, "RemoteServers", "")
+
+        # Only mailtos:
+        result = yield ScheduleViaISchedule.matchCalendarUserAddress("http://example.com/principal/user")
+        self.assertFalse(result)
+
+        # Need to setup a fake resolver
+        module = getModule(__name__)
+        dataPath = module.filePath.sibling("data")
+        bindPath = dataPath.child("db.example.com")
+        self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+        utils.DebugResolver = None
+        utils._initResolver()
+
+        result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.com")
+        self.assertTrue(result)
+        result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.org")
+        self.assertFalse(result)

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

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

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

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

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

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

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

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

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

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

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/utils.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,182 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.python.log import Logger
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.names import dns
-from twisted.names.authority import BindAuthority
-from twisted.names.client import getResolver
-from twisted.names.error import DomainError, AuthoritativeDomainError
-
-from twistedcaldav.config import config
-
-import socket
-
-log = Logger()
-
-DebugResolver = None
-
-
-def getIPsFromHost(host):
-    """
-    Map a hostname to an IPv4 or IPv6 address.
-
-    @param host: the hostname
-    @type host: C{str}
-
-    @return: a C{set} of IPs
-    """
-    ips = set()
-    for family in (socket.AF_INET, socket.AF_INET6):
-        results = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
-        for _ignore_family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in results:
-            ips.add(sockaddr[0])
-
-    return ips
-
-
-
- at inlineCallbacks
-def lookupServerViaSRV(domain, service="_ischedules"):
-
-    _initResolver()
-
-    lookup = "%s._tcp.%s" % (service, domain,)
-    log.debug("DNS SRV: lookup: %s" % (lookup,))
-    try:
-        answers = (yield DebugResolver.lookupService(lookup))
-    except (DomainError, AuthoritativeDomainError), e:
-        log.debug("DNS SRV: lookup failed: %s" % (e,))
-        returnValue(None)
-
-    if len(answers) == 1 and answers[0].type == dns.SRV \
-                         and answers[0].payload \
-                         and answers[0].payload.target == dns.Name('.'):
-        # decidedly not available
-        log.debug("DNS SRV: disabled: %s" % (lookup,))
-        returnValue(None)
-
-    servers = []
-    for a in answers:
-
-        if a.type != dns.SRV or not a.payload:
-            continue
-
-        servers.append((a.payload.priority, a.payload.weight, str(a.payload.target), a.payload.port))
-
-    log.debug("DNS SRV: lookup results: %s\n%s" % (lookup, servers,))
-
-
-    def _serverCmp(a, b):
-        if a[0] != b[0]:
-            return cmp(a[0], b[0])
-        else:
-            return cmp(a[1], b[1])
-
-    servers.sort(_serverCmp)
-    minPriority = servers[0][0]
-
-    weightIndex = zip(xrange(len(servers)), [x[1] for x in servers if x[0] == minPriority])
-    weightSum = reduce(lambda x, y: (None, x[1] + y[1]), weightIndex, (None, 0))[1]
-
-    for index, weight in weightIndex:
-        weightSum -= weight
-        if weightSum <= 0:
-            chosen = servers[index]
-            _ignore_p, _ignore_w, host, port = chosen
-            host = host.rstrip(".")
-            break
-    else:
-        log.debug("DNS SRV: unable to determine best record to use: %s" % (lookup,))
-        returnValue(None)
-
-    log.debug("DNS SRV: lookup chosen service: %s %s %s" % (lookup, host, port,))
-    returnValue((host, port,))
-
-
-
- at inlineCallbacks
-def lookupDataViaTXT(domain, prefix=""):
-
-    _initResolver()
-
-    lookup = "%s.%s" % (prefix, domain,) if prefix else domain
-    log.debug("DNS TXT: lookup: %s" % (lookup,))
-    try:
-        answers = (yield DebugResolver.lookupText(lookup))
-    except (DomainError, AuthoritativeDomainError), e:
-        log.debug("DNS TXT: lookup failed: %s" % (e,))
-        answers = ()
-
-    results = []
-    for a in answers:
-
-        if a.type != dns.TXT or not a.payload:
-            continue
-
-        results.append("".join(a.payload.data))
-
-    log.debug("DNS TXT: lookup results: %s\n%s" % (lookup, "\n".join(results),))
-    returnValue(results)
-
-
-
-class FakeBindAuthority(BindAuthority):
-
-    @inlineCallbacks
-    def _lookup(self, name, cls, type, timeout=None):
-        log.debug("DNS FakeBindAuthority: lookup: %s %s %s" % (name, cls, type,))
-        result = yield BindAuthority._lookup(self, name, cls, type, timeout)
-        log.debug("DNS FakeBindAuthority: lookup results: %s %s %s\n%s" % (name, cls, type, result[0]))
-        returnValue(result[0])
-
-
-    def stripComments(self, lines):
-        """
-        Work around a bug in the base implementation that causes parsing of TXT RRs with
-        a ; in the RDATA to fail because the ; is treated as the start of a comment. Here
-        we simply ignore all comments.
-        """
-        return [
-            (a.find(';') == -1 or "TXT" in a) and a or a[:a.find(';')] for a in [
-                b.strip() for b in lines
-            ]
-        ]
-
-
-    def parseLines(self, lines):
-        """
-        Work around a bug in the base implementation that causes parsing of TXT RRs with
-        spaces in the RDATA to be broken into multiple fragments and for quotes around the
-        data to not be removed.
-        """
-        for line in lines:
-            if line[3] == "TXT":
-                line[4] = " ".join(line[4:])[1:-1]
-                del line[5:]
-
-        BindAuthority.parseLines(self, lines)
-
-
-
-def _initResolver():
-    global DebugResolver
-    if DebugResolver is None:
-        if config.Scheduling.iSchedule.DNSDebug:
-            DebugResolver = FakeBindAuthority(config.Scheduling.iSchedule.DNSDebug)
-        else:
-            DebugResolver = getResolver()

Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/utils.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,182 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.python.log import Logger
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.names import dns
+from twisted.names.authority import BindAuthority
+from twisted.names.client import getResolver
+from twisted.names.error import DomainError, AuthoritativeDomainError
+
+from twistedcaldav.config import config
+
+import socket
+
+log = Logger()
+
+DebugResolver = None
+
+
+def getIPsFromHost(host):
+    """
+    Map a hostname to an IPv4 or IPv6 address.
+
+    @param host: the hostname
+    @type host: C{str}
+
+    @return: a C{set} of IPs
+    """
+    ips = set()
+    for family in (socket.AF_INET, socket.AF_INET6):
+        results = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
+        for _ignore_family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in results:
+            ips.add(sockaddr[0])
+
+    return ips
+
+
+
+ at inlineCallbacks
+def lookupServerViaSRV(domain, service="_ischedules"):
+
+    _initResolver()
+
+    lookup = "%s._tcp.%s" % (service, domain,)
+    log.debug("DNS SRV: lookup: %s" % (lookup,))
+    try:
+        answers = (yield DebugResolver.lookupService(lookup))
+    except (DomainError, AuthoritativeDomainError), e:
+        log.debug("DNS SRV: lookup failed: %s" % (e,))
+        returnValue(None)
+
+    if len(answers) == 1 and answers[0].type == dns.SRV \
+                         and answers[0].payload \
+                         and answers[0].payload.target == dns.Name('.'):
+        # decidedly not available
+        log.debug("DNS SRV: disabled: %s" % (lookup,))
+        returnValue(None)
+
+    servers = []
+    for a in answers:
+
+        if a.type != dns.SRV or not a.payload:
+            continue
+
+        servers.append((a.payload.priority, a.payload.weight, str(a.payload.target), a.payload.port))
+
+    log.debug("DNS SRV: lookup results: %s\n%s" % (lookup, servers,))
+
+
+    def _serverCmp(a, b):
+        if a[0] != b[0]:
+            return cmp(a[0], b[0])
+        else:
+            return cmp(a[1], b[1])
+
+    servers.sort(_serverCmp)
+    minPriority = servers[0][0]
+
+    weightIndex = zip(xrange(len(servers)), [x[1] for x in servers if x[0] == minPriority])
+    weightSum = reduce(lambda x, y: (None, x[1] + y[1]), weightIndex, (None, 0))[1]
+
+    for index, weight in weightIndex:
+        weightSum -= weight
+        if weightSum <= 0:
+            chosen = servers[index]
+            _ignore_p, _ignore_w, host, port = chosen
+            host = host.rstrip(".")
+            break
+    else:
+        log.debug("DNS SRV: unable to determine best record to use: %s" % (lookup,))
+        returnValue(None)
+
+    log.debug("DNS SRV: lookup chosen service: %s %s %s" % (lookup, host, port,))
+    returnValue((host, port,))
+
+
+
+ at inlineCallbacks
+def lookupDataViaTXT(domain, prefix=""):
+
+    _initResolver()
+
+    lookup = "%s.%s" % (prefix, domain,) if prefix else domain
+    log.debug("DNS TXT: lookup: %s" % (lookup,))
+    try:
+        answers = (yield DebugResolver.lookupText(lookup))
+    except (DomainError, AuthoritativeDomainError), e:
+        log.debug("DNS TXT: lookup failed: %s" % (e,))
+        answers = ()
+
+    results = []
+    for a in answers:
+
+        if a.type != dns.TXT or not a.payload:
+            continue
+
+        results.append("".join(a.payload.data))
+
+    log.debug("DNS TXT: lookup results: %s\n%s" % (lookup, "\n".join(results),))
+    returnValue(results)
+
+
+
+class FakeBindAuthority(BindAuthority):
+
+    @inlineCallbacks
+    def _lookup(self, name, cls, type, timeout=None):
+        log.debug("DNS FakeBindAuthority: lookup: %s %s %s" % (name, cls, type,))
+        result = yield BindAuthority._lookup(self, name, cls, type, timeout)
+        log.debug("DNS FakeBindAuthority: lookup results: %s %s %s\n%s" % (name, cls, type, result[0]))
+        returnValue(result[0])
+
+
+    def stripComments(self, lines):
+        """
+        Work around a bug in the base implementation that causes parsing of TXT RRs with
+        a ; in the RDATA to fail because the ; is treated as the start of a comment. Here
+        we simply ignore all comments.
+        """
+        return [
+            (a.find(';') == -1 or "TXT" in a) and a or a[:a.find(';')] for a in [
+                b.strip() for b in lines
+            ]
+        ]
+
+
+    def parseLines(self, lines):
+        """
+        Work around a bug in the base implementation that causes parsing of TXT RRs with
+        spaces in the RDATA to be broken into multiple fragments and for quotes around the
+        data to not be removed.
+        """
+        for line in lines:
+            if line[3] == "TXT":
+                line[4] = " ".join(line[4:])[1:-1]
+                del line[5:]
+
+        BindAuthority.parseLines(self, lines)
+
+
+
+def _initResolver():
+    global DebugResolver
+    if DebugResolver is None:
+        if config.Scheduling.iSchedule.DNSDebug:
+            DebugResolver = FakeBindAuthority(config.Scheduling.iSchedule.DNSDebug)
+        else:
+            DebugResolver = getResolver()

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py	2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,281 +0,0 @@
-##
-# Copyright (c) 2011-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-This module provides XML definitions for use with Timezone Standard Service.
-"""
-
-from twistedcaldav.config import config
-from twistedcaldav.ical import Component as iComponent
-from txdav.xml.element import PCDATAElement, WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
-from txdav.xml.element import registerElement
-
-
-##
-# iSchedule XML Definitions
-##
-
-ischedule_namespace = "urn:ietf:params:xml:ns:ischedule"
-
-
- at registerElement
-class QueryResult (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "query-result"
-
-    allowed_children = {
-        (ischedule_namespace, "capability-set"): (0, None),
-    }
-
-
-
- at registerElement
-class Capabilities (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "capabilities"
-
-    allowed_children = {
-        (ischedule_namespace, "versions"): (1, 1),
-        (ischedule_namespace, "scheduling-messages"): (1, 1),
-        (ischedule_namespace, "calendar-data-types"): (1, 1),
-        (ischedule_namespace, "attachments"): (1, 1),
-        (ischedule_namespace, "supported-recipient-uri-scheme-set"): (1, 1),
-        (ischedule_namespace, "max-content-length"): (1, 1),
-        (ischedule_namespace, "min-date-time"): (1, 1),
-        (ischedule_namespace, "max-date-time"): (1, 1),
-        (ischedule_namespace, "max-instances"): (1, 1),
-        (ischedule_namespace, "max-recipients"): (1, 1),
-        (ischedule_namespace, "administrator"): (1, 1),
-    }
-
-
-
- at registerElement
-class Versions (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "versions"
-
-    allowed_children = {
-        (ischedule_namespace, "version"): (1, None),
-    }
-
-
-
- at registerElement
-class Version (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "version"
-
-
-
- at registerElement
-class SchedulingMessages (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "scheduling-messages"
-
-    allowed_children = {
-        (ischedule_namespace, "component"): (1, None),
-    }
-
-
-
- at registerElement
-class Component (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "component"
-
-    allowed_children = {
-        (ischedule_namespace, "method"): (0, None),
-    }
-    allowed_attributes = {"name": True}
-
-
-
- at registerElement
-class Method (WebDAVEmptyElement):
-    namespace = ischedule_namespace
-    name = "method"
-
-    allowed_attributes = {"name": True}
-
-
-
- at registerElement
-class CalendarDataTypes (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "calendar-data-types"
-
-    allowed_children = {
-        (ischedule_namespace, "calendar-data-type"): (1, None),
-    }
-
-
-
- at registerElement
-class CalendarDataType (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "calendar-data-type"
-
-    allowed_attributes = {
-        "content-type": True,
-        "version": True,
-    }
-
-
-
- at registerElement
-class Attachments (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "attachments"
-
-    allowed_children = {
-        (ischedule_namespace, "inline"): (0, 1),
-        (ischedule_namespace, "external"): (0, 1),
-    }
-
-
-
- at registerElement
-class Inline (WebDAVEmptyElement):
-    namespace = ischedule_namespace
-    name = "inline"
-
-
-
- at registerElement
-class External (WebDAVEmptyElement):
-    namespace = ischedule_namespace
-    name = "external"
-
-
-
- at registerElement
-class MaxContentLength (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "max-content-length"
-
-
-
- at registerElement
-class MinDateTime (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "min-date-time"
-
-
-
- at registerElement
-class MaxDateTime (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "max-date-time"
-
-
-
- at registerElement
-class MaxInstances (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "max-instances"
-
-
-
- at registerElement
-class MaxRecipients (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "max-recipients"
-
-
-
- at registerElement
-class Administrator (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "administrator"
-
-
-
- at registerElement
-class ScheduleResponse (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "schedule-response"
-
-    allowed_children = {
-        (ischedule_namespace, "response"): (0, None),
-    }
-
-
-
- at registerElement
-class Response (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "response"
-
-    allowed_children = {
-        (ischedule_namespace, "recipient"): (1, 1),
-        (ischedule_namespace, "request-status"): (1, 1),
-        (ischedule_namespace, "calendar-data"): (0, 1),
-        (ischedule_namespace, "error"): (0, 1),
-        (ischedule_namespace, "response-description"): (0, 1),
-    }
-
-
-
- at registerElement
-class Recipient (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "recipient"
-
-
-
- at registerElement
-class RequestStatus (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "request-status"
-
-
-
- at registerElement
-class CalendarData (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "calendar-data"
-
-
-    @classmethod
-    def fromCalendar(clazz, calendar):
-        if isinstance(calendar, str):
-            if not calendar:
-                raise ValueError("Missing calendar data")
-            return clazz(PCDATAElement(calendar))
-        elif isinstance(calendar, iComponent):
-            assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
-            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
-        else:
-            raise ValueError("Not a calendar: %s" % (calendar,))
-
-    fromTextData = fromCalendar
-
-
-
- at registerElement
-class Error (WebDAVElement):
-    namespace = ischedule_namespace
-    name = "error"
-
-    allowed_children = {WebDAVElement: (0, None)}
-
-
-
- at registerElement
-class ResponseDescription (WebDAVTextElement):
-    namespace = ischedule_namespace
-    name = "response-description"

Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,281 @@
+##
+# Copyright (c) 2011-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+This module provides XML definitions for use with Timezone Standard Service.
+"""
+
+from twistedcaldav.config import config
+from twistedcaldav.ical import Component as iComponent
+from txdav.xml.element import PCDATAElement, WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
+from txdav.xml.element import registerElement
+
+
+##
+# iSchedule XML Definitions
+##
+
+ischedule_namespace = "urn:ietf:params:xml:ns:ischedule"
+
+
+ at registerElement
+class QueryResult (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "query-result"
+
+    allowed_children = {
+        (ischedule_namespace, "capability-set"): (0, None),
+    }
+
+
+
+ at registerElement
+class Capabilities (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "capabilities"
+
+    allowed_children = {
+        (ischedule_namespace, "versions"): (1, 1),
+        (ischedule_namespace, "scheduling-messages"): (1, 1),
+        (ischedule_namespace, "calendar-data-types"): (1, 1),
+        (ischedule_namespace, "attachments"): (1, 1),
+        (ischedule_namespace, "supported-recipient-uri-scheme-set"): (1, 1),
+        (ischedule_namespace, "max-content-length"): (1, 1),
+        (ischedule_namespace, "min-date-time"): (1, 1),
+        (ischedule_namespace, "max-date-time"): (1, 1),
+        (ischedule_namespace, "max-instances"): (1, 1),
+        (ischedule_namespace, "max-recipients"): (1, 1),
+        (ischedule_namespace, "administrator"): (1, 1),
+    }
+
+
+
+ at registerElement
+class Versions (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "versions"
+
+    allowed_children = {
+        (ischedule_namespace, "version"): (1, None),
+    }
+
+
+
+ at registerElement
+class Version (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "version"
+
+
+
+ at registerElement
+class SchedulingMessages (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "scheduling-messages"
+
+    allowed_children = {
+        (ischedule_namespace, "component"): (1, None),
+    }
+
+
+
+ at registerElement
+class Component (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "component"
+
+    allowed_children = {
+        (ischedule_namespace, "method"): (0, None),
+    }
+    allowed_attributes = {"name": True}
+
+
+
+ at registerElement
+class Method (WebDAVEmptyElement):
+    namespace = ischedule_namespace
+    name = "method"
+
+    allowed_attributes = {"name": True}
+
+
+
+ at registerElement
+class CalendarDataTypes (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "calendar-data-types"
+
+    allowed_children = {
+        (ischedule_namespace, "calendar-data-type"): (1, None),
+    }
+
+
+
+ at registerElement
+class CalendarDataType (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "calendar-data-type"
+
+    allowed_attributes = {
+        "content-type": True,
+        "version": True,
+    }
+
+
+
+ at registerElement
+class Attachments (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "attachments"
+
+    allowed_children = {
+        (ischedule_namespace, "inline"): (0, 1),
+        (ischedule_namespace, "external"): (0, 1),
+    }
+
+
+
+ at registerElement
+class Inline (WebDAVEmptyElement):
+    namespace = ischedule_namespace
+    name = "inline"
+
+
+
+ at registerElement
+class External (WebDAVEmptyElement):
+    namespace = ischedule_namespace
+    name = "external"
+
+
+
+ at registerElement
+class MaxContentLength (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "max-content-length"
+
+
+
+ at registerElement
+class MinDateTime (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "min-date-time"
+
+
+
+ at registerElement
+class MaxDateTime (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "max-date-time"
+
+
+
+ at registerElement
+class MaxInstances (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "max-instances"
+
+
+
+ at registerElement
+class MaxRecipients (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "max-recipients"
+
+
+
+ at registerElement
+class Administrator (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "administrator"
+
+
+
+ at registerElement
+class ScheduleResponse (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "schedule-response"
+
+    allowed_children = {
+        (ischedule_namespace, "response"): (0, None),
+    }
+
+
+
+ at registerElement
+class Response (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "response"
+
+    allowed_children = {
+        (ischedule_namespace, "recipient"): (1, 1),
+        (ischedule_namespace, "request-status"): (1, 1),
+        (ischedule_namespace, "calendar-data"): (0, 1),
+        (ischedule_namespace, "error"): (0, 1),
+        (ischedule_namespace, "response-description"): (0, 1),
+    }
+
+
+
+ at registerElement
+class Recipient (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "recipient"
+
+
+
+ at registerElement
+class RequestStatus (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "request-status"
+
+
+
+ at registerElement
+class CalendarData (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "calendar-data"
+
+
+    @classmethod
+    def fromCalendar(clazz, calendar):
+        if isinstance(calendar, str):
+            if not calendar:
+                raise ValueError("Missing calendar data")
+            return clazz(PCDATAElement(calendar))
+        elif isinstance(calendar, iComponent):
+            assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
+            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
+        else:
+            raise ValueError("Not a calendar: %s" % (calendar,))
+
+    fromTextData = fromCalendar
+
+
+
+ at registerElement
+class Error (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "error"
+
+    allowed_children = {WebDAVElement: (0, None)}
+
+
+
+ at registerElement
+class ResponseDescription (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "response-description"

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

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,195 +0,0 @@
-##
-# Copyright (c) 2006-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.python.filepath import CachingFilePath as FilePath
-
-from twext.python.log import Logger
-
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.xmlutil import readXML
-
-"""
-XML based iSchedule configuration file handling.
-"""
-
-__all__ = [
-    "IScheduleServers",
-]
-
-log = Logger()
-
-class IScheduleServers(object):
-    
-    _fileInfo = None
-    _xmlFile = None
-    _servers = None
-    _domainMap = None
-    
-    def __init__(self):
-        
-        self._loadConfig()
-
-    def _loadConfig(self):
-        if IScheduleServers._servers is None:
-            IScheduleServers._xmlFile = FilePath(
-                fullServerPath(
-                    config.ConfigRoot,
-                    config.Scheduling[DeliveryService.serviceType_ischedule]["Servers"]
-                )
-            )
-        IScheduleServers._xmlFile.restat()
-        fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
-        if fileInfo != IScheduleServers._fileInfo:
-            parser = IScheduleServersParser(IScheduleServers._xmlFile)
-            IScheduleServers._servers = parser.servers
-            self._mapDomains()
-            IScheduleServers._fileInfo = fileInfo
-        
-    def _mapDomains(self):
-        IScheduleServers._domainMap = {}
-        for server in IScheduleServers._servers:
-            for domain in server.domains:
-                IScheduleServers._domainMap[domain] = server
-    
-    def mapDomain(self, domain):
-        """
-        Map a calendar user address domain to a suitable server that can
-        handle server-to-server requests for that user.
-        """
-        return IScheduleServers._domainMap.get(domain)
-
-ELEMENT_SERVERS                 = "servers"
-ELEMENT_SERVER                  = "server"
-ELEMENT_URI                     = "uri"
-ELEMENT_AUTHENTICATION          = "authentication"
-ATTRIBUTE_TYPE                  = "type"
-ATTRIBUTE_BASICAUTH             = "basic"
-ELEMENT_USER                    = "user"
-ELEMENT_PASSWORD                = "password"
-ELEMENT_ALLOW_REQUESTS_FROM     = "allow-requests-from"
-ELEMENT_ALLOW_REQUESTS_TO       = "allow-requests-to"
-ELEMENT_DOMAINS                 = "domains"
-ELEMENT_DOMAIN                  = "domain"
-ELEMENT_CLIENT_HOSTS            = "hosts"
-ELEMENT_HOST                    = "host"
-
-class IScheduleServersParser(object):
-    """
-    Server-to-server configuration file parser.
-    """
-    def __repr__(self):
-        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
-
-    def __init__(self, xmlFile):
-
-        self.servers = []
-        
-        # Read in XML
-        try:
-            _ignore_tree, servers_node = readXML(xmlFile.path, ELEMENT_SERVERS)
-        except ValueError:
-            log.error("Ignoring file %r because it is not a server-to-server config file" % (xmlFile,))
-
-        self._parseXML(servers_node)
-        
-    def _parseXML(self, node):
-        """
-        Parse the XML root node from the server-to-server configuration document.
-        @param node: the L{Node} to parse.
-        """
-        for child in node.getchildren():
-            if child.tag == ELEMENT_SERVER:
-                self.servers.append(IScheduleServerRecord())
-                self.servers[-1].parseXML(child)
-                
-class IScheduleServerRecord (object):
-    """
-    Contains server-to-server details.
-    """
-    def __init__(self, uri=None):
-        """
-        @param recordType: record type for directory entry.
-        """
-        self.uri = ""
-        self.authentication = None
-        self.allow_from = False
-        self.allow_to = True
-        self.domains = []
-        self.client_hosts = []
-        self.unNormalizeAddresses = True
-        self.moreHeaders = []
-        
-        if uri:
-            self.uri = uri
-            self._parseDetails()
-
-    def parseXML(self, node):
-        for child in node.getchildren():
-            if child.tag == ELEMENT_URI:
-                self.uri = child.text
-            elif child.tag == ELEMENT_AUTHENTICATION:
-                self._parseAuthentication(child)
-            elif child.tag == ELEMENT_ALLOW_REQUESTS_FROM:
-                self.allow_from = True
-            elif child.tag == ELEMENT_ALLOW_REQUESTS_TO:
-                self.allow_to = True
-            elif child.tag == ELEMENT_DOMAINS:
-                self._parseList(child, ELEMENT_DOMAIN, self.domains)
-            elif child.tag == ELEMENT_CLIENT_HOSTS:
-                self._parseList(child, ELEMENT_HOST, self.client_hosts)
-            else:
-                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child.tag,))
-        
-        self._parseDetails()
-
-    def _parseList(self, node, element_name, appendto):
-        for child in node.getchildren():
-            if child.tag == element_name:
-                appendto.append(child.text)
-
-    def _parseAuthentication(self, node):
-        atype = node.getAttribute(ATTRIBUTE_TYPE, "")
-        if atype != ATTRIBUTE_BASICAUTH:
-            return
-
-        for child in node.getchildren():
-            if child.tag == ELEMENT_USER:
-                user = child.text
-            elif child.tag == ELEMENT_PASSWORD:
-                password = child.text
-        
-        self.authentication = ("basic", user, password,)
-
-    def _parseDetails(self):
-        # Extract scheme, host, port and path
-        if self.uri.startswith("http://"):
-            self.ssl = False
-            rest = self.uri[7:]
-        elif self.uri.startswith("https://"):
-            self.ssl = True
-            rest = self.uri[8:]
-        
-        splits = rest.split("/", 1)
-        hostport = splits[0].split(":")
-        self.host = hostport[0]
-        if len(hostport) > 1:
-            self.port = int(hostport[1])
-        else:
-            self.port = {False:80, True:443}[self.ssl]
-        self.path = "/"
-        if len(splits) > 1:
-            self.path += splits[1]

Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -37,8 +37,6 @@
 
 log = Logger()
 
-__version__ = "0.0"
-
 __all__ = [
     "iTipProcessing",
     "iTipGenerator",

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

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

Deleted: CalendarServer/trunk/twistedcaldav/scheduling/test/test_caldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_caldav.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_caldav.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,33 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import twistedcaldav.test.util
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
-from twistedcaldav.config import config
-
-class CalDAV (twistedcaldav.test.util.TestCase):
-    """
-    twistedcaldav.scheduling.caldav tests
-    """
-
-    def test_matchCalendarUserAddress(self):
-        """
-        Make sure we do an exact comparison on EmailDomain
-        """
-        config.Scheduling[ScheduleViaCalDAV.serviceType()]["EmailDomain"] = "example.com"
-        self.assertTrue(ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at example.com"))
-        self.assertFalse(ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at foo.example.com"))
-        self.assertFalse(ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at xyzexample.com"))

Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -27,7 +27,7 @@
     """
 
     def test_organizer_diff(self):
-        
+
         data = (
             (
                 "#1.1 Simple component, no change",
@@ -477,7 +477,6 @@
 
     def test_attendee_merge_simple(self):
 
-        
         data = (
             (
                 "#1.1 Simple component, no change",
@@ -1185,8 +1184,9 @@
             )
             self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
     def test_attendee_merge_complex(self):
-        
+
         data = (
             (
                 "#1.1 Complex component, no change",
@@ -1684,7 +1684,7 @@
 
 
     def test_attendee_merge_exdate(self):
-        
+
         data = (
             (
                 "#1.1 Single component, one EXDATE",
@@ -2027,8 +2027,9 @@
             )
             self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
     def test_attendee_merge_cancelled(self):
-        
+
         data = (
             (
                 "#1.1 Remove EXDATE add CANCELLED",
@@ -2391,8 +2392,9 @@
             )
             self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
     def test_attendee_merge_dropbox(self):
-        
+
         data = (
             (
                 "#1.1 Remove dropbox",
@@ -2670,9 +2672,9 @@
             )
             self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
     def test_attendee_merge_fake_master(self):
 
-        
         data = (
             (
                 "#1.1 Single overridden component, partstat change - ok",
@@ -2857,7 +2859,7 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True, ('20080601T120000Z','20080602T120000Z',), """BEGIN:VCALENDAR
+                (True, True, ('20080601T120000Z', '20080602T120000Z',), """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
@@ -2929,7 +2931,7 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True, ('20080601T120000Z','20080602T120000Z',), """BEGIN:VCALENDAR
+                (True, True, ('20080601T120000Z', '20080602T120000Z',), """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
@@ -3220,8 +3222,9 @@
             )
             self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
     def test_what_is_different(self):
-        
+
         data1 = (
             (
                 "#1.1 Simple component, no change",
@@ -3279,7 +3282,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(),}},
+                {"": {"SUMMARY": set(), }},
             ),
             (
                 "#1.3 Simple component, one property change, one addition, one removal",
@@ -3307,7 +3310,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(), "LOCATION":set(), "DESCRIPTION":set(),}},
+                {"": {"SUMMARY": set(), "LOCATION": set(), "DESCRIPTION": set(), }},
             ),
             (
                 "#1.4 Simple component, add attendee",
@@ -3340,7 +3343,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#1.5 Simple component, remove attendee",
@@ -3371,7 +3374,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#1.6 Simple component, attendee PARTSTAT only",
@@ -3403,7 +3406,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#1.7 Simple component, attendee PARTSTAT and addition",
@@ -3436,7 +3439,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#1.8 Simple component, attendee RSVP only",
@@ -3468,7 +3471,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#1.9 Simple component, DTSTART/DTEND VALUE",
@@ -3500,7 +3503,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"DTEND":set(("VALUE",)), "DTSTART":set(("VALUE",)),}},
+                {"": {"DTEND": set(("VALUE",)), "DTSTART": set(("VALUE",)), }},
             ),
             (
                 "#1.10 Simple component, DTSTART/DTEND TZID",
@@ -3568,10 +3571,10 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"DTEND":set(("TZID",)), "DTSTART":set(("TZID",)),}},
+                {"": {"DTEND": set(("TZID",)), "DTSTART": set(("TZID",)), }},
             ),
         )
-        
+
         data2 = (
             (
                 "#2.1 Simple recurring component, no change",
@@ -3633,7 +3636,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(),}},
+                {"": {"SUMMARY": set(), }},
             ),
             (
                 "#2.3 Simple component, one property change, one addition, one removal",
@@ -3663,7 +3666,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(), "LOCATION":set(), "DESCRIPTION":set(),}},
+                {"": {"SUMMARY": set(), "LOCATION": set(), "DESCRIPTION": set(), }},
             ),
             (
                 "#2.4 Simple component, add attendee",
@@ -3698,7 +3701,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#2.5 Simple component, remove attendee",
@@ -3731,7 +3734,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#2.6 Simple component, attendee PARTSTAT only",
@@ -3765,7 +3768,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#2.7 Simple component, attendee PARTSTAT and addition",
@@ -3800,7 +3803,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"ATTENDEE":set(),}},
+                {"": {"ATTENDEE": set(), }},
             ),
             (
                 "#2.8 Simple recurring component, property order change",
@@ -3841,7 +3844,7 @@
                 {},
             ),
         )
-        
+
         data3 = (
             (
                 "#3.1 Complex recurring component, no change",
@@ -3943,7 +3946,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"20080602T120000Z":{"SUMMARY":set(),}},
+                {"20080602T120000Z": {"SUMMARY": set(), }},
             ),
             (
                 "#3.3 Simple component, one property change in master",
@@ -3991,7 +3994,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(),}},
+                {"": {"SUMMARY": set(), }},
             ),
             (
                 "#3.4 Simple component, one property change in master and instance",
@@ -4039,7 +4042,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set(),}, "20080602T120000Z":{"SUMMARY":set(),}},
+                {"": {"SUMMARY": set(), }, "20080602T120000Z": {"SUMMARY": set(), }},
             ),
             (
                 "#3.5 Simple component, different property change in master and instance",
@@ -4088,7 +4091,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"":{"SUMMARY":set()}, "20080602T120000Z":{"Description":set()}},
+                {"": {"SUMMARY": set()}, "20080602T120000Z": {"Description": set()}},
             ),
             (
                 "#3.6 Simple component, instance added no change",
@@ -4158,7 +4161,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"20080602T120000Z":{"DTSTART":set(), "DTEND":set(), }},
+                {"20080602T120000Z": {"DTSTART": set(), "DTEND": set(), }},
             ),
             (
                 "#3.8 Simple component, instance removed no change",
@@ -4228,17 +4231,18 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                {"20080602T120000Z":{"DTSTART":set(), "DTEND":set(), }},
+                {"20080602T120000Z": {"DTSTART": set(), "DTEND": set(), }},
             ),
         )
-        
+
         for description, calendar1, calendar2, rids in itertools.chain(data1, data2, data3,):
             differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
             got_rids = differ.whatIsDifferent()
             self.assertEqual(got_rids, rids, msg="%s expected R-IDs: '%s', got: '%s'" % (description, rids, got_rids,))
-        
+
+
     def test_organizer_smart_merge(self):
-        
+
         data1 = (
             (
                 "#1.1 Simple component, no change",
@@ -4481,7 +4485,7 @@
 """,
             ),
         )
-        
+
         data2 = (
             (
                 "#2.1 Simple recurring component, no change",
@@ -5036,7 +5040,7 @@
 """,
             ),
         )
-        
+
         for description, calendar1, calendar2, changed_calendar in itertools.chain(data1, data2,):
             cal1 = Component.fromString(calendar1)
             cal2 = Component.fromString(calendar2)

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

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

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

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

Modified: CalendarServer/trunk/twistedcaldav/scheduling/utils.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/utils.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/utils.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -43,7 +43,7 @@
 
         # Get matching newstore objects
         objectResources = (yield calendar_home.getCalendarResourcesForUID(uid, allow_shared))
-        
+
         # We really want only one or zero of these
         if len(objectResources) == 1:
             result["calendar_collection_uri"] = joinURL(calendar_home.url(), objectResources[0]._parentCollection.name())
@@ -54,4 +54,3 @@
             log.debug("Should only have zero or one scheduling object resource with UID '%s' in calendar home: %s" % (uid, calendar_home,))
 
     returnValue((result["resource"], result["resource_name"], result["calendar_collection"], result["calendar_collection_uri"],))
-

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

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

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

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

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

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

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

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

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

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

Modified: CalendarServer/trunk/txdav/base/datastore/subpostgres.py
===================================================================
--- CalendarServer/trunk/txdav/base/datastore/subpostgres.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/txdav/base/datastore/subpostgres.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,5 +1,5 @@
 # -*- test-case-name: txdav.base.datastore.test.test_subpostgres -*-
-##
+# #
 # Copyright (c) 2010-2012 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-##
+# #
 
 """
 Run and manage PostgreSQL as a subprocess.
@@ -69,8 +69,9 @@
             if _MAGIC_READY_COOKIE in line:
                 self.svc.ready()
 
+    disconnecting = False
 
-    disconnecting = False
+
     def connectionMade(self):
         self.lineReceiver.makeConnection(self)
 
@@ -190,25 +191,32 @@
         # completed, our stopService( ) examines the delayedShutdown flag.
         # If True, we wait on the shutdownDeferred to fire before proceeding.
         # The deferred gets fired once database init is complete.
-        self.delayedShutdown = False # set to True when in critical code
-        self.shutdownDeferred = None # the actual deferred
+        self.delayedShutdown = False  # set to True when in critical code
+        self.shutdownDeferred = None  # the actual deferred
 
         # Options from config
         self.databaseName = databaseName
         self.logFile = logFile
-        if socketDir:
-            # Unix socket length path limit
-            self.socketDir = CachingFilePath("%s/ccs_postgres_%s/" %
-                (socketDir, md5(dataStoreDirectory.path).hexdigest()))
-            if len(self.socketDir.path) > 64:
-                socketDir = "/tmp"
-                self.socketDir = CachingFilePath("/tmp/ccs_postgres_%s/" %
-                    (md5(dataStoreDirectory.path).hexdigest()))
-            self.host = self.socketDir.path
+        if listenAddresses:
+            self.socketDir = None
+            self.host, self.port = listenAddresses[0].split(":") if ":" in listenAddresses[0] else (listenAddresses[0], None,)
+            self.listenAddresses = [addr.split(":")[0] for addr in listenAddresses]
         else:
-            self.socketDir = None
-            self.host = "localhost"
-        self.listenAddresses = listenAddresses
+            if socketDir:
+                # Unix socket length path limit
+                self.socketDir = CachingFilePath("%s/ccs_postgres_%s/" %
+                    (socketDir, md5(dataStoreDirectory.path).hexdigest()))
+                if len(self.socketDir.path) > 64:
+                    socketDir = "/tmp"
+                    self.socketDir = CachingFilePath("/tmp/ccs_postgres_%s/" %
+                        (md5(dataStoreDirectory.path).hexdigest()))
+                self.host = self.socketDir.path
+                self.port = None
+            else:
+                self.socketDir = None
+                self.host = "localhost"
+                self.port = None
+            self.listenAddresses = []
         self.sharedBuffers = sharedBuffers if not testMode else 16
         self.maxConnections = maxConnections if not testMode else 4
         self.options = options
@@ -251,9 +259,14 @@
                 pwd.getpwuid(self.uid).pw_name)
         else:
             dsn = "%s:dbname=%s" % (self.host, databaseName)
-        return DBAPIConnector(pgdb, postgresPreflight, dsn)
 
+        kwargs = {}
+        if self.port:
+            kwargs["host"] = "%s:%s" % (self.host, self.port,)
 
+        return DBAPIConnector(pgdb, postgresPreflight, dsn, **kwargs)
+
+
     def produceConnection(self, label="<unlabeled>", databaseName=None):
         """
         Produce a DB-API 2.0 connection pointed at this database.
@@ -314,17 +327,19 @@
 #        for pipe in self.monitor.transport.pipes.values():
 #            pipe.stopReading()
 #            pipe.stopWriting()
+        pass
 
 
     def unpauseMonitor(self):
         """
         Unpause monitoring.
 
-        @see: L{pauseMonitor} 
+        @see: L{pauseMonitor}
         """
 #        for pipe in self.monitor.transport.pipes.values():
 #            pipe.startReading()
 #            pipe.startWriting()
+        pass
 
 
     def startDatabase(self):
@@ -341,6 +356,8 @@
         )
         if self.socketDir:
             options.append("-k '%s'" % (self.socketDir.path,))
+        if self.port:
+            options.append("-c port=%s" % (self.port,))
         options.append("-c shared_buffers=%d" % (self.sharedBuffers,))
         options.append("-c max_connections=%d" % (self.maxConnections,))
         options.append("-c standard_conforming_strings=on")

Modified: CalendarServer/trunk/txdav/caldav/datastore/util.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/util.py	2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/txdav/caldav/datastore/util.py	2012-10-25 15:20:28 UTC (rev 9984)
@@ -224,7 +224,7 @@
                 if exists is None:
                     newattachment = yield outObject.createAttachmentWithName(name)
                     transport = newattachment.store(ctype)
-                    proto =_AttachmentMigrationProto(transport)
+                    proto = _AttachmentMigrationProto(transport)
                     attachment.retrieve(proto)
                     yield proto.done
 
@@ -267,18 +267,18 @@
     # usual suspects.
     contentTypes.update(
         {
-            '.conf':  'text/plain',
-            '.diff':  'text/plain',
-            '.exe':   'application/x-executable',
-            '.flac':  'audio/x-flac',
-            '.java':  'text/plain',
-            '.ogg':   'application/ogg',
-            '.oz':    'text/x-oz',
-            '.swf':   'application/x-shockwave-flash',
-            '.tgz':   'application/x-gtar',
-            '.wml':   'text/vnd.wap.wml',
-            '.xul':   'application/vnd.mozilla.xul+xml',
-            '.py':    'text/plain',
+            '.conf': 'text/plain',
+            '.diff': 'text/plain',
+            '.exe': 'application/x-executable',
+            '.flac': 'audio/x-flac',
+            '.java': 'text/plain',
+            '.ogg': 'application/ogg',
+            '.oz': 'text/x-oz',
+            '.swf': 'application/x-shockwave-flash',
+            '.tgz': 'application/x-gtar',
+            '.wml': 'text/vnd.wap.wml',
+            '.xul': 'application/vnd.mozilla.xul+xml',
+            '.py': 'text/plain',
             '.patch': 'text/plain',
         }
     )
@@ -304,12 +304,15 @@
         self.storeTransport = storeTransport
         self.done = Deferred()
 
+
     def connectionMade(self):
         self.storeTransport.registerProducer(self.transport, False)
 
+
     def dataReceived(self, data):
         self.storeTransport.write(data)
 
+
     @inlineCallbacks
     def connectionLost(self, reason):
         try:
@@ -566,14 +569,12 @@
     def getHost(self):
         return self
 
-
     _everResumedProducing = False
 
     def resumeProducing(self):
         self._everResumedProducing = True
         super(AttachmentRetrievalTransport, self).resumeProducing()
 
-
     _deliveryLoop = None
 
     def _maybeLoopDelivery(self):
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121025/4d597db6/attachment-0001.html>


More information about the calendarserver-changes mailing list