[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):
-        """