[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("&", "&").replace("<", "<")
-
- 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("&", "&").replace("<", "<")
+
+ self.assertIn(expectedPlain, plainPart)
+ self.assertIn(expectedHTML, htmlPart)
+
+
+ def test_stringFormatTemplateLoader(self):
+ """
+ L{StringFormatTemplateLoader.load} will convert a template with
+ C{%(x)s}-format slots by converting it to a template with C{<t:slot
+ name="x" />} slots, and a renderer on the document element named
+ according to the constructor argument.
+ """
+ class StubElement(Element):
+ loader = StringFormatTemplateLoader(
+ lambda : StringIO(
+ "<test><alpha>%(slot1)s</alpha>%(other)s</test>"
+ ),
+ "testRenderHere"
+ )
+
+ @renderer
+ def testRenderHere(self, request, tag):
+ return tag.fillSlots(slot1="hello",
+ other="world")
+ result = []
+ flattenString(None, StubElement()).addCallback(result.append)
+ self.assertEquals(result,
+ ["<test><alpha>hello</alpha>world</test>"])
+
+
+ def test_templateLoaderWithAttributes(self):
+ """
+ L{StringFormatTemplateLoader.load} will convert a template with
+ C{%(x)s}-format slots inside attributes into t:attr elements containing
+ t:slot slots.
+ """
+ class StubElement(Element):
+ loader = StringFormatTemplateLoader(
+ lambda : StringIO(
+ '<test><alpha beta="before %(slot1)s after">inner</alpha>'
+ '%(other)s</test>'
+ ),
+ "testRenderHere"
+ )
+
+ @renderer
+ def testRenderHere(self, request, tag):
+ return tag.fillSlots(slot1="hello",
+ other="world")
+ result = []
+ flattenString(None, StubElement()).addCallback(result.append)
+ self.assertEquals(result,
+ ['<test><alpha beta="before hello after">'
+ 'inner</alpha>world</test>'])
+
+
+ def test_templateLoaderTagSoup(self):
+ """
+ L{StringFormatTemplateLoader.load} will convert a template with
+ C{%(x)s}-format slots into t:slot slots, and render a well-formed output
+ document, even if the input is malformed (i.e. missing necessary closing
+ tags).
+ """
+ class StubElement(Element):
+ loader = StringFormatTemplateLoader(
+ lambda : StringIO(
+ '<test><alpha beta="before %(slot1)s after">inner</alpha>'
+ '%(other)s'
+ ),
+ "testRenderHere"
+ )
+
+ @renderer
+ def testRenderHere(self, request, tag):
+ return tag.fillSlots(slot1="hello",
+ other="world")
+ result = []
+ flattenString(None, StubElement()).addCallback(result.append)
+ self.assertEquals(result,
+ ['<test><alpha beta="before hello after">'
+ 'inner</alpha>world</test>'])
+
+
+
+def partByType(message, contentType):
+ """
+ Retrieve a MIME part from an L{email.message.Message} based on a content
+ type.
+ """
+ for part in message.walk():
+ if part.get_content_type() == contentType:
+ return part
+ raise KeyError(contentType)
+
+
+
+class MailGatewayTokensDatabaseTests(TestCase):
+
+ def setUp(self):
+ TestCase.setUp(self)
+ self.db = MailGatewayTokensDatabase(":memory:")
+
+
+ def test_tokens(self):
+ self.assertEquals(self.db.lookupByToken("xyzzy"), None)
+
+ token = self.db.createToken("organizer", "attendee", "icaluid")
+ self.assertEquals(self.db.getToken("organizer", "attendee", "icaluid"),
+ token)
+ self.assertEquals(self.db.lookupByToken(token),
+ ("organizer", "attendee", "icaluid"))
+ self.db.deleteToken(token)
+ self.assertEquals(self.db.lookupByToken(token), None)
+
+
+serverData = """<?xml version="1.0" encoding="utf-8"?>
+<servers>
+ <server>
+ <id>00001</id>
+ <uri>http://caldav1.example.com:8008</uri>
+ <allowed-from>127.0.0.1</allowed-from>
+ <shared-secret>foobar</shared-secret>
+ </server>
+ <server>
+ <id>00002</id>
+ <uri>https://caldav2.example.com:8843</uri>
+ <partitions>
+ <partition>
+ <id>A</id>
+ <uri>https://machine1.example.com:8443</uri>
+ </partition>
+ <partition>
+ <id>B</id>
+ <uri>https://machine2.example.com:8443</uri>
+ </partition>
+ </partitions>
+ </server>
+</servers>
+"""
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/imip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,150 +0,0 @@
-# -*- test-case-name: twistedcaldav.scheduling.test.test_imip -*-
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Handles the sending of scheduling messages via iMIP (mail gateway).
-"""
-
-from twisted.python.failure import Failure
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-from twext.python.log import Logger
-from twext.web2.dav.http import ErrorResponse
-
-from twext.web2 import responsecode
-from twext.web2.http import HTTPError
-from twisted.web import client
-
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.util import AuthorizedHTTPGetter
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.internet.adaptendpoint import connect
-
-
-__all__ = [
- "ScheduleViaIMip",
-]
-
-log = Logger()
-
-
-class ScheduleViaIMip(DeliveryService):
-
- @classmethod
- def serviceType(cls):
- return DeliveryService.serviceType_imip
-
- @inlineCallbacks
- def generateSchedulingResponses(self):
- def failForRecipient(recipient):
- err = HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "recipient-failed"),
- "iMIP request failed",
- ))
- self.responses.add(
- recipient.cuaddr,
- Failure(exc_value=err),
- reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE,
- suppressErrorLog=True
- )
-
- # Generate an HTTP client request
- try:
- # We do not do freebusy requests via iMIP
- if self.freebusy:
- raise ValueError("iMIP VFREEBUSY requests not supported.")
-
- method = self.scheduler.calendar.propertyValue("METHOD")
- if method not in (
- "PUBLISH",
- "REQUEST",
- "REPLY",
- "ADD",
- "CANCEL",
- "DECLINE_COUNTER",
- ):
- log.info("Could not do server-to-imip method: %s" % (method,))
- for recipient in self.recipients:
- err = HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "recipient-failed"),
- "iMIP method not allowed: %s" % (method,),
- ))
- self.responses.add(
- recipient.cuaddr,
- Failure(exc_value=err),
- reqstatus=iTIPRequestStatus.NO_USER_SUPPORT
- )
- returnValue(None)
-
- caldata = str(self.scheduler.calendar)
-
- for recipient in self.recipients:
- try:
- toAddr = str(recipient.cuaddr)
- if not toAddr.lower().startswith("mailto:"):
- raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP operation." % (toAddr,))
-
- fromAddr = str(self.scheduler.originator.cuaddr)
-
- log.debug("POSTing iMIP message to gateway... To: '%s', From :'%s'\n%s" % (toAddr, fromAddr, caldata,))
- yield self.postToGateway(fromAddr, toAddr, caldata)
-
- except Exception, e:
- # Generated failed response for this recipient
- log.debug("iMIP request %s failed for recipient %s: %s" % (self, recipient, e))
- failForRecipient(recipient)
-
- else:
- self.responses.add(
- recipient.cuaddr,
- responsecode.OK,
- reqstatus=iTIPRequestStatus.MESSAGE_SENT
- )
-
- except Exception, e:
- # Generated failed responses for each recipient
- log.debug("iMIP request %s failed: %s" % (self, e))
- for recipient in self.recipients:
- failForRecipient(recipient)
-
- def postToGateway(self, fromAddr, toAddr, caldata, reactor=None):
- if reactor is None:
- from twisted.internet import reactor
-
- mailGatewayServer = config.Scheduling['iMIP']['MailGatewayServer']
- mailGatewayPort = config.Scheduling['iMIP']['MailGatewayPort']
- url = "http://%s:%d/inbox" % (mailGatewayServer, mailGatewayPort)
- headers = {
- 'Content-Type' : 'text/calendar',
- 'Originator' : fromAddr,
- 'Recipient' : toAddr,
- config.Scheduling.iMIP.Header : config.Scheduling.iMIP.Password,
- }
- factory = client.HTTPClientFactory(url, method='POST', headers=headers,
- postdata=caldata, agent="CalDAV server")
-
- factory.noisy = False
- factory.protocol = AuthorizedHTTPGetter
- connect(GAIEndpoint(reactor, mailGatewayServer, mailGatewayPort),
- factory)
- return factory.deferred
-
Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -31,7 +31,7 @@
normalizeCUAddr
from twistedcaldav.scheduling.icaldiff import iCalDiff
from twistedcaldav.scheduling.itip import iTipGenerator, iTIPRequestStatus
-from twistedcaldav.scheduling.scheduler import CalDAVScheduler
+from twistedcaldav.scheduling.caldav.scheduler import CalDAVScheduler
from twistedcaldav.scheduling.utils import getCalendarObjectForPrincipals
from twistedcaldav.config import config
@@ -1149,7 +1149,7 @@
if config.Scheduling.iMIP.Enabled and self.organizerAddress.cuaddr.lower().startswith("mailto:"):
return is_server
- if not local_organizer and is_server:
+ if not config.Scheduling.iSchedule.Enabled and not local_organizer and is_server:
# Coerce ORGANIZER to SCHEDULE-AGENT=NONE
log.debug("Attendee '%s' is not allowed to use SCHEDULE-AGENT=SERVER on organizer: UID:%s" % (self.attendeePrincipal, self.uid,))
self.calendar.setParameterToValueForPropertyWithValue("SCHEDULE-AGENT", "NONE", "ORGANIZER", None)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/__init__.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/__init__.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,26 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-
-"""
-iSchedule scheduling.
-"""
-
-from twext.web2 import http_headers
-
-# These new HTTP headers should appear with case-preserved
-hdrs = ("iSchedule-Version", "iSchedule-Message-ID", "DKIM-Signature",)
-http_headers.casemappingify(dict([(i, i) for i in hdrs]))
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/__init__.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/__init__.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/__init__.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,26 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+"""
+iSchedule scheduling.
+"""
+
+from twext.web2 import http_headers
+
+# These new HTTP headers should appear with case-preserved
+hdrs = ("iSchedule-Version", "iSchedule-Message-ID", "DKIM-Signature",)
+http_headers.casemappingify(dict([(i, i) for i in hdrs]))
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/delivery.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/delivery.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,498 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from StringIO import StringIO
-
-from twisted.internet.defer import inlineCallbacks, DeferredList, returnValue
-from twisted.internet.protocol import Factory
-
-from twisted.python.failure import Failure
-
-from twext.web2 import responsecode
-from twext.web2.client.http import ClientRequest
-from twext.web2.client.http import HTTPClientProtocol
-from twext.web2.dav.util import davXMLFromStream, joinURL, allDataFromStream
-from twext.web2.http import HTTPError
-from twext.web2.http_headers import Headers
-from twext.web2.http_headers import MimeType
-from twext.web2.stream import MemoryStream
-
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.python.log import Logger
-from twext.web2.dav.http import ErrorResponse
-
-from twistedcaldav.accounting import accountingEnabledForCategory, emitAccounting
-from twistedcaldav.client.pool import _configuredClientContextFactory
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser, RemoteCalendarUser, \
- OtherServerCalendarUser
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.ischedule.dkim import DKIMRequest, DKIMUtils
-from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServerRecord
-from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServers
-from twistedcaldav.scheduling.ischedule.utils import lookupServerViaSRV
-from twistedcaldav.scheduling.ischedule.xml import ScheduleResponse, Response, \
- RequestStatus, Recipient, ischedule_namespace, CalendarData, \
- ResponseDescription, Error
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.util import utf8String, normalizationLookup
-from urlparse import urlsplit
-from twistedcaldav.ical import normalizeCUAddress
-
-"""
-Handles the sending of iSchedule scheduling messages. Used for both cross-domain scheduling,
-as well as internal partitioning or podding.
-"""
-
-__all__ = [
- "ScheduleViaISchedule",
-]
-
-log = Logger()
-
-
-
-class ScheduleViaISchedule(DeliveryService):
-
- domainServerMap = {}
-
- @classmethod
- def serviceType(cls):
- return DeliveryService.serviceType_ischedule
-
-
- @classmethod
- @inlineCallbacks
- def matchCalendarUserAddress(cls, cuaddr):
-
- # TODO: here is where we would attempt service discovery based on the cuaddr.
-
- # Only handle mailtos:
- if cuaddr.lower().startswith("mailto:"):
- _ignore_local, domain = cuaddr[7:].split("@", 1)
- server = (yield cls.serverForDomain(domain))
- returnValue(server is not None)
-
- # Do default match
- result = (yield super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr))
- returnValue(result)
-
-
- @classmethod
- @inlineCallbacks
- def serverForDomain(cls, domain):
- if domain not in cls.domainServerMap:
-
- # First check built-in list of remote servers
- servermgr = IScheduleServers()
- server = servermgr.mapDomain(domain)
- if server is not None:
- cls.domainServerMap[domain] = server
- else:
- # Lookup domain
- result = (yield lookupServerViaSRV(domain))
- if result is None:
- # Lookup domain
- result = (yield lookupServerViaSRV(domain, service="_ischedule"))
- if result is None:
- cls.domainServerMap[domain] = None
- else:
- # Create the iSchedule server record for this server
- cls.domainServerMap[domain] = IScheduleServerRecord(uri="http://%s:%s/.well-known/ischedule" % result)
- else:
- # Create the iSchedule server record for this server
- cls.domainServerMap[domain] = IScheduleServerRecord(uri="https://%s:%s/.well-known/ischedule" % result)
-
- returnValue(cls.domainServerMap[domain])
-
-
- @inlineCallbacks
- def generateSchedulingResponses(self, refreshOnly=False):
- """
- Generate scheduling responses for remote recipients.
- """
-
- # Group recipients by server so that we can do a single request with multiple recipients
- # to each different server.
- groups = {}
- for recipient in self.recipients:
- if isinstance(recipient, RemoteCalendarUser):
- # Map the recipient's domain to a server
- server = (yield self.serverForDomain(recipient.domain))
- elif isinstance(recipient, PartitionedCalendarUser):
- server = self._getServerForPartitionedUser(recipient)
- elif isinstance(recipient, OtherServerCalendarUser):
- server = self._getServerForOtherServerUser(recipient)
- else:
- assert False, "Incorrect calendar user address class"
- if not server:
- # Cannot do server-to-server for this recipient.
- err = HTTPError(ErrorResponse(
- responsecode.NOT_FOUND,
- (ischedule_namespace, "recipient-allowed"),
- "No server for recipient",
- ))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
-
- # Process next recipient
- continue
-
- if not server.allow_to:
- # Cannot do server-to-server outgoing requests for this server.
- err = HTTPError(ErrorResponse(
- responsecode.NOT_FOUND,
- (ischedule_namespace, "recipient-allowed"),
- "Cannot send to recipient's server",
- ))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
- # Process next recipient
- continue
-
- groups.setdefault(server, []).append(recipient)
-
- if len(groups) == 0:
- returnValue(None)
-
- # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
- # we will generate for each request. That way we can have parallel requests in progress
- # rather than serialize them.
- deferreds = []
- for server, recipients in groups.iteritems():
- requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses, refreshOnly)
- deferreds.append(requestor.doRequest())
-
- yield DeferredList(deferreds)
-
-
- def _getServerForPartitionedUser(self, recipient):
-
- if not hasattr(self, "partitionedServers"):
- self.partitionedServers = {}
-
- partition = recipient.principal.partitionURI()
- if partition not in self.partitionedServers:
- self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
- self.partitionedServers[partition].unNormalizeAddresses = False
- self.partitionedServers[partition].moreHeaders.append(recipient.principal.server().secretHeader())
-
- return self.partitionedServers[partition]
-
-
- def _getServerForOtherServerUser(self, recipient):
-
- if not hasattr(self, "otherServers"):
- self.otherServers = {}
-
- serverURI = recipient.principal.serverURI()
- if serverURI not in self.otherServers:
- self.otherServers[serverURI] = IScheduleServerRecord(uri=joinURL(serverURI, "/ischedule"))
- self.otherServers[serverURI].unNormalizeAddresses = not recipient.principal.server().isImplicit
- self.otherServers[serverURI].moreHeaders.append(recipient.principal.server().secretHeader())
-
- return self.otherServers[serverURI]
-
-
-
-class IScheduleRequest(object):
-
- def __init__(self, scheduler, server, recipients, responses, refreshOnly=False):
-
- self.scheduler = scheduler
- self.server = server
- self.recipients = recipients
- self.responses = responses
- self.refreshOnly = refreshOnly
- self.headers = None
- self.data = None
-
-
- @inlineCallbacks
- def doRequest(self):
-
- # Generate an HTTP client request
- try:
- if not hasattr(self.scheduler.request, "extendedLogItems"):
- self.scheduler.request.extendedLogItems = {}
- if "itip.ischedule" not in self.scheduler.request.extendedLogItems:
- self.scheduler.request.extendedLogItems["itip.ischedule"] = 0
- self.scheduler.request.extendedLogItems["itip.ischedule"] += 1
-
- # Loop over at most 3 redirects
- ssl, host, port, path = self.server.details()
- for _ignore in xrange(3):
- self._prepareRequest(host, port)
- response = (yield self._processRequest(ssl, host, port, path))
- if response.code not in (responsecode.MOVED_PERMANENTLY, responsecode.TEMPORARY_REDIRECT,):
- break
- if response.code == responsecode.MOVED_PERMANENTLY:
- self.server.redirect(response.headers.getRawHeaders("location")[0])
- ssl, host, port, path = self.server.details()
- else:
- scheme, netloc, path, _ignore_query, _ignore_fragment = urlsplit(response.headers.getRawHeaders("location")[0])
- ssl = scheme.lower() == "https"
- host = netloc.split(":")
- if ":" in netloc:
- host, port = netloc.split(":")
- port = int(port)
- else:
- host = netloc
- port = 443 if ssl else 80
- else:
- raise ValueError("Too many redirects")
-
- if accountingEnabledForCategory("iSchedule"):
- self.loggedResponse = yield self.logResponse(response)
- emitAccounting("iSchedule", "", self.loggedRequest + "\n" + self.loggedResponse, "POST")
-
- if response.code in (responsecode.OK,):
- xml = (yield davXMLFromStream(response.stream))
- self._parseResponse(xml)
- else:
- raise ValueError("Incorrect server response status code: %s" % (response.code,))
-
- except Exception, e:
- # Generated failed responses for each recipient
- log.err("Could not do server-to-server request : %s %s" % (self, e))
- for recipient in self.recipients:
- err = HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (ischedule_namespace, "recipient-failed"),
- "Server-to-server request failed",
- ))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
-
- @inlineCallbacks
- def logRequest(self, request):
- """
- Log an HTTP request.
- """
-
- iostr = StringIO()
- iostr.write(">>>> Request start\n\n")
- if hasattr(request, "clientproto"):
- protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
- else:
- protocol = "HTTP/1.1"
- iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
- for name, valuelist in request.headers.getAllRawHeaders():
- for value in valuelist:
- # Do not log authorization details
- if name not in ("Authorization",):
- iostr.write("%s: %s\n" % (name, value))
- else:
- iostr.write("%s: xxxxxxxxx\n" % (name,))
- iostr.write("\n")
-
- # We need to play a trick with the request stream as we can only read it once. So we
- # read it, store the value in a MemoryStream, and replace the request's stream with that,
- # so the data can be read again.
- data = (yield allDataFromStream(request.stream))
- iostr.write(data)
- request.stream = MemoryStream(data if data is not None else "")
- request.stream.doStartReading = None
-
- iostr.write("\n\n>>>> Request end\n")
- returnValue(iostr.getvalue())
-
-
- @inlineCallbacks
- def logResponse(self, response):
- """
- Log an HTTP request.
- """
- iostr = StringIO()
- iostr.write(">>>> Response start\n\n")
- code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
- iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
- for name, valuelist in response.headers.getAllRawHeaders():
- for value in valuelist:
- # Do not log authorization details
- if name not in ("WWW-Authenticate",):
- iostr.write("%s: %s\n" % (name, value))
- else:
- iostr.write("%s: xxxxxxxxx\n" % (name,))
- iostr.write("\n")
-
- # We need to play a trick with the response stream to ensure we don't mess it up. So we
- # read it, store the value in a MemoryStream, and replace the response's stream with that,
- # so the data can be read again.
- data = (yield allDataFromStream(response.stream))
- iostr.write(data)
- response.stream = MemoryStream(data if data is not None else "")
- response.stream.doStartReading = None
-
- iostr.write("\n\n>>>> Response end\n")
- returnValue(iostr.getvalue())
-
-
- def _prepareRequest(self, host, port):
- """
- Setup the request for sending. We might need to do this several times
- whilst following redirects.
- """
-
- self._prepareHeaders(host, port)
- self._prepareData()
-
-
- def _prepareHeaders(self, host, port):
- """
- Always generate a new set of headers because the Host may varying during redirects,
- or we may need to dump DKIM added headers during a redirect.
- """
- self.sign_headers = []
-
- self.headers = Headers()
- self.headers.setHeader("Host", utf8String(host + ":%s" % (port,)))
- self.sign_headers.append("Host")
-
- # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
- originator = self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee
- originator = normalizeCUAddress(originator, normalizationLookup, self.scheduler.resource.principalForCalendarUserAddress, toUUID=False)
- self.headers.addRawHeader("Originator", utf8String(originator))
- self.sign_headers.append("Originator")
-
- for recipient in self.recipients:
- self.headers.addRawHeader("Recipient", utf8String(recipient.cuaddr))
-
- # Remember to "over sign" the Recipient header
- self.sign_headers.append("Recipient+")
-
- self._doAuthentication()
-
- self.headers.setHeader("Content-Type", MimeType("text", "calendar", params={"charset": "utf-8"}))
- self.sign_headers.append("Content-Type")
-
- # Add any additional headers
- for name, value in self.server.moreHeaders:
- self.headers.addRawHeader(name, value)
-
- if self.refreshOnly:
- self.headers.addRawHeader("X-CALENDARSERVER-ITIP-REFRESHONLY", "T")
-
-
- def _doAuthentication(self):
- if self.server.authentication and self.server.authentication[0] == "basic":
- self.headers.setHeader(
- "Authorization",
- ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
- )
- self.sign_headers.append("Authorization")
-
-
- def _prepareData(self):
- """
- Prepare data via normalization etc. Only need to do this once even when
- redirects occur.
- """
-
- if self.data is None:
- # Need to remap cuaddrs from urn:uuid
- if self.server.unNormalizeAddresses and self.scheduler.method == "PUT":
- normalizedCalendar = self.scheduler.calendar.duplicate()
- normalizedCalendar.normalizeCalendarUserAddresses(
- normalizationLookup,
- self.scheduler.resource.principalForCalendarUserAddress,
- toUUID=False)
- else:
- normalizedCalendar = self.scheduler.calendar
-
- # For VFREEBUSY we need to strip out ATTENDEEs that do not match the recipient list
- if self.scheduler.isfreebusy:
- normalizedCalendar.removeAllButTheseAttendees([recipient.cuaddr for recipient in self.recipients])
-
- self.data = str(normalizedCalendar)
-
-
- @inlineCallbacks
- def _processRequest(self, ssl, host, port, path):
- from twisted.internet import reactor
- f = Factory()
- f.protocol = HTTPClientProtocol
- if ssl:
- ep = GAIEndpoint(reactor, host, port,
- _configuredClientContextFactory())
- else:
- ep = GAIEndpoint(reactor, host, port)
- proto = (yield ep.connect(f))
-
- if config.Scheduling.iSchedule.DKIM.Enabled:
- domain, selector, key_file, algorithm, useDNSKey, useHTTPKey, usePrivateExchangeKey, expire = DKIMUtils.getConfiguration(config)
- request = DKIMRequest(
- "POST",
- path,
- self.headers,
- self.data,
- domain,
- selector,
- key_file,
- algorithm,
- self.sign_headers,
- useDNSKey,
- useHTTPKey,
- usePrivateExchangeKey,
- expire,
- )
- yield request.sign()
- else:
- request = ClientRequest("POST", path, self.headers, self.data)
-
- if accountingEnabledForCategory("iSchedule"):
- self.loggedRequest = yield self.logRequest(request)
-
- response = (yield proto.submitRequest(request))
-
- returnValue(response)
-
-
- def _parseResponse(self, xml):
-
- # Check for correct root element
- schedule_response = xml.root_element
- if not isinstance(schedule_response, ScheduleResponse) or not schedule_response.children:
- raise HTTPError(responsecode.BAD_REQUEST)
-
- # Parse each response - do this twice: once looking for errors that will
- # result in all recipients shown as failures; the second loop adds all the
- # valid responses to the actual result.
- for response in schedule_response.children:
- if not isinstance(response, Response) or not response.children:
- raise HTTPError(responsecode.BAD_REQUEST)
- recipient = response.childOfType(Recipient)
- request_status = response.childOfType(RequestStatus)
- if not recipient or not request_status:
- raise HTTPError(responsecode.BAD_REQUEST)
- for response in schedule_response.children:
- recipient = str(response.childOfType(Recipient))
- request_status = str(response.childOfType(RequestStatus))
- calendar_data = response.childOfType(CalendarData)
- if calendar_data:
- calendar_data = str(calendar_data)
- error = response.childOfType(Error)
- if error:
- error = error.children
- desc = response.childOfType(ResponseDescription)
- if desc:
- desc = str(desc)
- self.responses.clone(
- recipient,
- request_status,
- calendar_data,
- error,
- desc,
- )
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/delivery.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/delivery.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/delivery.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/delivery.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,498 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from StringIO import StringIO
+
+from twisted.internet.defer import inlineCallbacks, DeferredList, returnValue
+from twisted.internet.protocol import Factory
+
+from twisted.python.failure import Failure
+
+from twext.web2 import responsecode
+from twext.web2.client.http import ClientRequest
+from twext.web2.client.http import HTTPClientProtocol
+from twext.web2.dav.util import davXMLFromStream, joinURL, allDataFromStream
+from twext.web2.http import HTTPError
+from twext.web2.http_headers import Headers
+from twext.web2.http_headers import MimeType
+from twext.web2.stream import MemoryStream
+
+from twext.internet.gaiendpoint import GAIEndpoint
+from twext.python.log import Logger
+from twext.web2.dav.http import ErrorResponse
+
+from twistedcaldav.accounting import accountingEnabledForCategory, emitAccounting
+from twistedcaldav.client.pool import _configuredClientContextFactory
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser, RemoteCalendarUser, \
+ OtherServerCalendarUser
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.ischedule.dkim import DKIMRequest, DKIMUtils
+from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServerRecord
+from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServers
+from twistedcaldav.scheduling.ischedule.utils import lookupServerViaSRV
+from twistedcaldav.scheduling.ischedule.xml import ScheduleResponse, Response, \
+ RequestStatus, Recipient, ischedule_namespace, CalendarData, \
+ ResponseDescription, Error
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from twistedcaldav.util import utf8String, normalizationLookup
+from urlparse import urlsplit
+from twistedcaldav.ical import normalizeCUAddress
+
+"""
+Handles the sending of iSchedule scheduling messages. Used for both cross-domain scheduling,
+as well as internal partitioning or podding.
+"""
+
+__all__ = [
+ "ScheduleViaISchedule",
+]
+
+log = Logger()
+
+
+
+class ScheduleViaISchedule(DeliveryService):
+
+ domainServerMap = {}
+
+ @classmethod
+ def serviceType(cls):
+ return DeliveryService.serviceType_ischedule
+
+
+ @classmethod
+ @inlineCallbacks
+ def matchCalendarUserAddress(cls, cuaddr):
+
+ # TODO: here is where we would attempt service discovery based on the cuaddr.
+
+ # Only handle mailtos:
+ if cuaddr.lower().startswith("mailto:"):
+ _ignore_local, domain = cuaddr[7:].split("@", 1)
+ server = (yield cls.serverForDomain(domain))
+ returnValue(server is not None)
+
+ # Do default match
+ result = (yield super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr))
+ returnValue(result)
+
+
+ @classmethod
+ @inlineCallbacks
+ def serverForDomain(cls, domain):
+ if domain not in cls.domainServerMap:
+
+ # First check built-in list of remote servers
+ servermgr = IScheduleServers()
+ server = servermgr.mapDomain(domain)
+ if server is not None:
+ cls.domainServerMap[domain] = server
+ else:
+ # Lookup domain
+ result = (yield lookupServerViaSRV(domain))
+ if result is None:
+ # Lookup domain
+ result = (yield lookupServerViaSRV(domain, service="_ischedule"))
+ if result is None:
+ cls.domainServerMap[domain] = None
+ else:
+ # Create the iSchedule server record for this server
+ cls.domainServerMap[domain] = IScheduleServerRecord(uri="http://%s:%s/.well-known/ischedule" % result)
+ else:
+ # Create the iSchedule server record for this server
+ cls.domainServerMap[domain] = IScheduleServerRecord(uri="https://%s:%s/.well-known/ischedule" % result)
+
+ returnValue(cls.domainServerMap[domain])
+
+
+ @inlineCallbacks
+ def generateSchedulingResponses(self, refreshOnly=False):
+ """
+ Generate scheduling responses for remote recipients.
+ """
+
+ # Group recipients by server so that we can do a single request with multiple recipients
+ # to each different server.
+ groups = {}
+ for recipient in self.recipients:
+ if isinstance(recipient, RemoteCalendarUser):
+ # Map the recipient's domain to a server
+ server = (yield self.serverForDomain(recipient.domain))
+ elif isinstance(recipient, PartitionedCalendarUser):
+ server = self._getServerForPartitionedUser(recipient)
+ elif isinstance(recipient, OtherServerCalendarUser):
+ server = self._getServerForOtherServerUser(recipient)
+ else:
+ assert False, "Incorrect calendar user address class"
+ if not server:
+ # Cannot do server-to-server for this recipient.
+ err = HTTPError(ErrorResponse(
+ responsecode.NOT_FOUND,
+ (ischedule_namespace, "recipient-allowed"),
+ "No server for recipient",
+ ))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
+
+ # Process next recipient
+ continue
+
+ if not server.allow_to:
+ # Cannot do server-to-server outgoing requests for this server.
+ err = HTTPError(ErrorResponse(
+ responsecode.NOT_FOUND,
+ (ischedule_namespace, "recipient-allowed"),
+ "Cannot send to recipient's server",
+ ))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+ # Process next recipient
+ continue
+
+ groups.setdefault(server, []).append(recipient)
+
+ if len(groups) == 0:
+ returnValue(None)
+
+ # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
+ # we will generate for each request. That way we can have parallel requests in progress
+ # rather than serialize them.
+ deferreds = []
+ for server, recipients in groups.iteritems():
+ requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses, refreshOnly)
+ deferreds.append(requestor.doRequest())
+
+ yield DeferredList(deferreds)
+
+
+ def _getServerForPartitionedUser(self, recipient):
+
+ if not hasattr(self, "partitionedServers"):
+ self.partitionedServers = {}
+
+ partition = recipient.principal.partitionURI()
+ if partition not in self.partitionedServers:
+ self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
+ self.partitionedServers[partition].unNormalizeAddresses = False
+ self.partitionedServers[partition].moreHeaders.append(recipient.principal.server().secretHeader())
+
+ return self.partitionedServers[partition]
+
+
+ def _getServerForOtherServerUser(self, recipient):
+
+ if not hasattr(self, "otherServers"):
+ self.otherServers = {}
+
+ serverURI = recipient.principal.serverURI()
+ if serverURI not in self.otherServers:
+ self.otherServers[serverURI] = IScheduleServerRecord(uri=joinURL(serverURI, "/ischedule"))
+ self.otherServers[serverURI].unNormalizeAddresses = not recipient.principal.server().isImplicit
+ self.otherServers[serverURI].moreHeaders.append(recipient.principal.server().secretHeader())
+
+ return self.otherServers[serverURI]
+
+
+
+class IScheduleRequest(object):
+
+ def __init__(self, scheduler, server, recipients, responses, refreshOnly=False):
+
+ self.scheduler = scheduler
+ self.server = server
+ self.recipients = recipients
+ self.responses = responses
+ self.refreshOnly = refreshOnly
+ self.headers = None
+ self.data = None
+
+
+ @inlineCallbacks
+ def doRequest(self):
+
+ # Generate an HTTP client request
+ try:
+ if not hasattr(self.scheduler.request, "extendedLogItems"):
+ self.scheduler.request.extendedLogItems = {}
+ if "itip.ischedule" not in self.scheduler.request.extendedLogItems:
+ self.scheduler.request.extendedLogItems["itip.ischedule"] = 0
+ self.scheduler.request.extendedLogItems["itip.ischedule"] += 1
+
+ # Loop over at most 3 redirects
+ ssl, host, port, path = self.server.details()
+ for _ignore in xrange(3):
+ self._prepareRequest(host, port)
+ response = (yield self._processRequest(ssl, host, port, path))
+ if response.code not in (responsecode.MOVED_PERMANENTLY, responsecode.TEMPORARY_REDIRECT,):
+ break
+ if response.code == responsecode.MOVED_PERMANENTLY:
+ self.server.redirect(response.headers.getRawHeaders("location")[0])
+ ssl, host, port, path = self.server.details()
+ else:
+ scheme, netloc, path, _ignore_query, _ignore_fragment = urlsplit(response.headers.getRawHeaders("location")[0])
+ ssl = scheme.lower() == "https"
+ host = netloc.split(":")
+ if ":" in netloc:
+ host, port = netloc.split(":")
+ port = int(port)
+ else:
+ host = netloc
+ port = 443 if ssl else 80
+ else:
+ raise ValueError("Too many redirects")
+
+ if accountingEnabledForCategory("iSchedule"):
+ self.loggedResponse = yield self.logResponse(response)
+ emitAccounting("iSchedule", "", self.loggedRequest + "\n" + self.loggedResponse, "POST")
+
+ if response.code in (responsecode.OK,):
+ xml = (yield davXMLFromStream(response.stream))
+ self._parseResponse(xml)
+ else:
+ raise ValueError("Incorrect server response status code: %s" % (response.code,))
+
+ except Exception, e:
+ # Generated failed responses for each recipient
+ log.err("Could not do server-to-server request : %s %s" % (self, e))
+ for recipient in self.recipients:
+ err = HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (ischedule_namespace, "recipient-failed"),
+ "Server-to-server request failed",
+ ))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+
+ @inlineCallbacks
+ def logRequest(self, request):
+ """
+ Log an HTTP request.
+ """
+
+ iostr = StringIO()
+ iostr.write(">>>> Request start\n\n")
+ if hasattr(request, "clientproto"):
+ protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
+ else:
+ protocol = "HTTP/1.1"
+ iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
+ for name, valuelist in request.headers.getAllRawHeaders():
+ for value in valuelist:
+ # Do not log authorization details
+ if name not in ("Authorization",):
+ iostr.write("%s: %s\n" % (name, value))
+ else:
+ iostr.write("%s: xxxxxxxxx\n" % (name,))
+ iostr.write("\n")
+
+ # We need to play a trick with the request stream as we can only read it once. So we
+ # read it, store the value in a MemoryStream, and replace the request's stream with that,
+ # so the data can be read again.
+ data = (yield allDataFromStream(request.stream))
+ iostr.write(data)
+ request.stream = MemoryStream(data if data is not None else "")
+ request.stream.doStartReading = None
+
+ iostr.write("\n\n>>>> Request end\n")
+ returnValue(iostr.getvalue())
+
+
+ @inlineCallbacks
+ def logResponse(self, response):
+ """
+ Log an HTTP request.
+ """
+ iostr = StringIO()
+ iostr.write(">>>> Response start\n\n")
+ code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
+ iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
+ for name, valuelist in response.headers.getAllRawHeaders():
+ for value in valuelist:
+ # Do not log authorization details
+ if name not in ("WWW-Authenticate",):
+ iostr.write("%s: %s\n" % (name, value))
+ else:
+ iostr.write("%s: xxxxxxxxx\n" % (name,))
+ iostr.write("\n")
+
+ # We need to play a trick with the response stream to ensure we don't mess it up. So we
+ # read it, store the value in a MemoryStream, and replace the response's stream with that,
+ # so the data can be read again.
+ data = (yield allDataFromStream(response.stream))
+ iostr.write(data)
+ response.stream = MemoryStream(data if data is not None else "")
+ response.stream.doStartReading = None
+
+ iostr.write("\n\n>>>> Response end\n")
+ returnValue(iostr.getvalue())
+
+
+ def _prepareRequest(self, host, port):
+ """
+ Setup the request for sending. We might need to do this several times
+ whilst following redirects.
+ """
+
+ self._prepareHeaders(host, port)
+ self._prepareData()
+
+
+ def _prepareHeaders(self, host, port):
+ """
+ Always generate a new set of headers because the Host may varying during redirects,
+ or we may need to dump DKIM added headers during a redirect.
+ """
+ self.sign_headers = []
+
+ self.headers = Headers()
+ self.headers.setHeader("Host", utf8String(host + ":%s" % (port,)))
+ self.sign_headers.append("Host")
+
+ # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
+ originator = self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee
+ originator = normalizeCUAddress(originator, normalizationLookup, self.scheduler.resource.principalForCalendarUserAddress, toUUID=False)
+ self.headers.addRawHeader("Originator", utf8String(originator))
+ self.sign_headers.append("Originator")
+
+ for recipient in self.recipients:
+ self.headers.addRawHeader("Recipient", utf8String(recipient.cuaddr))
+
+ # Remember to "over sign" the Recipient header
+ self.sign_headers.append("Recipient+")
+
+ self._doAuthentication()
+
+ self.headers.setHeader("Content-Type", MimeType("text", "calendar", params={"charset": "utf-8"}))
+ self.sign_headers.append("Content-Type")
+
+ # Add any additional headers
+ for name, value in self.server.moreHeaders:
+ self.headers.addRawHeader(name, value)
+
+ if self.refreshOnly:
+ self.headers.addRawHeader("X-CALENDARSERVER-ITIP-REFRESHONLY", "T")
+
+
+ def _doAuthentication(self):
+ if self.server.authentication and self.server.authentication[0] == "basic":
+ self.headers.setHeader(
+ "Authorization",
+ ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
+ )
+ self.sign_headers.append("Authorization")
+
+
+ def _prepareData(self):
+ """
+ Prepare data via normalization etc. Only need to do this once even when
+ redirects occur.
+ """
+
+ if self.data is None:
+ # Need to remap cuaddrs from urn:uuid
+ if self.server.unNormalizeAddresses and self.scheduler.method == "PUT":
+ normalizedCalendar = self.scheduler.calendar.duplicate()
+ normalizedCalendar.normalizeCalendarUserAddresses(
+ normalizationLookup,
+ self.scheduler.resource.principalForCalendarUserAddress,
+ toUUID=False)
+ else:
+ normalizedCalendar = self.scheduler.calendar
+
+ # For VFREEBUSY we need to strip out ATTENDEEs that do not match the recipient list
+ if self.scheduler.isfreebusy:
+ normalizedCalendar.removeAllButTheseAttendees([recipient.cuaddr for recipient in self.recipients])
+
+ self.data = str(normalizedCalendar)
+
+
+ @inlineCallbacks
+ def _processRequest(self, ssl, host, port, path):
+ from twisted.internet import reactor
+ f = Factory()
+ f.protocol = HTTPClientProtocol
+ if ssl:
+ ep = GAIEndpoint(reactor, host, port,
+ _configuredClientContextFactory())
+ else:
+ ep = GAIEndpoint(reactor, host, port)
+ proto = (yield ep.connect(f))
+
+ if config.Scheduling.iSchedule.DKIM.Enabled:
+ domain, selector, key_file, algorithm, useDNSKey, useHTTPKey, usePrivateExchangeKey, expire = DKIMUtils.getConfiguration(config)
+ request = DKIMRequest(
+ "POST",
+ path,
+ self.headers,
+ self.data,
+ domain,
+ selector,
+ key_file,
+ algorithm,
+ self.sign_headers,
+ useDNSKey,
+ useHTTPKey,
+ usePrivateExchangeKey,
+ expire,
+ )
+ yield request.sign()
+ else:
+ request = ClientRequest("POST", path, self.headers, self.data)
+
+ if accountingEnabledForCategory("iSchedule"):
+ self.loggedRequest = yield self.logRequest(request)
+
+ response = (yield proto.submitRequest(request))
+
+ returnValue(response)
+
+
+ def _parseResponse(self, xml):
+
+ # Check for correct root element
+ schedule_response = xml.root_element
+ if not isinstance(schedule_response, ScheduleResponse) or not schedule_response.children:
+ raise HTTPError(responsecode.BAD_REQUEST)
+
+ # Parse each response - do this twice: once looking for errors that will
+ # result in all recipients shown as failures; the second loop adds all the
+ # valid responses to the actual result.
+ for response in schedule_response.children:
+ if not isinstance(response, Response) or not response.children:
+ raise HTTPError(responsecode.BAD_REQUEST)
+ recipient = response.childOfType(Recipient)
+ request_status = response.childOfType(RequestStatus)
+ if not recipient or not request_status:
+ raise HTTPError(responsecode.BAD_REQUEST)
+ for response in schedule_response.children:
+ recipient = str(response.childOfType(Recipient))
+ request_status = str(response.childOfType(RequestStatus))
+ calendar_data = response.childOfType(CalendarData)
+ if calendar_data:
+ calendar_data = str(calendar_data)
+ error = response.childOfType(Error)
+ if error:
+ error = error.children
+ desc = response.childOfType(ResponseDescription)
+ if desc:
+ desc = str(desc)
+ self.responses.clone(
+ recipient,
+ request_status,
+ calendar_data,
+ error,
+ desc,
+ )
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/dkim.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/dkim.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/dkim.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,981 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.python.log import Logger
-from twext.web2.client.http import ClientRequest
-from twext.web2.dav.util import allDataFromStream, joinURL
-from twext.web2.http import Response
-from twext.web2.http_headers import MimeType
-from twext.web2.stream import MemoryStream
-
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
-from twistedcaldav.client.geturl import getURL
-from twistedcaldav.config import ConfigurationError
-from twistedcaldav.simpleresource import SimpleResource, SimpleDataResource
-from twistedcaldav.scheduling.ischedule.utils import lookupDataViaTXT, \
- lookupServerViaSRV
-
-from Crypto.Hash import SHA, SHA256
-from Crypto.PublicKey import RSA
-from Crypto.Signature import PKCS1_v1_5
-
-import base64
-import binascii
-import collections
-import hashlib
-import os
-import textwrap
-import time
-import uuid
-
-"""
-DKIM HTTP message generation and validation,
-"""
-
-log = Logger()
-
-# DKIM/iSchedule Constants
-RSA1 = "rsa-sha1"
-RSA256 = "rsa-sha256"
-Q_DNS = "dns/txt"
-Q_HTTP = "http/well-known"
-Q_PRIVATE = "private-exchange"
-
-KEY_SERVICE_TYPE = "ischedule"
-
-# Headers
-DKIM_SIGNATURE = "DKIM-Signature"
-ISCHEDULE_VERSION = "iSchedule-Version"
-ISCHEDULE_VERSION_VALUE = "1.0"
-ISCHEDULE_MESSAGE_ID = "iSchedule-Message-ID"
-
-
-
-class DKIMUtils(object):
- """
- Some useful functions.
- """
-
- @staticmethod
- def validConfiguration(config):
- if config.Scheduling.iSchedule.DKIM.Enabled:
-
- if not config.Scheduling.iSchedule.DKIM.Domain and not config.ServerHostName:
- msg = "DKIM: No domain specified"
- log.error(msg)
- raise ConfigurationError(msg)
-
- if not config.Scheduling.iSchedule.DKIM.KeySelector:
- msg = "DKIM: No selector specified"
- log.error(msg)
- raise ConfigurationError(msg)
-
- if config.Scheduling.iSchedule.DKIM.SignatureAlgorithm not in (RSA1, RSA256):
- msg = "DKIM: Invalid algorithm: %s" % (config.Scheduling.iSchedule.SignatureAlgorithm,)
- log.error(msg)
- raise ConfigurationError(msg)
-
- try:
- with open(config.Scheduling.iSchedule.DKIM.PrivateKeyFile) as f:
- key_data = f.read()
- except IOError, e:
- msg = "DKIM: Cannot read private key file: %s %s" % (config.Scheduling.iSchedule.DKIM.PrivateKeyFile, e,)
- log.error(msg)
- raise ConfigurationError(msg)
- try:
- RSA.importKey(key_data)
- except:
- msg = "DKIM: Invalid private key file: %s" % (config.Scheduling.iSchedule.DKIM.PrivateKeyFile,)
- log.error(msg)
- raise ConfigurationError(msg)
-
- try:
- with open(config.Scheduling.iSchedule.DKIM.PublicKeyFile) as f:
- key_data = f.read()
- except IOError, e:
- msg = "DKIM: Cannot read public key file: %s %s" % (config.Scheduling.iSchedule.DKIM.PublicKeyFile, e,)
- log.error(msg)
- raise ConfigurationError(msg)
- try:
- RSA.importKey(key_data)
- except:
- msg = "DKIM: Invalid public key file: %s" % (config.Scheduling.iSchedule.DKIM.PublicKeyFile,)
- log.error(msg)
- raise ConfigurationError(msg)
-
- if config.Scheduling.iSchedule.DKIM.PrivateExchanges:
- if not os.path.exists(config.Scheduling.iSchedule.DKIM.PrivateExchanges):
- try:
- os.makedirs(config.Scheduling.iSchedule.DKIM.PrivateExchanges)
- except IOError, e:
- msg = "DKIM: Cannot create public key private exchange directory: %s" % (config.Scheduling.iSchedule.DKIM.PrivateExchanges,)
- log.error(msg)
- raise ConfigurationError(msg)
- if not os.path.isdir(config.Scheduling.iSchedule.DKIM.PrivateExchanges):
- msg = "DKIM: Invalid public key private exchange directory: %s" % (config.Scheduling.iSchedule.DKIM.PrivateExchanges,)
- log.error(msg)
- raise ConfigurationError(msg)
- PublicKeyLookup_PrivateExchange.directory = config.Scheduling.iSchedule.DKIM.PrivateExchanges
-
- log.info("DKIM: Enabled")
- else:
- log.info("DKIM: Disabled")
-
-
- @staticmethod
- def getConfiguration(config):
- """
- Return a tuple of the parameters derived from the config that are used to initialize the DKIMRequest.
-
- @param config: configuration to look at
- @type config: L{Config}
- """
-
- domain = config.Scheduling.iSchedule.DKIM.Domain if config.Scheduling.iSchedule.DKIM.Domain else config.ServerHostName
- selector = config.Scheduling.iSchedule.DKIM.KeySelector
- key_file = config.Scheduling.iSchedule.DKIM.PrivateKeyFile
- algorithm = config.Scheduling.iSchedule.DKIM.SignatureAlgorithm
- useDNSKey = config.Scheduling.iSchedule.DKIM.UseDNSKey
- useHTTPKey = config.Scheduling.iSchedule.DKIM.UseHTTPKey
- usePrivateExchangeKey = config.Scheduling.iSchedule.DKIM.UsePrivateExchangeKey
- expire = config.Scheduling.iSchedule.DKIM.ExpireSeconds
-
- return domain, selector, key_file, algorithm, useDNSKey, useHTTPKey, usePrivateExchangeKey, expire
-
-
- @staticmethod
- def hashlib_method(algorithm):
- """
- Return hashlib function for DKIM algorithm.
- """
- return {
- RSA1 : hashlib.sha1,
- RSA256: hashlib.sha256,
- }[algorithm]
-
-
- @staticmethod
- def hash_name(algorithm):
- """
- Return RSA hash name for DKIM algorithm.
- """
- return {
- RSA1 : "SHA-1",
- RSA256: "SHA-256",
- }[algorithm]
-
-
- @staticmethod
- def hash_func(algorithm):
- """
- Return RSA hash name for DKIM algorithm.
- """
- return {
- RSA1 : SHA,
- RSA256: SHA256,
- }[algorithm]
-
-
- @staticmethod
- def extractTags(data):
- """
- Split a DKIM tag list into a dict, removing unneeded whitespace.
- """
- # Extract tags from the data
- splits = [item.strip() for item in data.split(";")]
- dkim_tags = {}
- for item in splits:
- try:
- name, value = item.split("=", 1)
- dkim_tags[name.strip()] = value.strip()
- except ValueError:
- pass
- return dkim_tags
-
-
- @staticmethod
- def canonicalizeHeader(name, value, remove_b=None):
- """
- Canonicalize the header using "relaxed" method. Optionally remove the b= value from
- any DKIM-Signature present.
-
- FIXME: this needs to be smarter about where valid WSP can occur in a header. Right now it will
- blindly collapse all runs of SP/HTAB into a single SP. That could be wrong if a legitimate sequence of
- SP/HTAB occurs in a header value.
-
- @param name: header name
- @type name: C{str}
- @param value: header value
- @type value: C{str}
- @param remove_b: the b= value to remove, or C{None} if no removal needed
- @type remove_b: C{str} or C{None}
- """
-
- # Basic relaxed behavior
- name = name.lower()
- value = " ".join(value.split())
-
- # Special case DKIM-Signature: remove the b= value for signature
- if remove_b is not None and name == DKIM_SIGNATURE.lower():
- pos = value.find(remove_b)
- value = value[:pos] + value[pos + len(remove_b):]
- value = " ".join(value.split())
-
- crlf = "" if name == DKIM_SIGNATURE.lower() else "\r\n"
- return "%s:%s%s" % (name, value, crlf)
-
-
- @staticmethod
- def canonicalizeBody(data):
- if not data.endswith("\r\n"):
- data += "\r\n"
- return data
-
-
- @staticmethod
- def sign(data, privkey, hashfunc):
- h = hashfunc.new(data)
- signer = PKCS1_v1_5.new(privkey)
- return base64.b64encode(signer.sign(h))
-
-
- @staticmethod
- def verify(data, signature, pubkey, hashfunc):
- h = hashfunc.new(data)
- verifier = PKCS1_v1_5.new(pubkey)
- if not verifier.verify(h, base64.b64decode(signature)):
- raise ValueError()
-
-
-
-class DKIMRequest(ClientRequest):
- """
- A ClientRequest that optionally creates a DKIM signature.
- """
-
- keys = {}
-
- def __init__(
- self,
- method,
- uri,
- headers,
- stream,
- domain,
- selector,
- key_file,
- algorithm,
- sign_headers,
- useDNSKey,
- useHTTPKey,
- usePrivateExchangeKey,
- expire,
- ):
- """
- Create a DKIM request, which is a regular client request with the additional information needed to sign the message.
-
- @param method: HTTP method to use
- @type method: C{str}
- @param uri: request-URI
- @type uri: C{str}
- @param headers: request headers
- @type headers: L{http_headers}
- @param stream: body data
- @type stream: L{Stream}
- @param domain: the signing domain
- @type domain: C{str}
- @param selector: the signing key selector
- @type selector: C{str}
- @param key_file: path to a private key file
- @type key_file: C{str}
- @param algorithm: the signing algorithm to use
- @type algorithm: C{str}
- @param sign_headers: list of header names to sign - to "over sign" a header append a "+" to the name
- @type sign_headers: C{tuple}
- @param useDNSKey: whether or not to add DNS TXT lookup as a key lookup option
- @type useDNSKey: C{bool}
- @param useHTTPKey: whether or not to add HTTP .well-known as a key lookup option
- @type useHTTPKey: C{bool}
- @param usePrivateExchangeKey: whether or not to add private-exchange as a key lookup option
- @type usePrivateExchangeKey: C{bool}
- @param expire: number of seconds to expiration of signature
- @type expire: C{int}
- """
- super(DKIMRequest, self).__init__(method, uri, headers, stream)
- self.domain = domain
- self.selector = selector
- self.algorithm = algorithm
- self.key_file = key_file
- self.sign_headers = sign_headers
- self.time = str(int(time.time()))
- self.expire = str(int(time.time() + expire))
-
- assert self.domain
- assert self.selector
- assert self.algorithm in (RSA1, RSA256,)
- assert useDNSKey or useHTTPKey or usePrivateExchangeKey
-
- self.hash_method = DKIMUtils.hashlib_method(self.algorithm)
- self.hash_name = DKIMUtils.hash_name(self.algorithm)
- self.hash_func = DKIMUtils.hash_func(self.algorithm)
-
- self.keyMethods = []
- if usePrivateExchangeKey:
- self.keyMethods.append(Q_PRIVATE)
- if useHTTPKey:
- self.keyMethods.append(Q_HTTP)
- if useDNSKey:
- self.keyMethods.append(Q_DNS)
-
- self.message_id = str(uuid.uuid4())
-
-
- @inlineCallbacks
- def sign(self):
- """
- Generate the DKIM headers by signing the request. This should only be called once on the request and there must
- be no changes to the request (no headers, no body change) after it is called.
- """
-
- # Get the headers and the DKIM-Signature tags
- headers, dkim_tags = (yield self.signatureHeaders())
-
- # Sign the hash
- signature = self.generateSignature(headers)
-
- # Complete the header
- dkim_tags[-1] = ("b", signature,)
- dkim_header = "; ".join(["%s=%s" % item for item in dkim_tags])
- self.headers.addRawHeader(DKIM_SIGNATURE, dkim_header)
-
- log.debug("DKIM: Generated header: DKIM-Signature:%s" % (dkim_header,))
- log.debug("DKIM: Signed headers:\n%s" % (headers,))
-
- returnValue(signature)
-
-
- @inlineCallbacks
- def bodyHash(self):
- """
- Generate the hash of the request body data.
- """
-
- # We need to play a trick with the request stream as we can only read it once. So we
- # read it, store the value in a MemoryStream, and replace the request's stream with that,
- # so the data can be read again.
- data = (yield allDataFromStream(self.stream))
- self.stream = MemoryStream(data if data is not None else "")
- self.stream.doStartReading = None
-
- returnValue(base64.b64encode(self.hash_method(DKIMUtils.canonicalizeBody(data)).digest()))
-
-
- @inlineCallbacks
- def signatureHeaders(self):
- """
- Generate the headers that are going to be signed as well as the DKIM-Signature tags.
- """
-
- # Make sure we have the required iSchedule headers
- self.headers.addRawHeader(ISCHEDULE_VERSION, ISCHEDULE_VERSION_VALUE)
- self.headers.addRawHeader(ISCHEDULE_MESSAGE_ID, self.message_id)
- self.sign_headers += (ISCHEDULE_VERSION, ISCHEDULE_MESSAGE_ID,)
-
- # Need Cache-Control
- self.headers.setRawHeaders("Cache-Control", ("no-cache", "no-transform",))
-
- # Figure out all the existing headers to sign
- headers = []
- sign_headers = []
- raw = dict([(name.lower(), values) for name, values in self.headers.getAllRawHeaders()])
- for name in self.sign_headers:
- oversign = name[-1] == "+"
- name = name.rstrip("+")
- for value in reversed(raw.get(name.lower(), ())):
- headers.append(DKIMUtils.canonicalizeHeader(name, value))
- sign_headers.append(name)
- if oversign:
- sign_headers.append(name)
-
- # Generate the DKIM header tags we care about
- dkim_tags = []
- dkim_tags.append(("v", "1",))
- dkim_tags.append(("d", self.domain,))
- dkim_tags.append(("s", self.selector,))
- dkim_tags.append(("t", self.time,))
- dkim_tags.append(("x", self.expire,))
- dkim_tags.append(("a", self.algorithm,))
- dkim_tags.append(("q", ":".join(self.keyMethods),))
- dkim_tags.append(("http", base64.encodestring("%s:%s" % (self.method, self.uri,)).strip()))
- dkim_tags.append(("c", "relaxed/simple",))
- dkim_tags.append(("h", ":".join(sign_headers),))
- dkim_tags.append(("bh", (yield self.bodyHash()),))
- dkim_tags.append(("b", "",))
- dkim_header = "; ".join(["%s=%s" % item for item in dkim_tags])
-
- headers.append(DKIMUtils.canonicalizeHeader(DKIM_SIGNATURE, dkim_header))
- headers = "".join(headers)
-
- returnValue((headers, dkim_tags,))
-
-
- def generateSignature(self, headers):
- # Sign the hash
- if self.key_file not in self.keys:
- self.keys[self.key_file] = RSA.importKey(open(self.key_file).read())
- return DKIMUtils.sign(headers, self.keys[self.key_file], self.hash_func)
-
-
-
-class DKIMMissingError(Exception):
- """
- Used to indicate that the DKIM-Signature header is not present when
- attempting verification.
- """
- pass
-
-
-
-class DKIMVerificationError(Exception):
- """
- Used to indicate a DKIM verification error.
- """
- pass
-
-
-
-class DKIMVerifier(object):
- """
- Class used to verify an DKIM-signed HTTP request.
- """
-
- def __init__(self, request, key_lookup=None, protocol_debug=False):
- """
- @param request: The HTTP request to process
- @type request: L{twext.server.Request}
- """
- self.request = request
- self._debug = protocol_debug
- self.dkim_tags = {}
-
- # Prefer private exchange over HTTP over DNS when multiple are present
- self.key_lookup_methods = (
- PublicKeyLookup_PrivateExchange,
- PublicKeyLookup_HTTP_WellKnown,
- PublicKeyLookup_DNSTXT,
- ) if key_lookup is None else key_lookup
-
- self.time = int(time.time())
-
-
- @inlineCallbacks
- def verify(self):
- """
- @raise: DKIMVerificationError
- """
-
- # Check presence of DKIM header
- self.processDKIMHeader()
-
- # Extract the set of canonicalized headers being signed
- headers = self.extractSignedHeaders()
- log.debug("DKIM: Signed headers:\n%s" % (headers,))
-
- # Locate the public key
- pubkey = (yield self.locatePublicKey())
- if pubkey is None:
- raise DKIMVerificationError("No public key to verify the DKIM signature")
-
- # Do header verification
- try:
- DKIMUtils.verify(headers, self.dkim_tags["b"], pubkey, self.hash_func)
- except ValueError:
- msg = "Could not verify signature"
- _debug_msg = """
-DKIM-Signature:%s
-
-Headers to evaluate:
-%s
-
-Public key used:
-%s
-""" % (self.request.headers.getRawHeaders(DKIM_SIGNATURE)[0], headers, pubkey._original_data,)
- log.debug("DKIM: %s:%s" % (msg, _debug_msg,))
- if self._debug:
- msg = "%s:%s" % (msg, _debug_msg,)
- raise DKIMVerificationError(msg)
-
- # Do body validation
- data = (yield allDataFromStream(self.request.stream))
- self.request.stream = MemoryStream(data if data is not None else "")
- self.request.stream.doStartReading = None
- body = DKIMUtils.canonicalizeBody(data)
- bh = base64.b64encode(self.hash_method(body).digest())
- if bh != self.dkim_tags["bh"]:
- msg = "Could not verify the DKIM body hash"
- _debug_msg = """
-DKIM-Signature:%s
-
-Hash Method: %s
-
-Base64 encoded body:
-%s
-""" % (self.request.headers.getRawHeaders(DKIM_SIGNATURE), self.hash_method.__name__, base64.b64encode(body),)
- log.debug("DKIM: %s:%s" % (msg, _debug_msg,))
- if self._debug:
- msg = "%s:%s" % (msg, _debug_msg,)
- raise DKIMVerificationError(msg)
-
-
- def processDKIMHeader(self):
- """
- Extract the DKIM-Signature header and process the tags.
-
- @raise: DKIMVerificationError
- """
-
- # Check presence of header
- dkim = self.request.headers.getRawHeaders(DKIM_SIGNATURE)
- if dkim is None:
- msg = "No DKIM-Signature header present in the request"
- log.debug("DKIM: " + msg)
- raise DKIMMissingError(msg)
- if len(dkim) != 1:
- # TODO: This might need to be changed if we ever support forwarding of iSchedule messages - the forwarder
- # might also sign the message and add its own header
- msg = "Only one DKIM-Signature allowed in the request"
- log.debug("DKIM: " + msg)
- raise DKIMVerificationError(msg)
- dkim = dkim[0]
- log.debug("DKIM: Found header: DKIM-Signature:%s" % (dkim,))
-
- # Extract tags from the header
- self.dkim_tags = DKIMUtils.extractTags(dkim)
-
- # Verify validity of tags
- required_tags = ("v", "a", "b", "bh", "c", "d", "h", "s", "http",)
- for tag in required_tags:
- if tag not in self.dkim_tags:
- msg = "Missing DKIM-Signature tag: %s" % (tag,)
- log.debug("DKIM: " + msg)
- raise DKIMVerificationError(msg)
-
- check_values = {
- "v": ("1",),
- "a": (RSA1, RSA256,),
- "c": ("relaxed", "relaxed/simple",),
- "q": (Q_DNS, Q_HTTP, Q_PRIVATE,),
- }
- for tag, values in check_values.items():
- if tag not in required_tags and tag not in self.dkim_tags:
- pass
-
- # Handle some structured values
- if tag == "q":
- test = self.dkim_tags[tag].split(":")
- else:
- test = (self.dkim_tags[tag],)
- for item in test:
- if item not in values:
- msg = "Tag: %s has incorrect value: %s" % (tag, self.dkim_tags[tag],)
- log.debug("DKIM: " + msg)
- raise DKIMVerificationError(msg)
-
- # Check expiration
- if "x" in self.dkim_tags:
- diff_time = self.time - int(self.dkim_tags["x"])
- if diff_time > 0:
- msg = "Signature expired: %d seconds" % (diff_time,)
- log.debug("DKIM: " + msg)
- raise DKIMVerificationError(msg)
-
- # Check HTTP method/request-uri
- try:
- http_tag = base64.decodestring(self.dkim_tags["http"])
- except binascii.Error:
- msg = "Tag: http is not valid base64"
- log.debug("DKIM: " + msg)
- raise DKIMVerificationError(msg)
- try:
- method, uri = http_tag.split(":", 1)
- except ValueError:
- msg = "Tag: base64-decoded http is not valid: %s" % (http_tag,)
- log.debug("DKIM: " + msg)
- raise DKIMVerificationError(msg)
- if method != self.request.method:
- msg = "Tag: http method does not match: %s" % (method,)
- log.debug("DKIM: " + msg)
- raise DKIMVerificationError(msg)
- if uri != self.request.uri:
- msg = "Tag: http request-URI does not match: %s" % (uri,)
- log.debug("DKIM: " + msg)
- raise DKIMVerificationError(msg)
-
- # Some useful bits
- self.hash_method = DKIMUtils.hashlib_method(self.dkim_tags["a"])
- self.hash_func = DKIMUtils.hash_func(self.dkim_tags["a"])
- self.key_methods = self.dkim_tags["q"].split(":")
-
-
- def extractSignedHeaders(self):
- """
- Extract the set of headers from the request that are supposed to be signed. Canonicalize them
- and return the expected signed data.
- """
-
- # Extract all the expected signed headers taking into account the possibility of "over_counting"
- # headers - a technique used to ensure headers cannot be added in transit
- header_list = [hdr.strip() for hdr in self.dkim_tags["h"].split(":")]
- header_counter = collections.defaultdict(int)
-
- headers = []
- for header in header_list:
- actual_headers = self.request.headers.getRawHeaders(header)
- if actual_headers:
- try:
- headers.append((header, actual_headers[-1 - header_counter[header]],))
- except IndexError:
- pass
- header_counter[header] += 1
-
- # DKIM-Signature is always included at the end
- headers.append((DKIM_SIGNATURE, self.request.headers.getRawHeaders(DKIM_SIGNATURE)[0],))
-
- # Now canonicalize the values
- return "".join([DKIMUtils.canonicalizeHeader(name, value, remove_b=self.dkim_tags["b"]) for name, value in headers])
-
-
- @inlineCallbacks
- def locatePublicKey(self):
- """
- Try to lookup the public key matching the signature.
- """
-
- for lookup in self.key_lookup_methods:
- if lookup.method in self.key_methods or lookup.method == "*":
- pubkey = (yield lookup(self.dkim_tags).getPublicKey())
- if pubkey is not None:
- returnValue(pubkey)
- else:
- returnValue(None)
-
-
-
-class PublicKeyLookup(object):
- """
- Abstract base class for public key lookup methods.
-
- The L{method} attribute indicated the DKIM q= lookup method that the class will support, or if set to "*",
- the class will handle any q= value.
- """
-
- keyCache = {}
- method = None
-
- def __init__(self, dkim_tags):
- self.dkim_tags = dkim_tags
-
-
- @inlineCallbacks
- def getPublicKey(self, useCache=True):
- """
- Get key from cache or directly do query.
-
- @param useCache: whether or not to use the cache
- @type useCache: C{bool}
- """
- key = self._getSelectorKey()
- if key not in PublicKeyLookup.keyCache or not useCache:
- pubkeys = (yield self._lookupKeys())
- PublicKeyLookup.keyCache[key] = pubkeys
-
- returnValue(self._selectKey())
-
-
- def _getSelectorKey(self):
- """
- Get a token used to uniquely identify the key being looked up. Token format will
- depend on the lookup method.
- """
- raise NotImplementedError
-
-
- def _lookupKeys(self):
- """
- Do the key lookup using the actual lookup method. Return a C{list} of C{dict}
- that contains the key tag-list. Return a L{Deferred}.
- """
- raise NotImplementedError
-
-
- def _selectKey(self):
- """
- Select a specific key from the list that best matches the DKIM-Signature tags
- """
-
- pubkeys = PublicKeyLookup.keyCache.get(self._getSelectorKey(), [])
- for pkey in pubkeys:
- # Check validity
- if pkey.get("v", "DKIM1") != "DKIM1":
- continue
-
- # Check key type
- if pkey.get("k", "rsa") != "rsa":
- continue
-
- # Check valid hash algorithms
- hashes = set([hash.strip() for hash in pkey.get("h", "sha1:sha256").split(":")])
- if self.dkim_tags["a"][4:] not in hashes:
- continue
-
- # Service type
- if pkey.get("s", KEY_SERVICE_TYPE) not in ("*", KEY_SERVICE_TYPE,):
- continue
-
- # Non-revoked key
- if len(pkey.get("p", "")) == 0:
- continue
-
- return self._makeKey(pkey)
-
- log.debug("DKIM: No valid public key: %s %s" % (self._getSelectorKey(), pubkeys,))
- return None
-
-
- def _makeKey(self, pkey):
- """
- Turn the key tag list into an actual RSA public key object
-
- @param pkey: key tag list
- @type pkey: C{list}
- """
- key_data = """-----BEGIN PUBLIC KEY-----
-%s
------END PUBLIC KEY-----
-""" % ("\n".join(textwrap.wrap(pkey["p"], 64)),)
-
- try:
- key = RSA.importKey(key_data)
- key._original_data = key_data
- return key
- except:
- log.debug("DKIM: Unable to make public key:\n%s" % (key_data,))
- return None
-
-
- def flushCache(self):
- PublicKeyLookup.keyCache = {}
-
-
-
-class PublicKeyLookup_DNSTXT(PublicKeyLookup):
-
- method = Q_DNS
-
- def _getSelectorKey(self):
- """
- Get a token used to uniquely identify the key being looked up. Token format will
- depend on the lookup method.
- """
- return "%s._domainkey.%s" % (self.dkim_tags["s"], self.dkim_tags["d"],)
-
-
- @inlineCallbacks
- def _lookupKeys(self):
- """
- Do the key lookup using the actual lookup method.
- """
- log.debug("DKIM: TXT lookup: %s" % (self._getSelectorKey(),))
- data = (yield lookupDataViaTXT(self._getSelectorKey()))
- log.debug("DKIM: TXT lookup results: %s\n%s" % (self._getSelectorKey(), "\n".join(data),))
- returnValue(tuple([DKIMUtils.extractTags(line) for line in data]))
-
-
-
-class PublicKeyLookup_HTTP_WellKnown(PublicKeyLookup):
-
- method = Q_HTTP
-
- def _getSelectorKey(self):
- """
- Get a token used to uniquely identify the key being looked up. Token format will
- depend on the lookup method.
- """
-
- host = ".".join(self.dkim_tags["d"].split(".")[-2:])
- return "https://%s/.well-known/domainkey/%s/%s" % (host, self.dkim_tags["d"], self.dkim_tags["s"],)
-
-
- @inlineCallbacks
- def _getURI(self):
- """
- Determine the well-known URI for the public key service.
- """
-
- # First we do an SRV lookup for _domainkey to get the public key server host/port
- result = (yield lookupServerViaSRV(self.dkim_tags["d"], service="_domainkey"))
- if result is None:
- log.debug("DKIM: SRV _domainkey failed on: %s trying domain directly" % (self.dkim_tags["d"],))
- host = self.dkim_tags["d"]
- port = ""
- scheme = "https"
- else:
- host, port = result
- scheme = "http" if port in (80, 8008, 8080,) else "https"
- if port == 80 and scheme == "http" or port == 443 and scheme == "https":
- port = ""
- else:
- port = ":%s" % (port,)
-
- returnValue("%s://%s%s/.well-known/domainkey/%s/%s" % (scheme, host, port, self.dkim_tags["d"], self.dkim_tags["s"],))
-
-
- @inlineCallbacks
- def _lookupKeys(self):
- """
- Do the key lookup using the actual lookup method.
- """
-
- # First we do an SRV lookup for _domainkey to get the public key server URI
- uri = (yield self._getURI())
-
- log.debug("DKIM: HTTP/.well-known lookup: %s" % (uri,))
- response = (yield getURL(uri))
- if response is None or response.code / 100 != 2:
- log.debug("DKIM: Failed http/well-known lookup: %s %s" % (uri, response,))
- returnValue(())
-
- ct = response.headers.getRawHeaders("content-type", ("bogus/type",))[0]
- ct = ct.split(";", 1)
- ct = ct[0].strip()
- if ct not in ("text/plain",):
- log.debug("DKIM: Failed http/well-known lookup: wrong content-type returned %s %s" % (uri, ct,))
- returnValue(())
-
- log.debug("DKIM: HTTP/.well-known lookup results: %s\n%s" % (uri, response.data,))
- returnValue(tuple([DKIMUtils.extractTags(line) for line in response.data.splitlines()]))
-
-
-
-class PublicKeyLookup_PrivateExchange(PublicKeyLookup):
-
- method = Q_PRIVATE
- directory = None
-
- def _getSelectorKey(self):
- """
- Get a token used to uniquely identify the key being looked up. Token format will
- depend on the lookup method.
- """
- return "%s#%s" % (self.dkim_tags["d"], self.dkim_tags["s"],)
-
-
- def _lookupKeys(self):
- """
- Key information is stored in a file, one record per line.
- """
-
- # Check validity of paths
- if PublicKeyLookup_PrivateExchange.directory is None:
- log.debug("DKIM: Failed private-exchange lookup: no directory configured")
- return succeed(())
- keyfile = os.path.join(PublicKeyLookup_PrivateExchange.directory, self._getSelectorKey())
- if not os.path.exists(keyfile):
- log.debug("DKIM: Failed private-exchange lookup: no path %s" % (keyfile,))
- return succeed(())
-
- # Now read the data
- log.debug("DKIM: Private exchange lookup: %s" % (keyfile,))
- try:
- with open(keyfile) as f:
- keys = f.read()
- except IOError, e:
- log.debug("DKIM: Failed private-exchange lookup: could not read %s %s" % (keyfile, e,))
- return succeed(())
-
- log.debug("DKIM: Private exchange lookup results: %s\n%s" % (keyfile, keys))
- return succeed(tuple([DKIMUtils.extractTags(line) for line in keys.splitlines()]))
-
-
-
-class DomainKeyResource (SimpleResource):
- """
- Domainkey well-known resource.
- """
-
- def __init__(self, domain, selector, pubkeyfile):
- """
- """
- assert domain
- assert selector
-
- SimpleResource.__init__(self, principalCollections=None, isdir=True, defaultACL=SimpleResource.allReadACL)
- self.makeKeyData(domain, selector, pubkeyfile)
- self.domain = domain
- self.selector = selector
-
-
- def makeKeyData(self, domain, selector, pubkeyfile):
- """
- Check that a valid key exists, create the TXT record format data and make the needed child resources.
- """
-
- # Get data from file
- try:
- with open(pubkeyfile) as f:
- key_data = f.read()
- except IOError, e:
- log.error("DKIM: Unable to open the public key file: %s because of %s" % (pubkeyfile, e,))
- raise
-
- # Make sure we can parse a valid public key
- try:
- RSA.importKey(key_data)
- except:
- log.error("DKIM: Invalid public key file: %s" % (pubkeyfile,))
- raise
-
- # Make the TXT record
- key_data = "".join(key_data.strip().splitlines()[1:-1])
- txt_data = "v=DKIM1; s=ischedule; p=%s\n" % (key_data,)
-
- # Setup resource hierarchy
- domainResource = SimpleResource(principalCollections=None, isdir=True, defaultACL=SimpleResource.allReadACL)
- self.putChild(domain, domainResource)
-
- selectorResource = SimpleDataResource(principalCollections=None, content_type=MimeType.fromString("text/plain"), data=txt_data, defaultACL=SimpleResource.allReadACL)
- domainResource.putChild(selector, selectorResource)
-
-
- def contentType(self):
- return MimeType.fromString("text/html; charset=utf-8")
-
-
- def render(self, request):
- output = """<html>
-<head>
-<title>DomainKey Resource</title>
-</head>
-<body>
-<h1>DomainKey Resource.</h1>
-<a href="%s">Domain: %s<br>
-Selector: %s</a>
-</body
-</html>""" % (joinURL(request.uri, self.domain, self.selector), self.domain, self.selector,)
-
- response = Response(200, {}, output)
- response.headers.setHeader("content-type", MimeType("text", "html"))
- return response
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/dkim.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/dkim.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/dkim.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/dkim.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,981 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.python.log import Logger
+from twext.web2.client.http import ClientRequest
+from twext.web2.dav.util import allDataFromStream, joinURL
+from twext.web2.http import Response
+from twext.web2.http_headers import MimeType
+from twext.web2.stream import MemoryStream
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twistedcaldav.client.geturl import getURL
+from twistedcaldav.config import ConfigurationError
+from twistedcaldav.simpleresource import SimpleResource, SimpleDataResource
+from twistedcaldav.scheduling.ischedule.utils import lookupDataViaTXT, \
+ lookupServerViaSRV
+
+from Crypto.Hash import SHA, SHA256
+from Crypto.PublicKey import RSA
+from Crypto.Signature import PKCS1_v1_5
+
+import base64
+import binascii
+import collections
+import hashlib
+import os
+import textwrap
+import time
+import uuid
+
+"""
+DKIM HTTP message generation and validation,
+"""
+
+log = Logger()
+
+# DKIM/iSchedule Constants
+RSA1 = "rsa-sha1"
+RSA256 = "rsa-sha256"
+Q_DNS = "dns/txt"
+Q_HTTP = "http/well-known"
+Q_PRIVATE = "private-exchange"
+
+KEY_SERVICE_TYPE = "ischedule"
+
+# Headers
+DKIM_SIGNATURE = "DKIM-Signature"
+ISCHEDULE_VERSION = "iSchedule-Version"
+ISCHEDULE_VERSION_VALUE = "1.0"
+ISCHEDULE_MESSAGE_ID = "iSchedule-Message-ID"
+
+
+
+class DKIMUtils(object):
+ """
+ Some useful functions.
+ """
+
+ @staticmethod
+ def validConfiguration(config):
+ if config.Scheduling.iSchedule.DKIM.Enabled:
+
+ if not config.Scheduling.iSchedule.DKIM.Domain and not config.ServerHostName:
+ msg = "DKIM: No domain specified"
+ log.error(msg)
+ raise ConfigurationError(msg)
+
+ if not config.Scheduling.iSchedule.DKIM.KeySelector:
+ msg = "DKIM: No selector specified"
+ log.error(msg)
+ raise ConfigurationError(msg)
+
+ if config.Scheduling.iSchedule.DKIM.SignatureAlgorithm not in (RSA1, RSA256):
+ msg = "DKIM: Invalid algorithm: %s" % (config.Scheduling.iSchedule.SignatureAlgorithm,)
+ log.error(msg)
+ raise ConfigurationError(msg)
+
+ try:
+ with open(config.Scheduling.iSchedule.DKIM.PrivateKeyFile) as f:
+ key_data = f.read()
+ except IOError, e:
+ msg = "DKIM: Cannot read private key file: %s %s" % (config.Scheduling.iSchedule.DKIM.PrivateKeyFile, e,)
+ log.error(msg)
+ raise ConfigurationError(msg)
+ try:
+ RSA.importKey(key_data)
+ except:
+ msg = "DKIM: Invalid private key file: %s" % (config.Scheduling.iSchedule.DKIM.PrivateKeyFile,)
+ log.error(msg)
+ raise ConfigurationError(msg)
+
+ try:
+ with open(config.Scheduling.iSchedule.DKIM.PublicKeyFile) as f:
+ key_data = f.read()
+ except IOError, e:
+ msg = "DKIM: Cannot read public key file: %s %s" % (config.Scheduling.iSchedule.DKIM.PublicKeyFile, e,)
+ log.error(msg)
+ raise ConfigurationError(msg)
+ try:
+ RSA.importKey(key_data)
+ except:
+ msg = "DKIM: Invalid public key file: %s" % (config.Scheduling.iSchedule.DKIM.PublicKeyFile,)
+ log.error(msg)
+ raise ConfigurationError(msg)
+
+ if config.Scheduling.iSchedule.DKIM.PrivateExchanges:
+ if not os.path.exists(config.Scheduling.iSchedule.DKIM.PrivateExchanges):
+ try:
+ os.makedirs(config.Scheduling.iSchedule.DKIM.PrivateExchanges)
+ except IOError, e:
+ msg = "DKIM: Cannot create public key private exchange directory: %s" % (config.Scheduling.iSchedule.DKIM.PrivateExchanges,)
+ log.error(msg)
+ raise ConfigurationError(msg)
+ if not os.path.isdir(config.Scheduling.iSchedule.DKIM.PrivateExchanges):
+ msg = "DKIM: Invalid public key private exchange directory: %s" % (config.Scheduling.iSchedule.DKIM.PrivateExchanges,)
+ log.error(msg)
+ raise ConfigurationError(msg)
+ PublicKeyLookup_PrivateExchange.directory = config.Scheduling.iSchedule.DKIM.PrivateExchanges
+
+ log.info("DKIM: Enabled")
+ else:
+ log.info("DKIM: Disabled")
+
+
+ @staticmethod
+ def getConfiguration(config):
+ """
+ Return a tuple of the parameters derived from the config that are used to initialize the DKIMRequest.
+
+ @param config: configuration to look at
+ @type config: L{Config}
+ """
+
+ domain = config.Scheduling.iSchedule.DKIM.Domain if config.Scheduling.iSchedule.DKIM.Domain else config.ServerHostName
+ selector = config.Scheduling.iSchedule.DKIM.KeySelector
+ key_file = config.Scheduling.iSchedule.DKIM.PrivateKeyFile
+ algorithm = config.Scheduling.iSchedule.DKIM.SignatureAlgorithm
+ useDNSKey = config.Scheduling.iSchedule.DKIM.UseDNSKey
+ useHTTPKey = config.Scheduling.iSchedule.DKIM.UseHTTPKey
+ usePrivateExchangeKey = config.Scheduling.iSchedule.DKIM.UsePrivateExchangeKey
+ expire = config.Scheduling.iSchedule.DKIM.ExpireSeconds
+
+ return domain, selector, key_file, algorithm, useDNSKey, useHTTPKey, usePrivateExchangeKey, expire
+
+
+ @staticmethod
+ def hashlib_method(algorithm):
+ """
+ Return hashlib function for DKIM algorithm.
+ """
+ return {
+ RSA1 : hashlib.sha1,
+ RSA256: hashlib.sha256,
+ }[algorithm]
+
+
+ @staticmethod
+ def hash_name(algorithm):
+ """
+ Return RSA hash name for DKIM algorithm.
+ """
+ return {
+ RSA1 : "SHA-1",
+ RSA256: "SHA-256",
+ }[algorithm]
+
+
+ @staticmethod
+ def hash_func(algorithm):
+ """
+ Return RSA hash name for DKIM algorithm.
+ """
+ return {
+ RSA1 : SHA,
+ RSA256: SHA256,
+ }[algorithm]
+
+
+ @staticmethod
+ def extractTags(data):
+ """
+ Split a DKIM tag list into a dict, removing unneeded whitespace.
+ """
+ # Extract tags from the data
+ splits = [item.strip() for item in data.split(";")]
+ dkim_tags = {}
+ for item in splits:
+ try:
+ name, value = item.split("=", 1)
+ dkim_tags[name.strip()] = value.strip()
+ except ValueError:
+ pass
+ return dkim_tags
+
+
+ @staticmethod
+ def canonicalizeHeader(name, value, remove_b=None):
+ """
+ Canonicalize the header using "relaxed" method. Optionally remove the b= value from
+ any DKIM-Signature present.
+
+ FIXME: this needs to be smarter about where valid WSP can occur in a header. Right now it will
+ blindly collapse all runs of SP/HTAB into a single SP. That could be wrong if a legitimate sequence of
+ SP/HTAB occurs in a header value.
+
+ @param name: header name
+ @type name: C{str}
+ @param value: header value
+ @type value: C{str}
+ @param remove_b: the b= value to remove, or C{None} if no removal needed
+ @type remove_b: C{str} or C{None}
+ """
+
+ # Basic relaxed behavior
+ name = name.lower()
+ value = " ".join(value.split())
+
+ # Special case DKIM-Signature: remove the b= value for signature
+ if remove_b is not None and name == DKIM_SIGNATURE.lower():
+ pos = value.find(remove_b)
+ value = value[:pos] + value[pos + len(remove_b):]
+ value = " ".join(value.split())
+
+ crlf = "" if name == DKIM_SIGNATURE.lower() else "\r\n"
+ return "%s:%s%s" % (name, value, crlf)
+
+
+ @staticmethod
+ def canonicalizeBody(data):
+ if not data.endswith("\r\n"):
+ data += "\r\n"
+ return data
+
+
+ @staticmethod
+ def sign(data, privkey, hashfunc):
+ h = hashfunc.new(data)
+ signer = PKCS1_v1_5.new(privkey)
+ return base64.b64encode(signer.sign(h))
+
+
+ @staticmethod
+ def verify(data, signature, pubkey, hashfunc):
+ h = hashfunc.new(data)
+ verifier = PKCS1_v1_5.new(pubkey)
+ if not verifier.verify(h, base64.b64decode(signature)):
+ raise ValueError()
+
+
+
+class DKIMRequest(ClientRequest):
+ """
+ A ClientRequest that optionally creates a DKIM signature.
+ """
+
+ keys = {}
+
+ def __init__(
+ self,
+ method,
+ uri,
+ headers,
+ stream,
+ domain,
+ selector,
+ key_file,
+ algorithm,
+ sign_headers,
+ useDNSKey,
+ useHTTPKey,
+ usePrivateExchangeKey,
+ expire,
+ ):
+ """
+ Create a DKIM request, which is a regular client request with the additional information needed to sign the message.
+
+ @param method: HTTP method to use
+ @type method: C{str}
+ @param uri: request-URI
+ @type uri: C{str}
+ @param headers: request headers
+ @type headers: L{http_headers}
+ @param stream: body data
+ @type stream: L{Stream}
+ @param domain: the signing domain
+ @type domain: C{str}
+ @param selector: the signing key selector
+ @type selector: C{str}
+ @param key_file: path to a private key file
+ @type key_file: C{str}
+ @param algorithm: the signing algorithm to use
+ @type algorithm: C{str}
+ @param sign_headers: list of header names to sign - to "over sign" a header append a "+" to the name
+ @type sign_headers: C{tuple}
+ @param useDNSKey: whether or not to add DNS TXT lookup as a key lookup option
+ @type useDNSKey: C{bool}
+ @param useHTTPKey: whether or not to add HTTP .well-known as a key lookup option
+ @type useHTTPKey: C{bool}
+ @param usePrivateExchangeKey: whether or not to add private-exchange as a key lookup option
+ @type usePrivateExchangeKey: C{bool}
+ @param expire: number of seconds to expiration of signature
+ @type expire: C{int}
+ """
+ super(DKIMRequest, self).__init__(method, uri, headers, stream)
+ self.domain = domain
+ self.selector = selector
+ self.algorithm = algorithm
+ self.key_file = key_file
+ self.sign_headers = sign_headers
+ self.time = str(int(time.time()))
+ self.expire = str(int(time.time() + expire))
+
+ assert self.domain
+ assert self.selector
+ assert self.algorithm in (RSA1, RSA256,)
+ assert useDNSKey or useHTTPKey or usePrivateExchangeKey
+
+ self.hash_method = DKIMUtils.hashlib_method(self.algorithm)
+ self.hash_name = DKIMUtils.hash_name(self.algorithm)
+ self.hash_func = DKIMUtils.hash_func(self.algorithm)
+
+ self.keyMethods = []
+ if usePrivateExchangeKey:
+ self.keyMethods.append(Q_PRIVATE)
+ if useHTTPKey:
+ self.keyMethods.append(Q_HTTP)
+ if useDNSKey:
+ self.keyMethods.append(Q_DNS)
+
+ self.message_id = str(uuid.uuid4())
+
+
+ @inlineCallbacks
+ def sign(self):
+ """
+ Generate the DKIM headers by signing the request. This should only be called once on the request and there must
+ be no changes to the request (no headers, no body change) after it is called.
+ """
+
+ # Get the headers and the DKIM-Signature tags
+ headers, dkim_tags = (yield self.signatureHeaders())
+
+ # Sign the hash
+ signature = self.generateSignature(headers)
+
+ # Complete the header
+ dkim_tags[-1] = ("b", signature,)
+ dkim_header = "; ".join(["%s=%s" % item for item in dkim_tags])
+ self.headers.addRawHeader(DKIM_SIGNATURE, dkim_header)
+
+ log.debug("DKIM: Generated header: DKIM-Signature:%s" % (dkim_header,))
+ log.debug("DKIM: Signed headers:\n%s" % (headers,))
+
+ returnValue(signature)
+
+
+ @inlineCallbacks
+ def bodyHash(self):
+ """
+ Generate the hash of the request body data.
+ """
+
+ # We need to play a trick with the request stream as we can only read it once. So we
+ # read it, store the value in a MemoryStream, and replace the request's stream with that,
+ # so the data can be read again.
+ data = (yield allDataFromStream(self.stream))
+ self.stream = MemoryStream(data if data is not None else "")
+ self.stream.doStartReading = None
+
+ returnValue(base64.b64encode(self.hash_method(DKIMUtils.canonicalizeBody(data)).digest()))
+
+
+ @inlineCallbacks
+ def signatureHeaders(self):
+ """
+ Generate the headers that are going to be signed as well as the DKIM-Signature tags.
+ """
+
+ # Make sure we have the required iSchedule headers
+ self.headers.addRawHeader(ISCHEDULE_VERSION, ISCHEDULE_VERSION_VALUE)
+ self.headers.addRawHeader(ISCHEDULE_MESSAGE_ID, self.message_id)
+ self.sign_headers += (ISCHEDULE_VERSION, ISCHEDULE_MESSAGE_ID,)
+
+ # Need Cache-Control
+ self.headers.setRawHeaders("Cache-Control", ("no-cache", "no-transform",))
+
+ # Figure out all the existing headers to sign
+ headers = []
+ sign_headers = []
+ raw = dict([(name.lower(), values) for name, values in self.headers.getAllRawHeaders()])
+ for name in self.sign_headers:
+ oversign = name[-1] == "+"
+ name = name.rstrip("+")
+ for value in reversed(raw.get(name.lower(), ())):
+ headers.append(DKIMUtils.canonicalizeHeader(name, value))
+ sign_headers.append(name)
+ if oversign:
+ sign_headers.append(name)
+
+ # Generate the DKIM header tags we care about
+ dkim_tags = []
+ dkim_tags.append(("v", "1",))
+ dkim_tags.append(("d", self.domain,))
+ dkim_tags.append(("s", self.selector,))
+ dkim_tags.append(("t", self.time,))
+ dkim_tags.append(("x", self.expire,))
+ dkim_tags.append(("a", self.algorithm,))
+ dkim_tags.append(("q", ":".join(self.keyMethods),))
+ dkim_tags.append(("http", base64.encodestring("%s:%s" % (self.method, self.uri,)).strip()))
+ dkim_tags.append(("c", "relaxed/simple",))
+ dkim_tags.append(("h", ":".join(sign_headers),))
+ dkim_tags.append(("bh", (yield self.bodyHash()),))
+ dkim_tags.append(("b", "",))
+ dkim_header = "; ".join(["%s=%s" % item for item in dkim_tags])
+
+ headers.append(DKIMUtils.canonicalizeHeader(DKIM_SIGNATURE, dkim_header))
+ headers = "".join(headers)
+
+ returnValue((headers, dkim_tags,))
+
+
+ def generateSignature(self, headers):
+ # Sign the hash
+ if self.key_file not in self.keys:
+ self.keys[self.key_file] = RSA.importKey(open(self.key_file).read())
+ return DKIMUtils.sign(headers, self.keys[self.key_file], self.hash_func)
+
+
+
+class DKIMMissingError(Exception):
+ """
+ Used to indicate that the DKIM-Signature header is not present when
+ attempting verification.
+ """
+ pass
+
+
+
+class DKIMVerificationError(Exception):
+ """
+ Used to indicate a DKIM verification error.
+ """
+ pass
+
+
+
+class DKIMVerifier(object):
+ """
+ Class used to verify an DKIM-signed HTTP request.
+ """
+
+ def __init__(self, request, key_lookup=None, protocol_debug=False):
+ """
+ @param request: The HTTP request to process
+ @type request: L{twext.server.Request}
+ """
+ self.request = request
+ self._debug = protocol_debug
+ self.dkim_tags = {}
+
+ # Prefer private exchange over HTTP over DNS when multiple are present
+ self.key_lookup_methods = (
+ PublicKeyLookup_PrivateExchange,
+ PublicKeyLookup_HTTP_WellKnown,
+ PublicKeyLookup_DNSTXT,
+ ) if key_lookup is None else key_lookup
+
+ self.time = int(time.time())
+
+
+ @inlineCallbacks
+ def verify(self):
+ """
+ @raise: DKIMVerificationError
+ """
+
+ # Check presence of DKIM header
+ self.processDKIMHeader()
+
+ # Extract the set of canonicalized headers being signed
+ headers = self.extractSignedHeaders()
+ log.debug("DKIM: Signed headers:\n%s" % (headers,))
+
+ # Locate the public key
+ pubkey = (yield self.locatePublicKey())
+ if pubkey is None:
+ raise DKIMVerificationError("No public key to verify the DKIM signature")
+
+ # Do header verification
+ try:
+ DKIMUtils.verify(headers, self.dkim_tags["b"], pubkey, self.hash_func)
+ except ValueError:
+ msg = "Could not verify signature"
+ _debug_msg = """
+DKIM-Signature:%s
+
+Headers to evaluate:
+%s
+
+Public key used:
+%s
+""" % (self.request.headers.getRawHeaders(DKIM_SIGNATURE)[0], headers, pubkey._original_data,)
+ log.debug("DKIM: %s:%s" % (msg, _debug_msg,))
+ if self._debug:
+ msg = "%s:%s" % (msg, _debug_msg,)
+ raise DKIMVerificationError(msg)
+
+ # Do body validation
+ data = (yield allDataFromStream(self.request.stream))
+ self.request.stream = MemoryStream(data if data is not None else "")
+ self.request.stream.doStartReading = None
+ body = DKIMUtils.canonicalizeBody(data)
+ bh = base64.b64encode(self.hash_method(body).digest())
+ if bh != self.dkim_tags["bh"]:
+ msg = "Could not verify the DKIM body hash"
+ _debug_msg = """
+DKIM-Signature:%s
+
+Hash Method: %s
+
+Base64 encoded body:
+%s
+""" % (self.request.headers.getRawHeaders(DKIM_SIGNATURE), self.hash_method.__name__, base64.b64encode(body),)
+ log.debug("DKIM: %s:%s" % (msg, _debug_msg,))
+ if self._debug:
+ msg = "%s:%s" % (msg, _debug_msg,)
+ raise DKIMVerificationError(msg)
+
+
+ def processDKIMHeader(self):
+ """
+ Extract the DKIM-Signature header and process the tags.
+
+ @raise: DKIMVerificationError
+ """
+
+ # Check presence of header
+ dkim = self.request.headers.getRawHeaders(DKIM_SIGNATURE)
+ if dkim is None:
+ msg = "No DKIM-Signature header present in the request"
+ log.debug("DKIM: " + msg)
+ raise DKIMMissingError(msg)
+ if len(dkim) != 1:
+ # TODO: This might need to be changed if we ever support forwarding of iSchedule messages - the forwarder
+ # might also sign the message and add its own header
+ msg = "Only one DKIM-Signature allowed in the request"
+ log.debug("DKIM: " + msg)
+ raise DKIMVerificationError(msg)
+ dkim = dkim[0]
+ log.debug("DKIM: Found header: DKIM-Signature:%s" % (dkim,))
+
+ # Extract tags from the header
+ self.dkim_tags = DKIMUtils.extractTags(dkim)
+
+ # Verify validity of tags
+ required_tags = ("v", "a", "b", "bh", "c", "d", "h", "s", "http",)
+ for tag in required_tags:
+ if tag not in self.dkim_tags:
+ msg = "Missing DKIM-Signature tag: %s" % (tag,)
+ log.debug("DKIM: " + msg)
+ raise DKIMVerificationError(msg)
+
+ check_values = {
+ "v": ("1",),
+ "a": (RSA1, RSA256,),
+ "c": ("relaxed", "relaxed/simple",),
+ "q": (Q_DNS, Q_HTTP, Q_PRIVATE,),
+ }
+ for tag, values in check_values.items():
+ if tag not in required_tags and tag not in self.dkim_tags:
+ pass
+
+ # Handle some structured values
+ if tag == "q":
+ test = self.dkim_tags[tag].split(":")
+ else:
+ test = (self.dkim_tags[tag],)
+ for item in test:
+ if item not in values:
+ msg = "Tag: %s has incorrect value: %s" % (tag, self.dkim_tags[tag],)
+ log.debug("DKIM: " + msg)
+ raise DKIMVerificationError(msg)
+
+ # Check expiration
+ if "x" in self.dkim_tags:
+ diff_time = self.time - int(self.dkim_tags["x"])
+ if diff_time > 0:
+ msg = "Signature expired: %d seconds" % (diff_time,)
+ log.debug("DKIM: " + msg)
+ raise DKIMVerificationError(msg)
+
+ # Check HTTP method/request-uri
+ try:
+ http_tag = base64.decodestring(self.dkim_tags["http"])
+ except binascii.Error:
+ msg = "Tag: http is not valid base64"
+ log.debug("DKIM: " + msg)
+ raise DKIMVerificationError(msg)
+ try:
+ method, uri = http_tag.split(":", 1)
+ except ValueError:
+ msg = "Tag: base64-decoded http is not valid: %s" % (http_tag,)
+ log.debug("DKIM: " + msg)
+ raise DKIMVerificationError(msg)
+ if method != self.request.method:
+ msg = "Tag: http method does not match: %s" % (method,)
+ log.debug("DKIM: " + msg)
+ raise DKIMVerificationError(msg)
+ if uri != self.request.uri:
+ msg = "Tag: http request-URI does not match: %s" % (uri,)
+ log.debug("DKIM: " + msg)
+ raise DKIMVerificationError(msg)
+
+ # Some useful bits
+ self.hash_method = DKIMUtils.hashlib_method(self.dkim_tags["a"])
+ self.hash_func = DKIMUtils.hash_func(self.dkim_tags["a"])
+ self.key_methods = self.dkim_tags["q"].split(":")
+
+
+ def extractSignedHeaders(self):
+ """
+ Extract the set of headers from the request that are supposed to be signed. Canonicalize them
+ and return the expected signed data.
+ """
+
+ # Extract all the expected signed headers taking into account the possibility of "over_counting"
+ # headers - a technique used to ensure headers cannot be added in transit
+ header_list = [hdr.strip() for hdr in self.dkim_tags["h"].split(":")]
+ header_counter = collections.defaultdict(int)
+
+ headers = []
+ for header in header_list:
+ actual_headers = self.request.headers.getRawHeaders(header)
+ if actual_headers:
+ try:
+ headers.append((header, actual_headers[-1 - header_counter[header]],))
+ except IndexError:
+ pass
+ header_counter[header] += 1
+
+ # DKIM-Signature is always included at the end
+ headers.append((DKIM_SIGNATURE, self.request.headers.getRawHeaders(DKIM_SIGNATURE)[0],))
+
+ # Now canonicalize the values
+ return "".join([DKIMUtils.canonicalizeHeader(name, value, remove_b=self.dkim_tags["b"]) for name, value in headers])
+
+
+ @inlineCallbacks
+ def locatePublicKey(self):
+ """
+ Try to lookup the public key matching the signature.
+ """
+
+ for lookup in self.key_lookup_methods:
+ if lookup.method in self.key_methods or lookup.method == "*":
+ pubkey = (yield lookup(self.dkim_tags).getPublicKey())
+ if pubkey is not None:
+ returnValue(pubkey)
+ else:
+ returnValue(None)
+
+
+
+class PublicKeyLookup(object):
+ """
+ Abstract base class for public key lookup methods.
+
+ The L{method} attribute indicated the DKIM q= lookup method that the class will support, or if set to "*",
+ the class will handle any q= value.
+ """
+
+ keyCache = {}
+ method = None
+
+ def __init__(self, dkim_tags):
+ self.dkim_tags = dkim_tags
+
+
+ @inlineCallbacks
+ def getPublicKey(self, useCache=True):
+ """
+ Get key from cache or directly do query.
+
+ @param useCache: whether or not to use the cache
+ @type useCache: C{bool}
+ """
+ key = self._getSelectorKey()
+ if key not in PublicKeyLookup.keyCache or not useCache:
+ pubkeys = (yield self._lookupKeys())
+ PublicKeyLookup.keyCache[key] = pubkeys
+
+ returnValue(self._selectKey())
+
+
+ def _getSelectorKey(self):
+ """
+ Get a token used to uniquely identify the key being looked up. Token format will
+ depend on the lookup method.
+ """
+ raise NotImplementedError
+
+
+ def _lookupKeys(self):
+ """
+ Do the key lookup using the actual lookup method. Return a C{list} of C{dict}
+ that contains the key tag-list. Return a L{Deferred}.
+ """
+ raise NotImplementedError
+
+
+ def _selectKey(self):
+ """
+ Select a specific key from the list that best matches the DKIM-Signature tags
+ """
+
+ pubkeys = PublicKeyLookup.keyCache.get(self._getSelectorKey(), [])
+ for pkey in pubkeys:
+ # Check validity
+ if pkey.get("v", "DKIM1") != "DKIM1":
+ continue
+
+ # Check key type
+ if pkey.get("k", "rsa") != "rsa":
+ continue
+
+ # Check valid hash algorithms
+ hashes = set([hash.strip() for hash in pkey.get("h", "sha1:sha256").split(":")])
+ if self.dkim_tags["a"][4:] not in hashes:
+ continue
+
+ # Service type
+ if pkey.get("s", KEY_SERVICE_TYPE) not in ("*", KEY_SERVICE_TYPE,):
+ continue
+
+ # Non-revoked key
+ if len(pkey.get("p", "")) == 0:
+ continue
+
+ return self._makeKey(pkey)
+
+ log.debug("DKIM: No valid public key: %s %s" % (self._getSelectorKey(), pubkeys,))
+ return None
+
+
+ def _makeKey(self, pkey):
+ """
+ Turn the key tag list into an actual RSA public key object
+
+ @param pkey: key tag list
+ @type pkey: C{list}
+ """
+ key_data = """-----BEGIN PUBLIC KEY-----
+%s
+-----END PUBLIC KEY-----
+""" % ("\n".join(textwrap.wrap(pkey["p"], 64)),)
+
+ try:
+ key = RSA.importKey(key_data)
+ key._original_data = key_data
+ return key
+ except:
+ log.debug("DKIM: Unable to make public key:\n%s" % (key_data,))
+ return None
+
+
+ def flushCache(self):
+ PublicKeyLookup.keyCache = {}
+
+
+
+class PublicKeyLookup_DNSTXT(PublicKeyLookup):
+
+ method = Q_DNS
+
+ def _getSelectorKey(self):
+ """
+ Get a token used to uniquely identify the key being looked up. Token format will
+ depend on the lookup method.
+ """
+ return "%s._domainkey.%s" % (self.dkim_tags["s"], self.dkim_tags["d"],)
+
+
+ @inlineCallbacks
+ def _lookupKeys(self):
+ """
+ Do the key lookup using the actual lookup method.
+ """
+ log.debug("DKIM: TXT lookup: %s" % (self._getSelectorKey(),))
+ data = (yield lookupDataViaTXT(self._getSelectorKey()))
+ log.debug("DKIM: TXT lookup results: %s\n%s" % (self._getSelectorKey(), "\n".join(data),))
+ returnValue(tuple([DKIMUtils.extractTags(line) for line in data]))
+
+
+
+class PublicKeyLookup_HTTP_WellKnown(PublicKeyLookup):
+
+ method = Q_HTTP
+
+ def _getSelectorKey(self):
+ """
+ Get a token used to uniquely identify the key being looked up. Token format will
+ depend on the lookup method.
+ """
+
+ host = ".".join(self.dkim_tags["d"].split(".")[-2:])
+ return "https://%s/.well-known/domainkey/%s/%s" % (host, self.dkim_tags["d"], self.dkim_tags["s"],)
+
+
+ @inlineCallbacks
+ def _getURI(self):
+ """
+ Determine the well-known URI for the public key service.
+ """
+
+ # First we do an SRV lookup for _domainkey to get the public key server host/port
+ result = (yield lookupServerViaSRV(self.dkim_tags["d"], service="_domainkey"))
+ if result is None:
+ log.debug("DKIM: SRV _domainkey failed on: %s trying domain directly" % (self.dkim_tags["d"],))
+ host = self.dkim_tags["d"]
+ port = ""
+ scheme = "https"
+ else:
+ host, port = result
+ scheme = "http" if port in (80, 8008, 8080,) else "https"
+ if port == 80 and scheme == "http" or port == 443 and scheme == "https":
+ port = ""
+ else:
+ port = ":%s" % (port,)
+
+ returnValue("%s://%s%s/.well-known/domainkey/%s/%s" % (scheme, host, port, self.dkim_tags["d"], self.dkim_tags["s"],))
+
+
+ @inlineCallbacks
+ def _lookupKeys(self):
+ """
+ Do the key lookup using the actual lookup method.
+ """
+
+ # First we do an SRV lookup for _domainkey to get the public key server URI
+ uri = (yield self._getURI())
+
+ log.debug("DKIM: HTTP/.well-known lookup: %s" % (uri,))
+ response = (yield getURL(uri))
+ if response is None or response.code / 100 != 2:
+ log.debug("DKIM: Failed http/well-known lookup: %s %s" % (uri, response,))
+ returnValue(())
+
+ ct = response.headers.getRawHeaders("content-type", ("bogus/type",))[0]
+ ct = ct.split(";", 1)
+ ct = ct[0].strip()
+ if ct not in ("text/plain",):
+ log.debug("DKIM: Failed http/well-known lookup: wrong content-type returned %s %s" % (uri, ct,))
+ returnValue(())
+
+ log.debug("DKIM: HTTP/.well-known lookup results: %s\n%s" % (uri, response.data,))
+ returnValue(tuple([DKIMUtils.extractTags(line) for line in response.data.splitlines()]))
+
+
+
+class PublicKeyLookup_PrivateExchange(PublicKeyLookup):
+
+ method = Q_PRIVATE
+ directory = None
+
+ def _getSelectorKey(self):
+ """
+ Get a token used to uniquely identify the key being looked up. Token format will
+ depend on the lookup method.
+ """
+ return "%s#%s" % (self.dkim_tags["d"], self.dkim_tags["s"],)
+
+
+ def _lookupKeys(self):
+ """
+ Key information is stored in a file, one record per line.
+ """
+
+ # Check validity of paths
+ if PublicKeyLookup_PrivateExchange.directory is None:
+ log.debug("DKIM: Failed private-exchange lookup: no directory configured")
+ return succeed(())
+ keyfile = os.path.join(PublicKeyLookup_PrivateExchange.directory, self._getSelectorKey())
+ if not os.path.exists(keyfile):
+ log.debug("DKIM: Failed private-exchange lookup: no path %s" % (keyfile,))
+ return succeed(())
+
+ # Now read the data
+ log.debug("DKIM: Private exchange lookup: %s" % (keyfile,))
+ try:
+ with open(keyfile) as f:
+ keys = f.read()
+ except IOError, e:
+ log.debug("DKIM: Failed private-exchange lookup: could not read %s %s" % (keyfile, e,))
+ return succeed(())
+
+ log.debug("DKIM: Private exchange lookup results: %s\n%s" % (keyfile, keys))
+ return succeed(tuple([DKIMUtils.extractTags(line) for line in keys.splitlines()]))
+
+
+
+class DomainKeyResource (SimpleResource):
+ """
+ Domainkey well-known resource.
+ """
+
+ def __init__(self, domain, selector, pubkeyfile):
+ """
+ """
+ assert domain
+ assert selector
+
+ SimpleResource.__init__(self, principalCollections=None, isdir=True, defaultACL=SimpleResource.allReadACL)
+ self.makeKeyData(domain, selector, pubkeyfile)
+ self.domain = domain
+ self.selector = selector
+
+
+ def makeKeyData(self, domain, selector, pubkeyfile):
+ """
+ Check that a valid key exists, create the TXT record format data and make the needed child resources.
+ """
+
+ # Get data from file
+ try:
+ with open(pubkeyfile) as f:
+ key_data = f.read()
+ except IOError, e:
+ log.error("DKIM: Unable to open the public key file: %s because of %s" % (pubkeyfile, e,))
+ raise
+
+ # Make sure we can parse a valid public key
+ try:
+ RSA.importKey(key_data)
+ except:
+ log.error("DKIM: Invalid public key file: %s" % (pubkeyfile,))
+ raise
+
+ # Make the TXT record
+ key_data = "".join(key_data.strip().splitlines()[1:-1])
+ txt_data = "v=DKIM1; s=ischedule; p=%s\n" % (key_data,)
+
+ # Setup resource hierarchy
+ domainResource = SimpleResource(principalCollections=None, isdir=True, defaultACL=SimpleResource.allReadACL)
+ self.putChild(domain, domainResource)
+
+ selectorResource = SimpleDataResource(principalCollections=None, content_type=MimeType.fromString("text/plain"), data=txt_data, defaultACL=SimpleResource.allReadACL)
+ domainResource.putChild(selector, selectorResource)
+
+
+ def contentType(self):
+ return MimeType.fromString("text/html; charset=utf-8")
+
+
+ def render(self, request):
+ output = """<html>
+<head>
+<title>DomainKey Resource</title>
+</head>
+<body>
+<h1>DomainKey Resource.</h1>
+<a href="%s">Domain: %s<br>
+Selector: %s</a>
+</body
+</html>""" % (joinURL(request.uri, self.domain, self.selector), self.domain, self.selector,)
+
+ response = Response(200, {}, output)
+ response.headers.setHeader("content-type", MimeType("text", "html"))
+ return response
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/localservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/localservers.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/localservers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,323 +0,0 @@
-##
-# Copyright (c) 2011-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.python.log import Logger
-from twisted.internet.abstract import isIPAddress
-from twistedcaldav.client.pool import installPool
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.xmlutil import readXML
-import socket
-import urlparse
-from twistedcaldav.scheduling.ischedule.utils import getIPsFromHost
-
-"""
-XML based server configuration file handling.
-
-This is used in an environment where more than one server is being used within a single domain. i.e., all
-the principals across the whole domain need to be able to directly schedule each other and know of each others
-existence. A common scenario would be a production server and a development/test server.
-
-Each server is identified by an id and url. The id is used when assigning principals to a specific server. Each
-server can also support multiple partitions, and each of those is identified by an id and url, with the id also
-being used to assign principals to a specific partition.
-
-These servers support the concept of "partitioning" and "podding".
-
-A "partitioned" service is one that spreads its
-users out across multiple stores and does reverse proxying of incoming requests to the appropriate partitioned host.
-All servers within the same partition have to be running the same version of the software etc.
-
-A "podded" service is one where different groups of users are hosted on different servers, which may be of
-different versions etc. A "pod" may itself be "partitioned", but the partitioning is "invisible" to the outside world.
-"""
-
-__all__ = [
- "Servers",
-]
-
-log = Logger()
-
-SERVER_SECRET_HEADER = "X-CALENDARSERVER-ISCHEDULE"
-
-class ServersDB(object):
- """
- Represents the set of servers within the same domain.
- """
-
- def __init__(self):
-
- self._servers = {}
- self._xmlFile = None
- self._thisServer = None
-
-
- def load(self, xmlFile=None, ignoreIPLookupFailures=False):
- if self._xmlFile is None or xmlFile is not None:
- self._servers = {}
- if xmlFile:
- self._xmlFile = xmlFile
- else:
- self._xmlFile = fullServerPath(
- config.ConfigRoot,
- config.Servers.ConfigFile
- )
- self._servers = ServersParser.parse(self._xmlFile, ignoreIPLookupFailures=ignoreIPLookupFailures)
- for server in self._servers.values():
- if server.thisServer:
- self._thisServer = server
- break
- else:
- raise ValueError("No server in %s matches this server." % (self._xmlFile,))
-
-
- def clear(self):
- self._servers = {}
- self._xmlFile = None
- self._thisServer = None
-
-
- def getServerById(self, id):
- return self._servers.get(id)
-
-
- def getServerURIById(self, id):
- try:
- return self._servers[id].uri
- except KeyError:
- return None
-
-
- def getThisServer(self):
- return self._thisServer
-
-Servers = ServersDB() # Global server DB
-
-
-
-class Server(object):
- """
- Represents a server which may itself be partitioned.
- """
-
- def __init__(self):
- self.id = None
- self.uri = None
- self.thisServer = False
- self.ips = set()
- self.allowed_from_ips = set()
- self.shared_secret = None
- self.partitions = {}
- self.partitions_ips = set()
- self.isImplicit = True
-
-
- def check(self, ignoreIPLookupFailures=False):
- # Check whether this matches the current server
- parsed_uri = urlparse.urlparse(self.uri)
- if parsed_uri.hostname == config.ServerHostName:
- if parsed_uri.scheme == "http":
- if config.HTTPPort:
- self.thisServer = parsed_uri.port in (config.HTTPPort,) + tuple(config.BindHTTPPorts)
- elif parsed_uri.scheme == "https":
- if config.SSLPort:
- self.thisServer = parsed_uri.port in (config.SSLPort,) + tuple(config.BindSSLPorts)
-
- # Need to cache IP addresses
- try:
- ips = getIPsFromHost(parsed_uri.hostname)
- except socket.gaierror, e:
- msg = "Unable to lookup ip-addr for server '%s': %s" % (parsed_uri.hostname, str(e))
- log.error(msg)
- if ignoreIPLookupFailures:
- ips = ()
- else:
- raise ValueError(msg)
- self.ips = set(ips)
-
- actual_ips = set()
- for item in self.allowed_from_ips:
- if not isIPAddress(item):
- try:
- ips = getIPsFromHost(item)
- except socket.gaierror, e:
- msg = "Unable to lookup ip-addr for allowed-from '%s': %s" % (item, str(e))
- log.error(msg)
- if not ignoreIPLookupFailures:
- raise ValueError(msg)
- else:
- actual_ips.update(ips)
- else:
- actual_ips.add(item)
- self.allowed_from_ips = actual_ips
-
- for uri in self.partitions.values():
- parsed_uri = urlparse.urlparse(uri)
- try:
- ips = getIPsFromHost(parsed_uri.hostname)
- except socket.gaierror, e:
- msg = "Unable to lookup ip-addr for partition '%s': %s" % (parsed_uri.hostname, str(e))
- log.error(msg)
- if ignoreIPLookupFailures:
- ips = ()
- else:
- raise ValueError(msg)
- self.partitions_ips.update(ips)
-
-
- def checkThisIP(self, ip):
- """
- Check that the passed in IP address corresponds to this server or one of its partitions.
- """
- return (ip in self.ips) or (ip in self.partitions_ips)
-
-
- def hasAllowedFromIP(self):
- return len(self.allowed_from_ips) > 0
-
-
- def checkAllowedFromIP(self, ip):
- return ip in self.allowed_from_ips
-
-
- def checkSharedSecret(self, request):
-
- # Get header from the request
- request_secret = request.headers.getRawHeaders(SERVER_SECRET_HEADER)
-
- if request_secret is not None and self.shared_secret is None:
- log.error("iSchedule request included unexpected %s header" % (SERVER_SECRET_HEADER,))
- return False
- elif request_secret is None and self.shared_secret is not None:
- log.error("iSchedule request did not include required %s header" % (SERVER_SECRET_HEADER,))
- return False
- elif (request_secret[0] if request_secret else None) != self.shared_secret:
- log.error("iSchedule request %s header did not match" % (SERVER_SECRET_HEADER,))
- return False
- else:
- return True
-
-
- def secretHeader(self):
- """
- Return a tuple of header name, header value
- """
- return (SERVER_SECRET_HEADER, self.shared_secret,)
-
-
- def addPartition(self, id, uri):
- self.partitions[id] = uri
-
-
- def getPartitionURIForId(self, id):
- return self.partitions.get(id)
-
-
- def isPartitioned(self):
- return len(self.partitions) != 0
-
-
- def installReverseProxies(self, ownUID, maxClients):
-
- for partition, url in self.partitions.iteritems():
- if partition != ownUID:
- installPool(
- partition,
- url,
- maxClients,
- )
-
-
-
-ELEMENT_SERVERS = "servers"
-ELEMENT_SERVER = "server"
-ELEMENT_ID = "id"
-ELEMENT_URI = "uri"
-ELEMENT_ALLOWED_FROM = "allowed-from"
-ELEMENT_SHARED_SECRET = "shared-secret"
-ELEMENT_PARTITIONS = "partitions"
-ELEMENT_PARTITION = "partition"
-ATTR_IMPLICIT = "implicit"
-ATTR_VALUE_YES = "yes"
-ATTR_VALUE_NO = "no"
-
-class ServersParser(object):
- """
- Servers configuration file parser.
- """
- @staticmethod
- def parse(xmlFile, ignoreIPLookupFailures=False):
-
- results = {}
-
- # Read in XML
- try:
- _ignore_tree, servers_node = readXML(xmlFile, ELEMENT_SERVERS)
- except ValueError, e:
- log.error("XML parse error for '%s' because: %s" % (xmlFile, e,), raiseException=RuntimeError)
-
- for child in servers_node.getchildren():
-
- if child.tag != ELEMENT_SERVER:
- log.error("Unknown server type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
- server = Server()
- server.isImplicit = child.get(ATTR_IMPLICIT, ATTR_VALUE_YES) == ATTR_VALUE_YES
-
- for node in child.getchildren():
- if node.tag == ELEMENT_ID:
- server.id = node.text
- elif node.tag == ELEMENT_URI:
- server.uri = node.text
- elif node.tag == ELEMENT_ALLOWED_FROM:
- server.allowed_from_ips.add(node.text)
- elif node.tag == ELEMENT_SHARED_SECRET:
- server.shared_secret = node.text
- elif node.tag == ELEMENT_PARTITIONS:
- ServersParser._parsePartition(xmlFile, node, server)
- else:
- log.error("Invalid element '%s' in servers file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
-
- if server.id is None or server.uri is None:
- log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
- server.check(ignoreIPLookupFailures=ignoreIPLookupFailures)
- results[server.id] = server
-
- return results
-
-
- @staticmethod
- def _parsePartition(xmlFile, partitions, server):
-
- for child in partitions.getchildren():
-
- if child.tag != ELEMENT_PARTITION:
- log.error("Unknown partition type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
- id = None
- uri = None
- for node in child.getchildren():
- if node.tag == ELEMENT_ID:
- id = node.text
- elif node.tag == ELEMENT_URI:
- uri = node.text
- else:
- log.error("Invalid element '%s' in augment file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
-
- if id is None or uri is None:
- log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
- server.addPartition(id, uri)
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/localservers.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/localservers.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/localservers.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/localservers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,323 @@
+##
+# Copyright (c) 2011-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.python.log import Logger
+from twisted.internet.abstract import isIPAddress
+from twistedcaldav.client.pool import installPool
+from twistedcaldav.config import config, fullServerPath
+from twistedcaldav.xmlutil import readXML
+import socket
+import urlparse
+from twistedcaldav.scheduling.ischedule.utils import getIPsFromHost
+
+"""
+XML based server configuration file handling.
+
+This is used in an environment where more than one server is being used within a single domain. i.e., all
+the principals across the whole domain need to be able to directly schedule each other and know of each others
+existence. A common scenario would be a production server and a development/test server.
+
+Each server is identified by an id and url. The id is used when assigning principals to a specific server. Each
+server can also support multiple partitions, and each of those is identified by an id and url, with the id also
+being used to assign principals to a specific partition.
+
+These servers support the concept of "partitioning" and "podding".
+
+A "partitioned" service is one that spreads its
+users out across multiple stores and does reverse proxying of incoming requests to the appropriate partitioned host.
+All servers within the same partition have to be running the same version of the software etc.
+
+A "podded" service is one where different groups of users are hosted on different servers, which may be of
+different versions etc. A "pod" may itself be "partitioned", but the partitioning is "invisible" to the outside world.
+"""
+
+__all__ = [
+ "Servers",
+]
+
+log = Logger()
+
+SERVER_SECRET_HEADER = "X-CALENDARSERVER-ISCHEDULE"
+
+class ServersDB(object):
+ """
+ Represents the set of servers within the same domain.
+ """
+
+ def __init__(self):
+
+ self._servers = {}
+ self._xmlFile = None
+ self._thisServer = None
+
+
+ def load(self, xmlFile=None, ignoreIPLookupFailures=False):
+ if self._xmlFile is None or xmlFile is not None:
+ self._servers = {}
+ if xmlFile:
+ self._xmlFile = xmlFile
+ else:
+ self._xmlFile = fullServerPath(
+ config.ConfigRoot,
+ config.Servers.ConfigFile
+ )
+ self._servers = ServersParser.parse(self._xmlFile, ignoreIPLookupFailures=ignoreIPLookupFailures)
+ for server in self._servers.values():
+ if server.thisServer:
+ self._thisServer = server
+ break
+ else:
+ raise ValueError("No server in %s matches this server." % (self._xmlFile,))
+
+
+ def clear(self):
+ self._servers = {}
+ self._xmlFile = None
+ self._thisServer = None
+
+
+ def getServerById(self, id):
+ return self._servers.get(id)
+
+
+ def getServerURIById(self, id):
+ try:
+ return self._servers[id].uri
+ except KeyError:
+ return None
+
+
+ def getThisServer(self):
+ return self._thisServer
+
+Servers = ServersDB() # Global server DB
+
+
+
+class Server(object):
+ """
+ Represents a server which may itself be partitioned.
+ """
+
+ def __init__(self):
+ self.id = None
+ self.uri = None
+ self.thisServer = False
+ self.ips = set()
+ self.allowed_from_ips = set()
+ self.shared_secret = None
+ self.partitions = {}
+ self.partitions_ips = set()
+ self.isImplicit = True
+
+
+ def check(self, ignoreIPLookupFailures=False):
+ # Check whether this matches the current server
+ parsed_uri = urlparse.urlparse(self.uri)
+ if parsed_uri.hostname == config.ServerHostName:
+ if parsed_uri.scheme == "http":
+ if config.HTTPPort:
+ self.thisServer = parsed_uri.port in (config.HTTPPort,) + tuple(config.BindHTTPPorts)
+ elif parsed_uri.scheme == "https":
+ if config.SSLPort:
+ self.thisServer = parsed_uri.port in (config.SSLPort,) + tuple(config.BindSSLPorts)
+
+ # Need to cache IP addresses
+ try:
+ ips = getIPsFromHost(parsed_uri.hostname)
+ except socket.gaierror, e:
+ msg = "Unable to lookup ip-addr for server '%s': %s" % (parsed_uri.hostname, str(e))
+ log.error(msg)
+ if ignoreIPLookupFailures:
+ ips = ()
+ else:
+ raise ValueError(msg)
+ self.ips = set(ips)
+
+ actual_ips = set()
+ for item in self.allowed_from_ips:
+ if not isIPAddress(item):
+ try:
+ ips = getIPsFromHost(item)
+ except socket.gaierror, e:
+ msg = "Unable to lookup ip-addr for allowed-from '%s': %s" % (item, str(e))
+ log.error(msg)
+ if not ignoreIPLookupFailures:
+ raise ValueError(msg)
+ else:
+ actual_ips.update(ips)
+ else:
+ actual_ips.add(item)
+ self.allowed_from_ips = actual_ips
+
+ for uri in self.partitions.values():
+ parsed_uri = urlparse.urlparse(uri)
+ try:
+ ips = getIPsFromHost(parsed_uri.hostname)
+ except socket.gaierror, e:
+ msg = "Unable to lookup ip-addr for partition '%s': %s" % (parsed_uri.hostname, str(e))
+ log.error(msg)
+ if ignoreIPLookupFailures:
+ ips = ()
+ else:
+ raise ValueError(msg)
+ self.partitions_ips.update(ips)
+
+
+ def checkThisIP(self, ip):
+ """
+ Check that the passed in IP address corresponds to this server or one of its partitions.
+ """
+ return (ip in self.ips) or (ip in self.partitions_ips)
+
+
+ def hasAllowedFromIP(self):
+ return len(self.allowed_from_ips) > 0
+
+
+ def checkAllowedFromIP(self, ip):
+ return ip in self.allowed_from_ips
+
+
+ def checkSharedSecret(self, request):
+
+ # Get header from the request
+ request_secret = request.headers.getRawHeaders(SERVER_SECRET_HEADER)
+
+ if request_secret is not None and self.shared_secret is None:
+ log.error("iSchedule request included unexpected %s header" % (SERVER_SECRET_HEADER,))
+ return False
+ elif request_secret is None and self.shared_secret is not None:
+ log.error("iSchedule request did not include required %s header" % (SERVER_SECRET_HEADER,))
+ return False
+ elif (request_secret[0] if request_secret else None) != self.shared_secret:
+ log.error("iSchedule request %s header did not match" % (SERVER_SECRET_HEADER,))
+ return False
+ else:
+ return True
+
+
+ def secretHeader(self):
+ """
+ Return a tuple of header name, header value
+ """
+ return (SERVER_SECRET_HEADER, self.shared_secret,)
+
+
+ def addPartition(self, id, uri):
+ self.partitions[id] = uri
+
+
+ def getPartitionURIForId(self, id):
+ return self.partitions.get(id)
+
+
+ def isPartitioned(self):
+ return len(self.partitions) != 0
+
+
+ def installReverseProxies(self, ownUID, maxClients):
+
+ for partition, url in self.partitions.iteritems():
+ if partition != ownUID:
+ installPool(
+ partition,
+ url,
+ maxClients,
+ )
+
+
+
+ELEMENT_SERVERS = "servers"
+ELEMENT_SERVER = "server"
+ELEMENT_ID = "id"
+ELEMENT_URI = "uri"
+ELEMENT_ALLOWED_FROM = "allowed-from"
+ELEMENT_SHARED_SECRET = "shared-secret"
+ELEMENT_PARTITIONS = "partitions"
+ELEMENT_PARTITION = "partition"
+ATTR_IMPLICIT = "implicit"
+ATTR_VALUE_YES = "yes"
+ATTR_VALUE_NO = "no"
+
+class ServersParser(object):
+ """
+ Servers configuration file parser.
+ """
+ @staticmethod
+ def parse(xmlFile, ignoreIPLookupFailures=False):
+
+ results = {}
+
+ # Read in XML
+ try:
+ _ignore_tree, servers_node = readXML(xmlFile, ELEMENT_SERVERS)
+ except ValueError, e:
+ log.error("XML parse error for '%s' because: %s" % (xmlFile, e,), raiseException=RuntimeError)
+
+ for child in servers_node.getchildren():
+
+ if child.tag != ELEMENT_SERVER:
+ log.error("Unknown server type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
+
+ server = Server()
+ server.isImplicit = child.get(ATTR_IMPLICIT, ATTR_VALUE_YES) == ATTR_VALUE_YES
+
+ for node in child.getchildren():
+ if node.tag == ELEMENT_ID:
+ server.id = node.text
+ elif node.tag == ELEMENT_URI:
+ server.uri = node.text
+ elif node.tag == ELEMENT_ALLOWED_FROM:
+ server.allowed_from_ips.add(node.text)
+ elif node.tag == ELEMENT_SHARED_SECRET:
+ server.shared_secret = node.text
+ elif node.tag == ELEMENT_PARTITIONS:
+ ServersParser._parsePartition(xmlFile, node, server)
+ else:
+ log.error("Invalid element '%s' in servers file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
+
+ if server.id is None or server.uri is None:
+ log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
+
+ server.check(ignoreIPLookupFailures=ignoreIPLookupFailures)
+ results[server.id] = server
+
+ return results
+
+
+ @staticmethod
+ def _parsePartition(xmlFile, partitions, server):
+
+ for child in partitions.getchildren():
+
+ if child.tag != ELEMENT_PARTITION:
+ log.error("Unknown partition type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
+
+ id = None
+ uri = None
+ for node in child.getchildren():
+ if node.tag == ELEMENT_ID:
+ id = node.text
+ elif node.tag == ELEMENT_URI:
+ uri = node.text
+ else:
+ log.error("Invalid element '%s' in augment file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
+
+ if id is None or uri is None:
+ log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
+
+ server.addPartition(id, uri)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/remoteservers.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,227 +0,0 @@
-##
-# Copyright (c) 2006-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.python.filepath import CachingFilePath as FilePath
-
-from twext.python.log import Logger
-
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav import xmlutil
-
-"""
-XML based iSchedule configuration file handling. This is for handling of remote servers. The localservers.py module
-handles servers that are local (partitioned or podded).
-"""
-
-__all__ = [
- "IScheduleServers",
-]
-
-log = Logger()
-
-
-
-class IScheduleServers(object):
-
- _fileInfo = None
- _xmlFile = None
- _servers = None
- _domainMap = None
-
- def __init__(self):
-
- self._loadConfig()
-
-
- def _loadConfig(self):
- if config.Scheduling.iSchedule.RemoteServers:
- if IScheduleServers._servers is None:
- IScheduleServers._xmlFile = FilePath(
- fullServerPath(
- config.ConfigRoot,
- config.Scheduling.iSchedule.RemoteServers,
- )
- )
- if IScheduleServers._xmlFile.exists():
- IScheduleServers._xmlFile.restat()
- fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
- if fileInfo != IScheduleServers._fileInfo:
- parser = IScheduleServersParser(IScheduleServers._xmlFile)
- IScheduleServers._servers = parser.servers
- self._mapDomains()
- IScheduleServers._fileInfo = fileInfo
- else:
- IScheduleServers._servers = ()
- IScheduleServers._domainMap = {}
-
- else:
- IScheduleServers._servers = ()
- IScheduleServers._domainMap = {}
-
-
- def _mapDomains(self):
- IScheduleServers._domainMap = {}
- for server in IScheduleServers._servers:
- for domain in server.domains:
- IScheduleServers._domainMap[domain] = server
-
-
- def mapDomain(self, domain):
- """
- Map a calendar user address domain to a suitable server that can
- handle server-to-server requests for that user.
- """
- return IScheduleServers._domainMap.get(domain)
-
-ELEMENT_SERVERS = "servers"
-ELEMENT_SERVER = "server"
-ELEMENT_URI = "uri"
-ELEMENT_AUTHENTICATION = "authentication"
-ATTRIBUTE_TYPE = "type"
-ATTRIBUTE_BASICAUTH = "basic"
-ELEMENT_USER = "user"
-ELEMENT_PASSWORD = "password"
-ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
-ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
-ELEMENT_DOMAINS = "domains"
-ELEMENT_DOMAIN = "domain"
-ELEMENT_CLIENT_HOSTS = "hosts"
-ELEMENT_HOST = "host"
-
-
-
-class IScheduleServersParser(object):
- """
- Server-to-server configuration file parser.
- """
- def __repr__(self):
- return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
-
-
- def __init__(self, xmlFile):
-
- self.servers = []
-
- # Read in XML
- _ignore_etree, servers_node = xmlutil.readXML(xmlFile.path, ELEMENT_SERVERS)
- self._parseXML(servers_node)
-
-
- def _parseXML(self, node):
- """
- Parse the XML root node from the server-to-server configuration document.
- @param node: the L{Node} to parse.
- """
-
- for child in node.getchildren():
- if child.tag == ELEMENT_SERVER:
- self.servers.append(IScheduleServerRecord())
- self.servers[-1].parseXML(child)
-
-
-
-class IScheduleServerRecord (object):
- """
- Contains server-to-server details.
- """
- def __init__(self, uri=None):
- """
- @param recordType: record type for directory entry.
- """
- self.uri = ""
- self.authentication = None
- self.allow_from = False
- self.allow_to = True
- self.domains = []
- self.client_hosts = []
- self.unNormalizeAddresses = True
- self.moreHeaders = []
-
- if uri:
- self.uri = uri
- self._parseDetails()
-
-
- def details(self):
- return (self.ssl, self.host, self.port, self.path,)
-
-
- def redirect(self, location):
- """
- Permanent redirect for the lifetime of this record.
- """
- self.uri = location
- self._parseDetails()
-
-
- def parseXML(self, node):
- for child in node.getchildren():
- if child.tag == ELEMENT_URI:
- self.uri = child.text
- elif child.tag == ELEMENT_AUTHENTICATION:
- self._parseAuthentication(child)
- elif child.tag == ELEMENT_ALLOW_REQUESTS_FROM:
- self.allow_from = True
- elif child.tag == ELEMENT_ALLOW_REQUESTS_TO:
- self.allow_to = True
- elif child.tag == ELEMENT_DOMAINS:
- self._parseList(child, ELEMENT_DOMAIN, self.domains)
- elif child.tag == ELEMENT_CLIENT_HOSTS:
- self._parseList(child, ELEMENT_HOST, self.client_hosts)
- else:
- raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child.tag,))
-
- self._parseDetails()
-
-
- def _parseList(self, node, element_name, appendto):
- for child in node.getchildren():
- if child.tag == element_name:
- appendto.append(child.text)
-
-
- def _parseAuthentication(self, node):
- if node.get(ATTRIBUTE_TYPE) != ATTRIBUTE_BASICAUTH:
- return
-
- for child in node.getchildren():
- if child.tag == ELEMENT_USER:
- user = child.text
- elif child.tag == ELEMENT_PASSWORD:
- password = child.text
-
- self.authentication = ("basic", user, password,)
-
-
- def _parseDetails(self):
- # Extract scheme, host, port and path
- if self.uri.startswith("http://"):
- self.ssl = False
- rest = self.uri[7:]
- elif self.uri.startswith("https://"):
- self.ssl = True
- rest = self.uri[8:]
-
- splits = rest.split("/", 1)
- hostport = splits[0].split(":")
- self.host = hostport[0]
- if len(hostport) > 1:
- self.port = int(hostport[1])
- else:
- self.port = {False: 80, True: 443}[self.ssl]
- self.path = "/"
- if len(splits) > 1:
- self.path += splits[1]
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/remoteservers.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/remoteservers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,227 @@
+##
+# Copyright (c) 2006-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.python.filepath import CachingFilePath as FilePath
+
+from twext.python.log import Logger
+
+from twistedcaldav.config import config, fullServerPath
+from twistedcaldav import xmlutil
+
+"""
+XML based iSchedule configuration file handling. This is for handling of remote servers. The localservers.py module
+handles servers that are local (partitioned or podded).
+"""
+
+__all__ = [
+ "IScheduleServers",
+]
+
+log = Logger()
+
+
+
+class IScheduleServers(object):
+
+ _fileInfo = None
+ _xmlFile = None
+ _servers = None
+ _domainMap = None
+
+ def __init__(self):
+
+ self._loadConfig()
+
+
+ def _loadConfig(self):
+ if config.Scheduling.iSchedule.RemoteServers:
+ if IScheduleServers._servers is None:
+ IScheduleServers._xmlFile = FilePath(
+ fullServerPath(
+ config.ConfigRoot,
+ config.Scheduling.iSchedule.RemoteServers,
+ )
+ )
+ if IScheduleServers._xmlFile.exists():
+ IScheduleServers._xmlFile.restat()
+ fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
+ if fileInfo != IScheduleServers._fileInfo:
+ parser = IScheduleServersParser(IScheduleServers._xmlFile)
+ IScheduleServers._servers = parser.servers
+ self._mapDomains()
+ IScheduleServers._fileInfo = fileInfo
+ else:
+ IScheduleServers._servers = ()
+ IScheduleServers._domainMap = {}
+
+ else:
+ IScheduleServers._servers = ()
+ IScheduleServers._domainMap = {}
+
+
+ def _mapDomains(self):
+ IScheduleServers._domainMap = {}
+ for server in IScheduleServers._servers:
+ for domain in server.domains:
+ IScheduleServers._domainMap[domain] = server
+
+
+ def mapDomain(self, domain):
+ """
+ Map a calendar user address domain to a suitable server that can
+ handle server-to-server requests for that user.
+ """
+ return IScheduleServers._domainMap.get(domain)
+
+ELEMENT_SERVERS = "servers"
+ELEMENT_SERVER = "server"
+ELEMENT_URI = "uri"
+ELEMENT_AUTHENTICATION = "authentication"
+ATTRIBUTE_TYPE = "type"
+ATTRIBUTE_BASICAUTH = "basic"
+ELEMENT_USER = "user"
+ELEMENT_PASSWORD = "password"
+ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
+ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
+ELEMENT_DOMAINS = "domains"
+ELEMENT_DOMAIN = "domain"
+ELEMENT_CLIENT_HOSTS = "hosts"
+ELEMENT_HOST = "host"
+
+
+
+class IScheduleServersParser(object):
+ """
+ Server-to-server configuration file parser.
+ """
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+
+ def __init__(self, xmlFile):
+
+ self.servers = []
+
+ # Read in XML
+ _ignore_etree, servers_node = xmlutil.readXML(xmlFile.path, ELEMENT_SERVERS)
+ self._parseXML(servers_node)
+
+
+ def _parseXML(self, node):
+ """
+ Parse the XML root node from the server-to-server configuration document.
+ @param node: the L{Node} to parse.
+ """
+
+ for child in node.getchildren():
+ if child.tag == ELEMENT_SERVER:
+ self.servers.append(IScheduleServerRecord())
+ self.servers[-1].parseXML(child)
+
+
+
+class IScheduleServerRecord (object):
+ """
+ Contains server-to-server details.
+ """
+ def __init__(self, uri=None):
+ """
+ @param recordType: record type for directory entry.
+ """
+ self.uri = ""
+ self.authentication = None
+ self.allow_from = False
+ self.allow_to = True
+ self.domains = []
+ self.client_hosts = []
+ self.unNormalizeAddresses = True
+ self.moreHeaders = []
+
+ if uri:
+ self.uri = uri
+ self._parseDetails()
+
+
+ def details(self):
+ return (self.ssl, self.host, self.port, self.path,)
+
+
+ def redirect(self, location):
+ """
+ Permanent redirect for the lifetime of this record.
+ """
+ self.uri = location
+ self._parseDetails()
+
+
+ def parseXML(self, node):
+ for child in node.getchildren():
+ if child.tag == ELEMENT_URI:
+ self.uri = child.text
+ elif child.tag == ELEMENT_AUTHENTICATION:
+ self._parseAuthentication(child)
+ elif child.tag == ELEMENT_ALLOW_REQUESTS_FROM:
+ self.allow_from = True
+ elif child.tag == ELEMENT_ALLOW_REQUESTS_TO:
+ self.allow_to = True
+ elif child.tag == ELEMENT_DOMAINS:
+ self._parseList(child, ELEMENT_DOMAIN, self.domains)
+ elif child.tag == ELEMENT_CLIENT_HOSTS:
+ self._parseList(child, ELEMENT_HOST, self.client_hosts)
+ else:
+ raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child.tag,))
+
+ self._parseDetails()
+
+
+ def _parseList(self, node, element_name, appendto):
+ for child in node.getchildren():
+ if child.tag == element_name:
+ appendto.append(child.text)
+
+
+ def _parseAuthentication(self, node):
+ if node.get(ATTRIBUTE_TYPE) != ATTRIBUTE_BASICAUTH:
+ return
+
+ for child in node.getchildren():
+ if child.tag == ELEMENT_USER:
+ user = child.text
+ elif child.tag == ELEMENT_PASSWORD:
+ password = child.text
+
+ self.authentication = ("basic", user, password,)
+
+
+ def _parseDetails(self):
+ # Extract scheme, host, port and path
+ if self.uri.startswith("http://"):
+ self.ssl = False
+ rest = self.uri[7:]
+ elif self.uri.startswith("https://"):
+ self.ssl = True
+ rest = self.uri[8:]
+
+ splits = rest.split("/", 1)
+ hostport = splits[0].split(":")
+ self.host = hostport[0]
+ if len(hostport) > 1:
+ self.port = int(hostport[1])
+ else:
+ self.port = {False: 80, True: 443}[self.ssl]
+ self.path = "/"
+ if len(splits) > 1:
+ self.path += splits[1]
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,245 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from pycalendar.datetime import PyCalendarDateTime
-from pycalendar.timezone import PyCalendarTimezone
-from twext.web2 import responsecode
-from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.http import Response, HTTPError, StatusResponse, XMLResponse
-from twext.web2.http_headers import MimeType
-from twisted.internet.defer import succeed, returnValue, inlineCallbacks
-from twistedcaldav import caldavxml
-from twistedcaldav.config import config
-from twistedcaldav.extensions import DAVResource, \
- DAVResourceWithoutChildrenMixin
-from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
-from twistedcaldav.scheduling.caldav.resource import deliverSchedulePrivilegeSet
-from twistedcaldav.scheduling.ischedule.scheduler import IScheduleScheduler
-from txdav.xml import element as davxml
-import twistedcaldav.scheduling.ischedule.xml as ischedulexml
-
-__all__ = [
- "IScheduleInboxResource",
-]
-
-class IScheduleInboxResource (ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
- """
- iSchedule Inbox resource.
-
- Extends L{DAVResource} to provide iSchedule inbox functionality.
- """
-
- def __init__(self, parent, store):
- """
- @param parent: the parent resource of this one.
- """
- assert parent is not None
-
- DAVResource.__init__(self, principalCollections=parent.principalCollections())
-
- self.parent = parent
- self._newStore = store
-
-
- def deadProperties(self):
- if not hasattr(self, "_dead_properties"):
- self._dead_properties = NonePropertyStore(self)
- return self._dead_properties
-
-
- def etag(self):
- return succeed(None)
-
-
- def checkPreconditions(self, request):
- return None
-
-
- def resourceType(self):
- return davxml.ResourceType.ischeduleinbox
-
-
- def contentType(self):
- return MimeType.fromString("text/html; charset=utf-8")
-
-
- def isCollection(self):
- return False
-
-
- def isCalendarCollection(self):
- return False
-
-
- def isPseudoCalendarCollection(self):
- return False
-
-
- def principalForCalendarUserAddress(self, address):
- for principalCollection in self.principalCollections():
- principal = principalCollection.principalForCalendarUserAddress(address)
- if principal is not None:
- return principal
- return None
-
-
- def render(self, request):
- output = """<html>
-<head>
-<title>Server To Server Inbox Resource</title>
-</head>
-<body>
-<h1>Server To Server Inbox Resource.</h1>
-</body
-</html>"""
-
- response = Response(200, {}, output)
- response.headers.setHeader("content-type", MimeType("text", "html"))
- return response
-
-
- def http_GET(self, request):
- """
- The iSchedule GET method.
- """
-
- if not request.args:
- # Do normal GET behavior
- return self.render(request)
-
- action = request.args.get("action", ("",))
- if len(action) != 1:
- raise HTTPError(StatusResponse(
- responsecode.BAD_REQUEST,
- "Invalid action parameter",
- ))
- action = action[0]
-
- action = {
- "capabilities" : self.doCapabilities,
- }.get(action, None)
-
- if action is None:
- raise HTTPError(StatusResponse(
- responsecode.BAD_REQUEST,
- "Unknown action action parameter",
- ))
-
- return action(request)
-
-
- def doCapabilities(self, request):
- """
- Return a list of all timezones known to the server.
- """
-
- # Determine min/max date-time for iSchedule
- now = PyCalendarDateTime.getNowUTC()
- minDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
- minDateTime.offsetYear(-1)
- maxDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
- maxDateTime.offsetYear(10)
-
- result = ischedulexml.QueryResult(
-
- ischedulexml.Capabilities(
- ischedulexml.Versions(
- ischedulexml.Version.fromString("1.0"),
- ),
- ischedulexml.SchedulingMessages(
- ischedulexml.Component(
- ischedulexml.Method(name="REQUEST"),
- ischedulexml.Method(name="CANCEL"),
- ischedulexml.Method(name="REPLY"),
- name="VEVENT"
- ),
- ischedulexml.Component(
- ischedulexml.Method(name="REQUEST"),
- ischedulexml.Method(name="CANCEL"),
- ischedulexml.Method(name="REPLY"),
- name="VTODO"
- ),
- ischedulexml.Component(
- ischedulexml.Method(name="REQUEST"),
- name="VFREEBUSY"
- ),
- ),
- ischedulexml.CalendarDataTypes(
- ischedulexml.CalendarDataType(**{
- "content-type": "text/calendar",
- "version": "2.0",
- }),
- ),
- ischedulexml.Attachments(
- ischedulexml.External(),
- ),
- ischedulexml.MaxContentLength.fromString(config.MaxResourceSize),
- ischedulexml.MinDateTime.fromString(minDateTime.getText()),
- ischedulexml.MaxDateTime.fromString(maxDateTime.getText()),
- ischedulexml.MaxInstances.fromString(config.MaxAllowedInstances),
- ischedulexml.MaxRecipients.fromString(config.MaxAttendeesPerInstance),
- ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")),
- ),
- )
- return XMLResponse(responsecode.OK, result)
-
-
- @inlineCallbacks
- def http_POST(self, request):
- """
- The server-to-server POST method.
- """
-
- # This is a server-to-server scheduling operation.
- scheduler = IScheduleScheduler(request, self)
-
- # Need a transaction to work with
- txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
- request._newStoreTransaction = txn
-
- # Do the POST processing treating this as a non-local schedule
- try:
- result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
- except Exception, e:
- yield txn.abort()
- raise e
- else:
- yield txn.commit()
- returnValue(result.response())
-
- ##
- # ACL
- ##
-
-
- def supportedPrivileges(self, request):
- return succeed(deliverSchedulePrivilegeSet)
-
-
- def defaultAccessControlList(self):
- privs = (
- davxml.Privilege(davxml.Read()),
- davxml.Privilege(caldavxml.ScheduleDeliver()),
- )
-
- return davxml.ACL(
- # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
- davxml.ACE(
- davxml.Principal(davxml.All()),
- davxml.Grant(*privs),
- davxml.Protected(),
- ),
- )
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/resource.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,245 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.timezone import PyCalendarTimezone
+from twext.web2 import responsecode
+from twext.web2.dav.noneprops import NonePropertyStore
+from twext.web2.http import Response, HTTPError, StatusResponse, XMLResponse
+from twext.web2.http_headers import MimeType
+from twisted.internet.defer import succeed, returnValue, inlineCallbacks
+from twistedcaldav import caldavxml
+from twistedcaldav.config import config
+from twistedcaldav.extensions import DAVResource, \
+ DAVResourceWithoutChildrenMixin
+from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
+from twistedcaldav.scheduling.caldav.resource import deliverSchedulePrivilegeSet
+from twistedcaldav.scheduling.ischedule.scheduler import IScheduleScheduler
+from txdav.xml import element as davxml
+import twistedcaldav.scheduling.ischedule.xml as ischedulexml
+
+__all__ = [
+ "IScheduleInboxResource",
+]
+
+class IScheduleInboxResource (ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
+ """
+ iSchedule Inbox resource.
+
+ Extends L{DAVResource} to provide iSchedule inbox functionality.
+ """
+
+ def __init__(self, parent, store):
+ """
+ @param parent: the parent resource of this one.
+ """
+ assert parent is not None
+
+ DAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+ self.parent = parent
+ self._newStore = store
+
+
+ def deadProperties(self):
+ if not hasattr(self, "_dead_properties"):
+ self._dead_properties = NonePropertyStore(self)
+ return self._dead_properties
+
+
+ def etag(self):
+ return succeed(None)
+
+
+ def checkPreconditions(self, request):
+ return None
+
+
+ def resourceType(self):
+ return davxml.ResourceType.ischeduleinbox
+
+
+ def contentType(self):
+ return MimeType.fromString("text/html; charset=utf-8")
+
+
+ def isCollection(self):
+ return False
+
+
+ def isCalendarCollection(self):
+ return False
+
+
+ def isPseudoCalendarCollection(self):
+ return False
+
+
+ def principalForCalendarUserAddress(self, address):
+ for principalCollection in self.principalCollections():
+ principal = principalCollection.principalForCalendarUserAddress(address)
+ if principal is not None:
+ return principal
+ return None
+
+
+ def render(self, request):
+ output = """<html>
+<head>
+<title>Server To Server Inbox Resource</title>
+</head>
+<body>
+<h1>Server To Server Inbox Resource.</h1>
+</body
+</html>"""
+
+ response = Response(200, {}, output)
+ response.headers.setHeader("content-type", MimeType("text", "html"))
+ return response
+
+
+ def http_GET(self, request):
+ """
+ The iSchedule GET method.
+ """
+
+ if not request.args:
+ # Do normal GET behavior
+ return self.render(request)
+
+ action = request.args.get("action", ("",))
+ if len(action) != 1:
+ raise HTTPError(StatusResponse(
+ responsecode.BAD_REQUEST,
+ "Invalid action parameter",
+ ))
+ action = action[0]
+
+ action = {
+ "capabilities" : self.doCapabilities,
+ }.get(action, None)
+
+ if action is None:
+ raise HTTPError(StatusResponse(
+ responsecode.BAD_REQUEST,
+ "Unknown action action parameter",
+ ))
+
+ return action(request)
+
+
+ def doCapabilities(self, request):
+ """
+ Return a list of all timezones known to the server.
+ """
+
+ # Determine min/max date-time for iSchedule
+ now = PyCalendarDateTime.getNowUTC()
+ minDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
+ minDateTime.offsetYear(-1)
+ maxDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
+ maxDateTime.offsetYear(10)
+
+ result = ischedulexml.QueryResult(
+
+ ischedulexml.Capabilities(
+ ischedulexml.Versions(
+ ischedulexml.Version.fromString("1.0"),
+ ),
+ ischedulexml.SchedulingMessages(
+ ischedulexml.Component(
+ ischedulexml.Method(name="REQUEST"),
+ ischedulexml.Method(name="CANCEL"),
+ ischedulexml.Method(name="REPLY"),
+ name="VEVENT"
+ ),
+ ischedulexml.Component(
+ ischedulexml.Method(name="REQUEST"),
+ ischedulexml.Method(name="CANCEL"),
+ ischedulexml.Method(name="REPLY"),
+ name="VTODO"
+ ),
+ ischedulexml.Component(
+ ischedulexml.Method(name="REQUEST"),
+ name="VFREEBUSY"
+ ),
+ ),
+ ischedulexml.CalendarDataTypes(
+ ischedulexml.CalendarDataType(**{
+ "content-type": "text/calendar",
+ "version": "2.0",
+ }),
+ ),
+ ischedulexml.Attachments(
+ ischedulexml.External(),
+ ),
+ ischedulexml.MaxContentLength.fromString(config.MaxResourceSize),
+ ischedulexml.MinDateTime.fromString(minDateTime.getText()),
+ ischedulexml.MaxDateTime.fromString(maxDateTime.getText()),
+ ischedulexml.MaxInstances.fromString(config.MaxAllowedInstances),
+ ischedulexml.MaxRecipients.fromString(config.MaxAttendeesPerInstance),
+ ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")),
+ ),
+ )
+ return XMLResponse(responsecode.OK, result)
+
+
+ @inlineCallbacks
+ def http_POST(self, request):
+ """
+ The server-to-server POST method.
+ """
+
+ # This is a server-to-server scheduling operation.
+ scheduler = IScheduleScheduler(request, self)
+
+ # Need a transaction to work with
+ txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
+ request._newStoreTransaction = txn
+
+ # Do the POST processing treating this as a non-local schedule
+ try:
+ result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
+ except Exception, e:
+ yield txn.abort()
+ raise e
+ else:
+ yield txn.commit()
+ returnValue(result.response())
+
+ ##
+ # ACL
+ ##
+
+
+ def supportedPrivileges(self, request):
+ return succeed(deliverSchedulePrivilegeSet)
+
+
+ def defaultAccessControlList(self):
+ privs = (
+ davxml.Privilege(davxml.Read()),
+ davxml.Privilege(caldavxml.ScheduleDeliver()),
+ )
+
+ return davxml.ACL(
+ # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
+ davxml.ACE(
+ davxml.Principal(davxml.All()),
+ davxml.Grant(*privs),
+ davxml.Protected(),
+ ),
+ )
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/scheduler.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/scheduler.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,455 +0,0 @@
-
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.python.log import Logger
-from twext.web2 import responsecode
-from twext.web2.http import HTTPError, Response
-from twisted.internet.abstract import isIPAddress
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twistedcaldav.scheduling import addressmapping
-from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
-from twistedcaldav.scheduling.cuaddress import calendarUserFromPrincipal
-from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServers
-from twistedcaldav.scheduling.scheduler import RemoteScheduler, \
- ScheduleResponseQueue
-import twistedcaldav.scheduling.ischedule.xml as ixml
-from twistedcaldav.scheduling.ischedule.localservers import Servers
-from twistedcaldav.util import normalizationLookup
-from txdav.xml import element as davxml
-import itertools
-import re
-import socket
-import urlparse
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule.dkim import DKIMVerifier, \
- DKIMVerificationError, DKIMMissingError
-from twext.web2.http_headers import MimeType
-from twistedcaldav.scheduling.ischedule.xml import ischedule_namespace
-from txdav.xml.base import WebDAVUnknownElement
-from twistedcaldav.scheduling.ischedule.utils import getIPsFromHost
-from twistedcaldav.scheduling.ischedule import xml
-from twistedcaldav.ical import normalizeCUAddress
-
-"""
-L{IScheduleScheduler} - handles deliveries for scheduling messages being POSTed to the iSchedule inbox.
-"""
-
-__all__ = [
- "IScheduleScheduler",
-]
-
-
-log = Logger()
-
-class ErrorResponse(Response):
- """
- A L{Response} object which contains a status code and a L{element.Error}
- element.
- Renders itself as a DAV:error XML document.
- """
- error = None
- unregistered = True # base class is already registered
-
- def __init__(self, code, error, description=None):
- """
- @param code: a response code.
- @param error: an L{WebDAVElement} identifying the error, or a
- tuple C{(namespace, name)} with which to create an empty element
- denoting the error. (The latter is useful in the case of
- preconditions and postconditions, not all of which have defined
- XML element classes.)
- @param description: an optional string that, if present, will get
- wrapped in a (twisted_dav_namespace, error-description) element.
- """
- if type(error) is tuple:
- xml_namespace, xml_name = error
- error = WebDAVUnknownElement()
- error.namespace = xml_namespace
- error.name = xml_name
-
- self.description = description
- if self.description:
- output = ixml.Error(error, ixml.ResponseDescription(self.description)).toxml()
- else:
- output = ixml.Error(error).toxml()
-
- Response.__init__(self, code=code, stream=output)
-
- self.headers.setHeader("content-type", MimeType("text", "xml"))
-
- self.error = error
-
-
- def __repr__(self):
- return "<%s %s %s>" % (self.__class__.__name__, self.code, self.error.sname())
-
-
-
-class IScheduleResponseQueue (ScheduleResponseQueue):
- """
- Stores a list of (typically error) responses for use in a
- L{ScheduleResponse}.
- """
-
- schedule_response_element = xml.ScheduleResponse
- response_element = xml.Response
- recipient_element = xml.Recipient
- recipient_uses_href = False
- request_status_element = xml.RequestStatus
- error_element = xml.Error
- response_description_element = xml.ResponseDescription
- calendar_data_element = xml.CalendarData
-
-
-
-class IScheduleScheduler(RemoteScheduler):
-
- scheduleResponse = IScheduleResponseQueue
-
- errorResponse = ErrorResponse
-
- errorElements = {
- "originator-missing": (ischedule_namespace, "originator-missing"),
- "originator-invalid": (ischedule_namespace, "originator-invalid"),
- "originator-denied": (ischedule_namespace, "originator-denied"),
- "recipient-missing": (ischedule_namespace, "recipient-missing"),
- "recipient-invalid": (ischedule_namespace, "recipient-invalid"),
- "organizer-denied": (ischedule_namespace, "organizer-denied"),
- "attendee-denied": (ischedule_namespace, "attendee-denied"),
- "invalid-calendar-data-type": (ischedule_namespace, "invalid-calendar-data-type"),
- "invalid-calendar-data": (ischedule_namespace, "invalid-calendar-data"),
- "invalid-scheduling-message": (ischedule_namespace, "invalid-scheduling-message"),
- "max-recipients": (ischedule_namespace, "max-recipients"),
- }
-
- @inlineCallbacks
- def doSchedulingViaPOST(self, transaction, use_request_headers=False):
- """
- Carry out iSchedule specific processing.
- """
-
- self.verified = False
- if config.Scheduling.iSchedule.DKIM.Enabled:
- verifier = DKIMVerifier(self.request, protocol_debug=config.Scheduling.iSchedule.DKIM.ProtocolDebug)
- try:
- yield verifier.verify()
- self.verified = True
-
- except DKIMMissingError:
- # Carry on processing, but we will do extra checks on the originator as we would
- # when DKIM is not enabled, so that any local policy via remoteservers.xml can be used.
- pass
-
- except DKIMVerificationError, e:
- # If DKIM is enabled and there was a DKIM header present, then fail
- msg = "Failed to verify DKIM signature"
- _debug_msg = str(e)
- log.debug("%s:%s" % (msg, _debug_msg,))
- if config.Scheduling.iSchedule.DKIM.ProtocolDebug:
- msg = "%s:%s" % (msg, _debug_msg,)
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- (ischedule_namespace, "verification-failed"),
- msg,
- ))
-
- result = (yield super(IScheduleScheduler, self).doSchedulingViaPOST(transaction, use_request_headers))
- returnValue(result)
-
-
- def loadFromRequestHeaders(self):
- """
- Load Originator and Recipient from request headers.
- """
- super(IScheduleScheduler, self).loadFromRequestHeaders()
-
- if self.request.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
- self.request.doing_attendee_refresh = 1
-
-
- def preProcessCalendarData(self):
- """
- For data coming in from outside we need to normalize the calendar user addresses so that later iTIP
- processing will match calendar users against those in stored calendar data. Only do that for invites
- not freebusy.
- """
-
- if not self.checkForFreeBusy():
- # Need to normalize the calendar data and recipient values to keep those in sync,
- # as we might later try to match them
- self.calendar.normalizeCalendarUserAddresses(normalizationLookup, self.resource.principalForCalendarUserAddress)
-
-
- def loadRecipientsFromRequestHeaders(self):
- """
- Need to normalize the calendar data and recipient values to keep those in sync,
- as we might later try to match them
- """
- super(IScheduleScheduler, self).loadRecipientsFromRequestHeaders()
- self.recipients = [normalizeCUAddress(recipient, normalizationLookup, self.resource.principalForCalendarUserAddress) for recipient in self.recipients]
-
-
- def checkAuthorization(self):
- # Must have an unauthenticated user
- if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
- log.err("Authenticated originators not allowed: %s" % (self.originator,))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["originator-denied"],
- "Authentication not allowed",
- ))
-
-
- @inlineCallbacks
- def checkOriginator(self):
- """
- Check the validity of the Originator header.
- """
-
- # For remote requests we do not allow the originator to be a local user or one within our domain.
- originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
- if originatorPrincipal or localUser:
- if originatorPrincipal.locallyHosted():
- log.err("Cannot use originator that is on this server: %s" % (self.originator,))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["originator-denied"],
- "Originator cannot be local to server",
- ))
- else:
- self.originator = calendarUserFromPrincipal(self.originator, originatorPrincipal)
- self._validAlternateServer(originatorPrincipal)
- else:
- self.originator = RemoteCalendarUser(self.originator)
- self._validiScheduleServer()
-
-
- def _validiScheduleServer(self):
- """
- Check the validity of the iSchedule host.
- """
-
- # Check for DKIM verification first and treat as valid
- if self.verified:
- return
-
- # We will only accept originator in known domains.
- servermgr = IScheduleServers()
- server = servermgr.mapDomain(self.originator.domain)
- if not server or not server.allow_from:
- log.err("Originator not on recognized server: %s" % (self.originator,))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["originator-denied"],
- "Originator not recognized by server",
- ))
- else:
- # Get the request IP and map to hostname.
- clientip = self.request.remoteAddr.host
-
- # First compare as dotted IP
- matched = False
- compare_with = (server.host,) + tuple(server.client_hosts)
- if clientip in compare_with:
- matched = True
- else:
- # Now do hostname lookup
- try:
- host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
- for host in itertools.chain((host,), aliases):
- # Try simple match first
- if host in compare_with:
- matched = True
- break
-
- # Try pattern match next
- for pattern in compare_with:
- try:
- if re.match(pattern, host) is not None:
- matched = True
- break
- except re.error:
- log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
- else:
- continue
- break
- except socket.herror, e:
- log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-
- if not matched:
- log.err("Originator not on allowed server: %s" % (self.originator,))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["originator-denied"],
- "Originator not allowed to send to this server",
- ))
-
-
- def _validAlternateServer(self, principal):
- """
- Check the validity of the partitioned host.
- """
-
- # Extract expected host/port. This will be the partitionURI, or if no partitions,
- # the serverURI
- expected_uri = principal.partitionURI()
- if expected_uri is None:
- expected_uri = principal.serverURI()
- expected_uri = urlparse.urlparse(expected_uri)
-
- # Get the request IP and map to hostname.
- clientip = self.request.remoteAddr.host
-
- # Check against this server (or any of its partitions). We need this because an external iTIP message
- # may be addressed to users on different partitions, and the node receiving the iTIP message will need to
- # forward it to the partition nodes, thus the client ip seen by the partitions will in fact be the initial
- # receiving node.
- matched = False
- if Servers.getThisServer().checkThisIP(clientip):
- matched = True
-
- # Checked allowed IPs - if any were defined we only check against them, we do not
- # go on to check the expected server host ip
- elif Servers.getThisServer().hasAllowedFromIP():
- matched = Servers.getThisServer().checkAllowedFromIP(clientip)
- if not matched:
- log.error("Invalid iSchedule connection from client: %s" % (clientip,))
-
- # Next compare as dotted IP
- elif isIPAddress(expected_uri.hostname):
- if clientip == expected_uri.hostname:
- matched = True
- else:
- # Now do expected hostname -> IP lookup
- try:
- # So now try the lookup of the expected host
- for ip in getIPsFromHost(expected_uri.hostname):
- if ip == clientip:
- matched = True
- break
- except socket.herror, e:
- log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-
- # Check possible shared secret
- if matched and not Servers.getThisServer().checkSharedSecret(self.request):
- log.err("Invalid iSchedule shared secret")
- matched = False
-
- if not matched:
- log.err("Originator not on allowed server: %s" % (self.originator,))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["originator-denied"],
- "Originator not allowed to send to this server",
- ))
-
-
- @inlineCallbacks
- def checkOrganizerAsOriginator(self):
- """
- Check the validity of the ORGANIZER value. ORGANIZER must not be local.
- """
-
- # Verify that the ORGANIZER's cu address does not map to a valid user
- organizer = self.calendar.getOrganizer()
- if organizer:
- organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
- if organizerPrincipal:
- if organizerPrincipal.locallyHosted():
- log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["organizer-denied"],
- "Organizer is not local to server",
- ))
- else:
- # Check that the origin server is the correct partition
- self.organizer = calendarUserFromPrincipal(organizer, organizerPrincipal)
- self._validAlternateServer(self.organizer.principal)
- else:
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
- if localUser:
- log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendar,))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["organizer-denied"],
- "Organizer not allowed to be originator",
- ))
- else:
- self.organizer = RemoteCalendarUser(organizer)
- else:
- log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["organizer-denied"],
- "No organizer in calendar data",
- ))
-
-
- @inlineCallbacks
- def checkAttendeeAsOriginator(self):
- """
- Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
- Only local attendees are allowed for message originating from this server.
- """
-
- # Attendee cannot be local.
- attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
- if attendeePrincipal:
- if attendeePrincipal.locallyHosted():
- log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["attendee-denied"],
- "Local attendee cannot send to this server",
- ))
- else:
- self._validAlternateServer(attendeePrincipal)
- else:
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
- if localUser:
- log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["attendee-denied"],
- "Attendee not allowed to schedule",
- ))
-
- # TODO: in this case we should check that the ORGANIZER is the sole recipient.
-
-
- @inlineCallbacks
- def securityChecks(self):
- """
- Check that the originator has the appropriate rights to send this type of iTIP message.
- """
-
- # Prevent spoofing of ORGANIZER with specific METHODs when local
- if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
- yield self.checkOrganizerAsOriginator()
-
- # Prevent spoofing when doing reply-like METHODs
- elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
- yield self.checkAttendeeAsOriginator()
-
- else:
- log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
- raise HTTPError(self.errorResponse(
- responsecode.FORBIDDEN,
- self.errorElements["invalid-scheduling-message"],
- "Unknown iTIP method",
- ))
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/scheduler.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/scheduler.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/scheduler.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/scheduler.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,455 @@
+
+# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.python.log import Logger
+from twext.web2 import responsecode
+from twext.web2.http import HTTPError, Response
+from twisted.internet.abstract import isIPAddress
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twistedcaldav.scheduling import addressmapping
+from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
+from twistedcaldav.scheduling.cuaddress import calendarUserFromPrincipal
+from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServers
+from twistedcaldav.scheduling.scheduler import RemoteScheduler, \
+ ScheduleResponseQueue
+import twistedcaldav.scheduling.ischedule.xml as ixml
+from twistedcaldav.scheduling.ischedule.localservers import Servers
+from twistedcaldav.util import normalizationLookup
+from txdav.xml import element as davxml
+import itertools
+import re
+import socket
+import urlparse
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule.dkim import DKIMVerifier, \
+ DKIMVerificationError, DKIMMissingError
+from twext.web2.http_headers import MimeType
+from twistedcaldav.scheduling.ischedule.xml import ischedule_namespace
+from txdav.xml.base import WebDAVUnknownElement
+from twistedcaldav.scheduling.ischedule.utils import getIPsFromHost
+from twistedcaldav.scheduling.ischedule import xml
+from twistedcaldav.ical import normalizeCUAddress
+
+"""
+L{IScheduleScheduler} - handles deliveries for scheduling messages being POSTed to the iSchedule inbox.
+"""
+
+__all__ = [
+ "IScheduleScheduler",
+]
+
+
+log = Logger()
+
+class ErrorResponse(Response):
+ """
+ A L{Response} object which contains a status code and a L{element.Error}
+ element.
+ Renders itself as a DAV:error XML document.
+ """
+ error = None
+ unregistered = True # base class is already registered
+
+ def __init__(self, code, error, description=None):
+ """
+ @param code: a response code.
+ @param error: an L{WebDAVElement} identifying the error, or a
+ tuple C{(namespace, name)} with which to create an empty element
+ denoting the error. (The latter is useful in the case of
+ preconditions and postconditions, not all of which have defined
+ XML element classes.)
+ @param description: an optional string that, if present, will get
+ wrapped in a (twisted_dav_namespace, error-description) element.
+ """
+ if type(error) is tuple:
+ xml_namespace, xml_name = error
+ error = WebDAVUnknownElement()
+ error.namespace = xml_namespace
+ error.name = xml_name
+
+ self.description = description
+ if self.description:
+ output = ixml.Error(error, ixml.ResponseDescription(self.description)).toxml()
+ else:
+ output = ixml.Error(error).toxml()
+
+ Response.__init__(self, code=code, stream=output)
+
+ self.headers.setHeader("content-type", MimeType("text", "xml"))
+
+ self.error = error
+
+
+ def __repr__(self):
+ return "<%s %s %s>" % (self.__class__.__name__, self.code, self.error.sname())
+
+
+
+class IScheduleResponseQueue (ScheduleResponseQueue):
+ """
+ Stores a list of (typically error) responses for use in a
+ L{ScheduleResponse}.
+ """
+
+ schedule_response_element = xml.ScheduleResponse
+ response_element = xml.Response
+ recipient_element = xml.Recipient
+ recipient_uses_href = False
+ request_status_element = xml.RequestStatus
+ error_element = xml.Error
+ response_description_element = xml.ResponseDescription
+ calendar_data_element = xml.CalendarData
+
+
+
+class IScheduleScheduler(RemoteScheduler):
+
+ scheduleResponse = IScheduleResponseQueue
+
+ errorResponse = ErrorResponse
+
+ errorElements = {
+ "originator-missing": (ischedule_namespace, "originator-missing"),
+ "originator-invalid": (ischedule_namespace, "originator-invalid"),
+ "originator-denied": (ischedule_namespace, "originator-denied"),
+ "recipient-missing": (ischedule_namespace, "recipient-missing"),
+ "recipient-invalid": (ischedule_namespace, "recipient-invalid"),
+ "organizer-denied": (ischedule_namespace, "organizer-denied"),
+ "attendee-denied": (ischedule_namespace, "attendee-denied"),
+ "invalid-calendar-data-type": (ischedule_namespace, "invalid-calendar-data-type"),
+ "invalid-calendar-data": (ischedule_namespace, "invalid-calendar-data"),
+ "invalid-scheduling-message": (ischedule_namespace, "invalid-scheduling-message"),
+ "max-recipients": (ischedule_namespace, "max-recipients"),
+ }
+
+ @inlineCallbacks
+ def doSchedulingViaPOST(self, transaction, use_request_headers=False):
+ """
+ Carry out iSchedule specific processing.
+ """
+
+ self.verified = False
+ if config.Scheduling.iSchedule.DKIM.Enabled:
+ verifier = DKIMVerifier(self.request, protocol_debug=config.Scheduling.iSchedule.DKIM.ProtocolDebug)
+ try:
+ yield verifier.verify()
+ self.verified = True
+
+ except DKIMMissingError:
+ # Carry on processing, but we will do extra checks on the originator as we would
+ # when DKIM is not enabled, so that any local policy via remoteservers.xml can be used.
+ pass
+
+ except DKIMVerificationError, e:
+ # If DKIM is enabled and there was a DKIM header present, then fail
+ msg = "Failed to verify DKIM signature"
+ _debug_msg = str(e)
+ log.debug("%s:%s" % (msg, _debug_msg,))
+ if config.Scheduling.iSchedule.DKIM.ProtocolDebug:
+ msg = "%s:%s" % (msg, _debug_msg,)
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ (ischedule_namespace, "verification-failed"),
+ msg,
+ ))
+
+ result = (yield super(IScheduleScheduler, self).doSchedulingViaPOST(transaction, use_request_headers))
+ returnValue(result)
+
+
+ def loadFromRequestHeaders(self):
+ """
+ Load Originator and Recipient from request headers.
+ """
+ super(IScheduleScheduler, self).loadFromRequestHeaders()
+
+ if self.request.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
+ self.request.doing_attendee_refresh = 1
+
+
+ def preProcessCalendarData(self):
+ """
+ For data coming in from outside we need to normalize the calendar user addresses so that later iTIP
+ processing will match calendar users against those in stored calendar data. Only do that for invites
+ not freebusy.
+ """
+
+ if not self.checkForFreeBusy():
+ # Need to normalize the calendar data and recipient values to keep those in sync,
+ # as we might later try to match them
+ self.calendar.normalizeCalendarUserAddresses(normalizationLookup, self.resource.principalForCalendarUserAddress)
+
+
+ def loadRecipientsFromRequestHeaders(self):
+ """
+ Need to normalize the calendar data and recipient values to keep those in sync,
+ as we might later try to match them
+ """
+ super(IScheduleScheduler, self).loadRecipientsFromRequestHeaders()
+ self.recipients = [normalizeCUAddress(recipient, normalizationLookup, self.resource.principalForCalendarUserAddress) for recipient in self.recipients]
+
+
+ def checkAuthorization(self):
+ # Must have an unauthenticated user
+ if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
+ log.err("Authenticated originators not allowed: %s" % (self.originator,))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["originator-denied"],
+ "Authentication not allowed",
+ ))
+
+
+ @inlineCallbacks
+ def checkOriginator(self):
+ """
+ Check the validity of the Originator header.
+ """
+
+ # For remote requests we do not allow the originator to be a local user or one within our domain.
+ originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+ localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
+ if originatorPrincipal or localUser:
+ if originatorPrincipal.locallyHosted():
+ log.err("Cannot use originator that is on this server: %s" % (self.originator,))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["originator-denied"],
+ "Originator cannot be local to server",
+ ))
+ else:
+ self.originator = calendarUserFromPrincipal(self.originator, originatorPrincipal)
+ self._validAlternateServer(originatorPrincipal)
+ else:
+ self.originator = RemoteCalendarUser(self.originator)
+ self._validiScheduleServer()
+
+
+ def _validiScheduleServer(self):
+ """
+ Check the validity of the iSchedule host.
+ """
+
+ # Check for DKIM verification first and treat as valid
+ if self.verified:
+ return
+
+ # We will only accept originator in known domains.
+ servermgr = IScheduleServers()
+ server = servermgr.mapDomain(self.originator.domain)
+ if not server or not server.allow_from:
+ log.err("Originator not on recognized server: %s" % (self.originator,))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["originator-denied"],
+ "Originator not recognized by server",
+ ))
+ else:
+ # Get the request IP and map to hostname.
+ clientip = self.request.remoteAddr.host
+
+ # First compare as dotted IP
+ matched = False
+ compare_with = (server.host,) + tuple(server.client_hosts)
+ if clientip in compare_with:
+ matched = True
+ else:
+ # Now do hostname lookup
+ try:
+ host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
+ for host in itertools.chain((host,), aliases):
+ # Try simple match first
+ if host in compare_with:
+ matched = True
+ break
+
+ # Try pattern match next
+ for pattern in compare_with:
+ try:
+ if re.match(pattern, host) is not None:
+ matched = True
+ break
+ except re.error:
+ log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
+ else:
+ continue
+ break
+ except socket.herror, e:
+ log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
+
+ if not matched:
+ log.err("Originator not on allowed server: %s" % (self.originator,))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["originator-denied"],
+ "Originator not allowed to send to this server",
+ ))
+
+
+ def _validAlternateServer(self, principal):
+ """
+ Check the validity of the partitioned host.
+ """
+
+ # Extract expected host/port. This will be the partitionURI, or if no partitions,
+ # the serverURI
+ expected_uri = principal.partitionURI()
+ if expected_uri is None:
+ expected_uri = principal.serverURI()
+ expected_uri = urlparse.urlparse(expected_uri)
+
+ # Get the request IP and map to hostname.
+ clientip = self.request.remoteAddr.host
+
+ # Check against this server (or any of its partitions). We need this because an external iTIP message
+ # may be addressed to users on different partitions, and the node receiving the iTIP message will need to
+ # forward it to the partition nodes, thus the client ip seen by the partitions will in fact be the initial
+ # receiving node.
+ matched = False
+ if Servers.getThisServer().checkThisIP(clientip):
+ matched = True
+
+ # Checked allowed IPs - if any were defined we only check against them, we do not
+ # go on to check the expected server host ip
+ elif Servers.getThisServer().hasAllowedFromIP():
+ matched = Servers.getThisServer().checkAllowedFromIP(clientip)
+ if not matched:
+ log.error("Invalid iSchedule connection from client: %s" % (clientip,))
+
+ # Next compare as dotted IP
+ elif isIPAddress(expected_uri.hostname):
+ if clientip == expected_uri.hostname:
+ matched = True
+ else:
+ # Now do expected hostname -> IP lookup
+ try:
+ # So now try the lookup of the expected host
+ for ip in getIPsFromHost(expected_uri.hostname):
+ if ip == clientip:
+ matched = True
+ break
+ except socket.herror, e:
+ log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
+
+ # Check possible shared secret
+ if matched and not Servers.getThisServer().checkSharedSecret(self.request):
+ log.err("Invalid iSchedule shared secret")
+ matched = False
+
+ if not matched:
+ log.err("Originator not on allowed server: %s" % (self.originator,))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["originator-denied"],
+ "Originator not allowed to send to this server",
+ ))
+
+
+ @inlineCallbacks
+ def checkOrganizerAsOriginator(self):
+ """
+ Check the validity of the ORGANIZER value. ORGANIZER must not be local.
+ """
+
+ # Verify that the ORGANIZER's cu address does not map to a valid user
+ organizer = self.calendar.getOrganizer()
+ if organizer:
+ organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+ if organizerPrincipal:
+ if organizerPrincipal.locallyHosted():
+ log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["organizer-denied"],
+ "Organizer is not local to server",
+ ))
+ else:
+ # Check that the origin server is the correct partition
+ self.organizer = calendarUserFromPrincipal(organizer, organizerPrincipal)
+ self._validAlternateServer(self.organizer.principal)
+ else:
+ localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
+ if localUser:
+ log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendar,))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["organizer-denied"],
+ "Organizer not allowed to be originator",
+ ))
+ else:
+ self.organizer = RemoteCalendarUser(organizer)
+ else:
+ log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["organizer-denied"],
+ "No organizer in calendar data",
+ ))
+
+
+ @inlineCallbacks
+ def checkAttendeeAsOriginator(self):
+ """
+ Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+ Only local attendees are allowed for message originating from this server.
+ """
+
+ # Attendee cannot be local.
+ attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
+ if attendeePrincipal:
+ if attendeePrincipal.locallyHosted():
+ log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["attendee-denied"],
+ "Local attendee cannot send to this server",
+ ))
+ else:
+ self._validAlternateServer(attendeePrincipal)
+ else:
+ localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
+ if localUser:
+ log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["attendee-denied"],
+ "Attendee not allowed to schedule",
+ ))
+
+ # TODO: in this case we should check that the ORGANIZER is the sole recipient.
+
+
+ @inlineCallbacks
+ def securityChecks(self):
+ """
+ Check that the originator has the appropriate rights to send this type of iTIP message.
+ """
+
+ # Prevent spoofing of ORGANIZER with specific METHODs when local
+ if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
+ yield self.checkOrganizerAsOriginator()
+
+ # Prevent spoofing when doing reply-like METHODs
+ elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+ yield self.checkAttendeeAsOriginator()
+
+ else:
+ log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
+ raise HTTPError(self.errorResponse(
+ responsecode.FORBIDDEN,
+ self.errorElements["invalid-scheduling-message"],
+ "Unknown iTIP method",
+ ))
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/__init__.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/data/db.example.com 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,19 +0,0 @@
-example.com. 10800 IN SOA ns.example.com. admin.example.com. (
- 2012090810 ; serial
- 3600 ; refresh (1 hour)
- 900 ; retry (15 minutes)
- 1209600 ; expire (2 weeks)
- 86400 ; minimum (1 day)
- )
- 10800 IN NS ns.example.com.
- 10800 IN A 127.0.0.1
-ns.example.com. 10800 IN A 127.0.0.1
-
-_caldavs._tcp.example.com. 10800 IN SRV 0 0 8443 example.com.
-_ischedules._tcp.example.com. 10800 IN SRV 0 0 8443 example.com.
-
-_ischedule._domainkey.example.com. 10800 IN TXT "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjUfDqd8ICAL0dyq2KdjKN6LS8O/Y4yMxOxgATqtSIMi7baKXEs1w5Wj9efOC2nU+aqyhP2/J6AzfFJfSB+GV5gcIT+LAC4btJKPGjPUyXcQFJV4a73y0jIgCTBzWxdaP6qD9P9rzYlvMPcdrrKiKoAOtI3JZqAAdZudOmGlc4QQIDAQAB"
-_revoked._domainkey.example.com. 10800 IN TXT "v=DKIM1; p="
-
-_domainkey._tcp.example.com. 10800 IN SRV 0 0 8443 key.example.com.
-_domainkey._tcp.www.example.com. 10800 IN SRV 0 0 80 key.example.com.
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/data/db.example.com)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.example.com 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,19 @@
+example.com. 10800 IN SOA ns.example.com. admin.example.com. (
+ 2012090810 ; serial
+ 3600 ; refresh (1 hour)
+ 900 ; retry (15 minutes)
+ 1209600 ; expire (2 weeks)
+ 86400 ; minimum (1 day)
+ )
+ 10800 IN NS ns.example.com.
+ 10800 IN A 127.0.0.1
+ns.example.com. 10800 IN A 127.0.0.1
+
+_caldavs._tcp.example.com. 10800 IN SRV 0 0 8443 example.com.
+_ischedules._tcp.example.com. 10800 IN SRV 0 0 8443 example.com.
+
+_ischedule._domainkey.example.com. 10800 IN TXT "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjUfDqd8ICAL0dyq2KdjKN6LS8O/Y4yMxOxgATqtSIMi7baKXEs1w5Wj9efOC2nU+aqyhP2/J6AzfFJfSB+GV5gcIT+LAC4btJKPGjPUyXcQFJV4a73y0jIgCTBzWxdaP6qD9P9rzYlvMPcdrrKiKoAOtI3JZqAAdZudOmGlc4QQIDAQAB"
+_revoked._domainkey.example.com. 10800 IN TXT "v=DKIM1; p="
+
+_domainkey._tcp.example.com. 10800 IN SRV 0 0 8443 key.example.com.
+_domainkey._tcp.www.example.com. 10800 IN SRV 0 0 80 key.example.com.
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.two.zones
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/data/db.two.zones 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.two.zones 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,31 +0,0 @@
-example.com. 10800 IN SOA ns.example.com. admin.example.com. (
- 2012090810 ; serial
- 3600 ; refresh (1 hour)
- 900 ; retry (15 minutes)
- 1209600 ; expire (2 weeks)
- 86400 ; minimum (1 day)
- )
- 10800 IN NS ns.example.com.
- 10800 IN A 127.0.0.1
-ns.example.com. 10800 IN A 127.0.0.1
-
-_caldavs._tcp.example.com. 10800 IN SRV 0 0 8443 example.com.
-_ischedules._tcp.example.com. 10800 IN SRV 0 0 8443 example.com.
-_ischedule._domainkey.example.com. 10800 IN TXT "v=DKIM1; p="
-
-
-
-example.org. 10800 IN SOA ns.example.org. admin.example.org. (
- 2012090810 ; serial
- 3600 ; refresh (1 hour)
- 900 ; retry (15 minutes)
- 1209600 ; expire (2 weeks)
- 86400 ; minimum (1 day)
- )
- 10800 IN NS ns.example.org.
- 10800 IN A 127.0.0.1
-ns.example.org. 10800 IN A 127.0.0.1
-
-_caldavs._tcp.example.org. 10800 IN SRV 0 0 8543 example.org.
-_ischedules._tcp.example.org. 10800 IN SRV 0 0 8543 example.org.
-_ischedule2._domainkey.example.org. 10800 IN TXT "v=DKIM1; s=ischedule; p="
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.two.zones (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/data/db.two.zones)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.two.zones (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/data/db.two.zones 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,31 @@
+example.com. 10800 IN SOA ns.example.com. admin.example.com. (
+ 2012090810 ; serial
+ 3600 ; refresh (1 hour)
+ 900 ; retry (15 minutes)
+ 1209600 ; expire (2 weeks)
+ 86400 ; minimum (1 day)
+ )
+ 10800 IN NS ns.example.com.
+ 10800 IN A 127.0.0.1
+ns.example.com. 10800 IN A 127.0.0.1
+
+_caldavs._tcp.example.com. 10800 IN SRV 0 0 8443 example.com.
+_ischedules._tcp.example.com. 10800 IN SRV 0 0 8443 example.com.
+_ischedule._domainkey.example.com. 10800 IN TXT "v=DKIM1; p="
+
+
+
+example.org. 10800 IN SOA ns.example.org. admin.example.org. (
+ 2012090810 ; serial
+ 3600 ; refresh (1 hour)
+ 900 ; retry (15 minutes)
+ 1209600 ; expire (2 weeks)
+ 86400 ; minimum (1 day)
+ )
+ 10800 IN NS ns.example.org.
+ 10800 IN A 127.0.0.1
+ns.example.org. 10800 IN A 127.0.0.1
+
+_caldavs._tcp.example.org. 10800 IN SRV 0 0 8543 example.org.
+_ischedules._tcp.example.org. 10800 IN SRV 0 0 8543 example.org.
+_ischedule2._domainkey.example.org. 10800 IN TXT "v=DKIM1; s=ischedule; p="
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_delivery.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,62 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import twistedcaldav.test.util
-from twisted.internet.defer import inlineCallbacks
-from twisted.python.modules import getModule
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule import utils
-from twisted.names import client
-from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
-
-class CalDAV (twistedcaldav.test.util.TestCase):
- """
- twistedcaldav.scheduling.caldav tests
- """
-
- def tearDown(self):
- """
- By setting the resolver to None, it will be recreated next time a name
- lookup is done.
- """
- client.theResolver = None
- utils.DebugResolver = None
-
-
- @inlineCallbacks
- def test_matchCalendarUserAddress(self):
- """
- Make sure we do an exact comparison on EmailDomain
- """
-
- self.patch(config.Scheduling.iSchedule, "RemoteServers", "")
-
- # Only mailtos:
- result = yield ScheduleViaISchedule.matchCalendarUserAddress("http://example.com/principal/user")
- self.assertFalse(result)
-
- # Need to setup a fake resolver
- module = getModule(__name__)
- dataPath = module.filePath.sibling("data")
- bindPath = dataPath.child("db.example.com")
- self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
- utils.DebugResolver = None
- utils._initResolver()
-
- result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.com")
- self.assertTrue(result)
- result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.org")
- self.assertFalse(result)
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_delivery.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_delivery.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,62 @@
+##
+# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import twistedcaldav.test.util
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.modules import getModule
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule import utils
+from twisted.names import client
+from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
+
+class CalDAV (twistedcaldav.test.util.TestCase):
+ """
+ twistedcaldav.scheduling.caldav tests
+ """
+
+ def tearDown(self):
+ """
+ By setting the resolver to None, it will be recreated next time a name
+ lookup is done.
+ """
+ client.theResolver = None
+ utils.DebugResolver = None
+
+
+ @inlineCallbacks
+ def test_matchCalendarUserAddress(self):
+ """
+ Make sure we do an exact comparison on EmailDomain
+ """
+
+ self.patch(config.Scheduling.iSchedule, "RemoteServers", "")
+
+ # Only mailtos:
+ result = yield ScheduleViaISchedule.matchCalendarUserAddress("http://example.com/principal/user")
+ self.assertFalse(result)
+
+ # Need to setup a fake resolver
+ module = getModule(__name__)
+ dataPath = module.filePath.sibling("data")
+ bindPath = dataPath.child("db.example.com")
+ self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+ utils.DebugResolver = None
+ utils._initResolver()
+
+ result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.com")
+ self.assertTrue(result)
+ result = yield ScheduleViaISchedule.matchCalendarUserAddress("mailto:user at example.org")
+ self.assertFalse(result)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_dkim.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_dkim.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_dkim.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,862 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from Crypto.PublicKey import RSA
-
-from twext.web2.http_headers import Headers, MimeType
-from twext.web2.stream import MemoryStream
-
-from twisted.internet.defer import inlineCallbacks, succeed
-from twisted.names import client
-from twisted.python.modules import getModule
-
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule import utils
-from twistedcaldav.scheduling.ischedule.dkim import DKIMRequest, DKIMVerifier, \
- DKIMVerificationError, DKIMUtils, PublicKeyLookup_DNSTXT, \
- PublicKeyLookup_HTTP_WellKnown, PublicKeyLookup_PrivateExchange
-
-import base64
-import hashlib
-import os
-import time
-import twistedcaldav.test.util
-
-class TestDKIMBase (twistedcaldav.test.util.TestCase):
- """
- DKIM support tests
- """
-
- class PublicKeyLookup_Testing(PublicKeyLookup_HTTP_WellKnown):
-
- keys = []
-
- def _lookupKeys(self):
- """
- Do the key lookup using the actual lookup method.
- """
- return succeed(self.keys)
-
-
- def setUp(self):
- super(TestDKIMBase, self).setUp()
-
- self.private_keyfile = self.mktemp()
- f = open(self.private_keyfile, "w")
- f.write("""-----BEGIN RSA PRIVATE KEY-----
-MIIEogIBAAKCAQEAw7bJxD1k5VSA5AqdfmJ7vj99oKQ4qYtSeJ5HiK6W40dzC++k
-LweUWLzeUErgXwcJlyOC6rqVVPBfSJq4l7yPdVqpWUo6s2jnUsSWOfhpre22yc4B
-K0QY2Euc3R+gT59eM0mtJPtWaQw5BmQ2GrV6f0OUiKi17jEPasKcxf1qZrWU0+Ik
-D2DhUCuRrNb/baUkuIkxoit6M7k7s5X9swT1hE/Eso0gS79FSti1fkDeoPZ296Gu
-5uYWdpaLl03Nr0w65Gbw+2v79AcwOyvbZD6y9xYGLWubic0dUeWuhUipZdmQf8Bd
-t7cZVgjQX/giQQqqLDFhfNFwapUZDhS7TCtujQIDAQABAoIBADfFuzHFHR+NOT3D
-GKaPghvxE+fXZJ5MKbBdypzUxAL4tXxNSkhsrIWtLN1MuSvbYYxEfmZNzYhrB3w1
-Oy1ieq9CqsfbM2c1GdaoVvcmJ1d9Sn2vyv19ZmcdBRKulIycKcgL0t+bEEDXTtjX
-beOmm8XwiD95dH7wVChkVTDGyq+BxtSY6wav9y15zWnBH7+BAeq3OnKaNIQB0iTI
-UA41jWocKYI18/6D5gQTDSoYvKB7saFVGw9IgmmHA/3rYztcHCxUoE15x7wWuwtF
-vzQanEt/QwEEFMibNTjvfIUPoeIeQH7MzcD56AL9u/cs8LNeSbappWE7BneQ0ll3
-CfTsAQECgYEA/eoDkpPMWxuoSrZ1oXLxeImEAB2EHNs4UV9dmcUkhNeYZP0rv7pL
-4jpkNHTRvFeeovy5khXhykb9BUYDuZy6rcELlFxpCKYRw3d+PPWM+wfqmJp+fIN7
-Z4F1Kpznt0F2e+9LXF1Qi5bM2dHy1maxEjaBUIOIoczbjJJDmNN8zR0CgYEAxVJg
-2VCpjaRoJtaZYeserkVgB8SFffBnm/8XQv8uTbKrz104t9nFyezbINmKrQs3cxT3
-1+PiVbLJpPRcik129x4xIlz3zapsMqwXL97Lz92vXm/nELRnV8d+F9SxVzlijRDL
-rvl3X3Vayq2zKb6euBOwOu8UnQO3xJkTtLPtHDECgYAptxuVJkEJqtaQR7+1oZu4
-UOdl2XOOBhoPjFplW/Uu+fiohst8OVAkP7GcyKB4j/CZGGoobP3mbJk/F4yfHvew
-eim72x7Kc/YxJd2QiEr8JwXMwn0LWdKZY7RrJtIO0mtz2xGHgDEubb0EADEkNkTb
-GCdQoft9kZl0U8dVQVGcpQKBgHsvjIre0ps8slDc1HDO6h597Q+sXnJbLTO0Mv9+
-c5fKHXydhBUy/UmsdrixVuPlBr7vrjK3b8t0jHJQo50r80MfNClxxLo+1MFlsiwO
-eUrR6POaBLTnC0U/o7aY8AW2K5JJk/8uepm7l+zEN/+to0Tj9bc1HrdPZOB1eFnt
-oe9hAoGAEwwDhNrmSlZjmZMT8WehCdyS7zQgI8OCLAlu9KTiwFzoSDcnhVbS4nd4
-iblllHCLZ2Q/rHSH3cQor94kxePm+b3KH9ZwAgInMModuSPcScrR5/vsORZCtJEO
-CAXnxZHhrExMGIIa7KV33W5v7Hstl7SnPWKFgCvlBH2QoMTjoUE=
------END RSA PRIVATE KEY-----
-""")
- f.close()
-
- pkey_data = """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw7bJxD1k5VSA5AqdfmJ7
-vj99oKQ4qYtSeJ5HiK6W40dzC++kLweUWLzeUErgXwcJlyOC6rqVVPBfSJq4l7yP
-dVqpWUo6s2jnUsSWOfhpre22yc4BK0QY2Euc3R+gT59eM0mtJPtWaQw5BmQ2GrV6
-f0OUiKi17jEPasKcxf1qZrWU0+IkD2DhUCuRrNb/baUkuIkxoit6M7k7s5X9swT1
-hE/Eso0gS79FSti1fkDeoPZ296Gu5uYWdpaLl03Nr0w65Gbw+2v79AcwOyvbZD6y
-9xYGLWubic0dUeWuhUipZdmQf8Bdt7cZVgjQX/giQQqqLDFhfNFwapUZDhS7TCtu
-jQIDAQAB
-"""
- self.public_keyfile = self.mktemp()
- f = open(self.public_keyfile, "w")
- f.write("""-----BEGIN PUBLIC KEY-----
-%s-----END PUBLIC KEY-----
-""" % (pkey_data,))
- f.close()
- self.public_key_data = pkey_data.replace("\n", "")
-
-
-
-class TestDKIMRequest (TestDKIMBase):
- """
- L{DKIMRequest} support tests.
- """
-
- @inlineCallbacks
- def test_body_hash(self):
-
- data = "Hello World!"
- for algorithm, hash_method in (
- ("rsa-sha1", hashlib.sha1,),
- ("rsa-sha256", hashlib.sha256,),
- ):
- stream = str(data)
- headers = Headers()
- headers.addRawHeader("Originator", "mailto:user01 at example.com")
- headers.addRawHeader("Recipient", "mailto:user02 at example.com")
- headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
- request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", "/tmp/key", algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
- hash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
- result = (yield request.bodyHash())
- self.assertEqual(result, hash)
-
-
- def test_generateSignature(self):
-
- data = "Hello World!"
-
- for algorithm, hash_method in (
- ("rsa-sha1", hashlib.sha1,),
- ("rsa-sha256", hashlib.sha256,),
- ):
- stream = MemoryStream(data)
- headers = Headers()
- headers.addRawHeader("Originator", "mailto:user01 at example.com")
- headers.addRawHeader("Recipient", "mailto:user02 at example.com")
- headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
- request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
-
- # Manually create what should be the correct thing to sign
- bodyhash = base64.b64encode(hash_method(data).digest())
- sign_this = """originator:mailto:user01 at example.com
-recipient:mailto:user02 at example.com
-content-type:%s
-ischedule-version:1.0
-dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], str(int(time.time())), str(int(time.time() + 3600)), algorithm, bodyhash)
-
- result = request.generateSignature(sign_this)
-
- key = RSA.importKey(open(self.private_keyfile).read())
- signature = DKIMUtils.sign(sign_this, key, DKIMUtils.hash_func(algorithm))
-
- self.assertEqual(result, signature)
-
-
- @inlineCallbacks
- def test_signatureHeaders(self):
-
- data = "Hello World!"
-
- for algorithm, hash_method in (
- ("rsa-sha1", hashlib.sha1,),
- ("rsa-sha256", hashlib.sha256,),
- ):
- stream = MemoryStream(data)
- headers = Headers()
- headers.addRawHeader("Originator", "mailto:user01 at example.com")
- headers.addRawHeader("Recipient", "mailto:user02 at example.com")
- headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
- request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
- result, _ignore_tags = (yield request.signatureHeaders())
-
- # Manually create what should be the correct thing to sign
- bodyhash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
- sign_this = """originator:mailto:user01 at example.com
-recipient:mailto:user02 at example.com
-content-type:%s
-ischedule-version:1.0
-ischedule-message-id:%s
-dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.message_id, request.time, request.expire, algorithm, bodyhash)
-
- self.assertEqual(result, sign_this)
-
-
- @inlineCallbacks
- def test_sign(self):
-
- data = "Hello World!"
- for algorithm, hash_method in (
- ("rsa-sha1", hashlib.sha1,),
- ("rsa-sha256", hashlib.sha256,),
- ):
- stream = MemoryStream(data)
- headers = Headers()
- headers.addRawHeader("Originator", "mailto:user01 at example.com")
- headers.addRawHeader("Recipient", "mailto:user02 at example.com")
- headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
- request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
- result = (yield request.sign())
-
- # Manually create what should be the correct thing to sign and make sure signatures match
- bodyhash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
- sign_this = """originator:mailto:user01 at example.com
-recipient:mailto:user02 at example.com
-content-type:%s
-ischedule-version:1.0
-ischedule-message-id:%s
-dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.message_id, request.time, request.expire, algorithm, bodyhash)
- key = RSA.importKey(open(self.private_keyfile).read())
- signature = DKIMUtils.sign(sign_this, key, DKIMUtils.hash_func(algorithm))
-
- self.assertEqual(result, signature)
-
- # Make sure header is updated in the request
- updated_header = "v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=%s" % (request.time, request.expire, algorithm, bodyhash, signature,)
- self.assertEqual(request.headers.getRawHeaders("DKIM-Signature")[0], updated_header)
-
- # Try to verify result using public key
- pubkey = RSA.importKey(open(self.public_keyfile).read())
- self.assertEqual(DKIMUtils.verify(sign_this, result, pubkey, DKIMUtils.hash_func(algorithm)), None)
-
-
-
-class TestDKIMVerifier (TestDKIMBase):
- """
- L{DKIMVerifier} support tests.
- """
-
- class StubRequest(object):
-
- def __init__(self, method, uri, headers, body):
- self.method = method
- self.uri = uri
- self.headers = Headers()
- for name, value in headers:
- self.headers.addRawHeader(name, value)
- self.stream = MemoryStream(body)
-
-
- def test_valid_dkim_headers(self):
- """
- L{DKIMVerifier.processDKIMHeader} correctly validates DKIM-Signature headers.
- """
-
- data = (
- # Bogus
- ((("DKIM-Signature", "v=1"),), False,),
-
- # More than one
- ((
- ("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),
- ("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha256; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),
- ), False,),
-
- # Valid
- ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), True,),
- ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha256; q=dns/txt; http=UE9TVDov; c=relaxed; h=Originator:Recipient; bh=abc; b=def"),), True,),
- ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=UE9TVDov; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() + 30),)),), True,),
-
- # Invalid
- ((("DKIM-Signature", "v=2; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
- ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha512; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
- ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/relaxed; h=Originator:Recipient; bh=abc; b=def"),), False,),
- ((("DKIM-Signature", "v=1; d=example.com; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
- ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=UE9TVDov; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
- ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
- ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=UE9TVDovaXNjaGVkdWxl; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
- ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=POST:/; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
- )
-
- for headers, result in data:
- request = self.StubRequest("POST", "/", headers, "")
- verifier = DKIMVerifier(request)
- if result:
- verifier.processDKIMHeader()
- else:
- self.assertRaises(DKIMVerificationError, verifier.processDKIMHeader)
-
-
- def test_canonicalize_header(self):
- """
- L{DKIMVerifier.canonicalizeHeader} correctly canonicalizes headers.
- """
-
- data = (
- ("Content-Type", " text/calendar ; charset = \"utf-8\" ", "content-type:text/calendar ; charset = \"utf-8\"\r\n"),
- ("Originator", " mailto:user01 at example.com ", "originator:mailto:user01 at example.com\r\n"),
- ("Recipient", " mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t ", "recipient:mailto:user02 at example.com , mailto:user03 at example.com\r\n"),
- ("iSchedule-Version", " 1.0 ", "ischedule-version:1.0\r\n"),
- (
- "DKIM-Signature",
- " v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def",
- "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=",
- ),
- (
- "DKIM-Signature",
- " v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; b= def ; http=\tUE9TVDov ; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc",
- "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; b= ; http= UE9TVDov ; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc",
- ),
- )
-
- for name, value, result in data:
- request = self.StubRequest("POST", "/", ((name, value,),), "")
- verifier = DKIMVerifier(request)
- if name == "DKIM-Signature":
- verifier.processDKIMHeader()
- canonicalized = DKIMUtils.canonicalizeHeader(name, value, remove_b=verifier.dkim_tags["b"] if name == "DKIM-Signature" else None)
- self.assertEqual(canonicalized, result)
-
-
- def test_extract_headers(self):
- """
- L{DKIMVerifier.extractSignedHeaders} correctly extracts canonicalizes headers.
- """
-
- data = (
- # Over count on Recipient
- ("""Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-iSchedule-Version: 1.0
-DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
-Cache-Control:no-cache
-Connection:close
-""",
- """content-type:text/calendar ; charset = "utf-8"
-originator:mailto:user01 at example.com
-recipient:mailto:user02 at example.com , mailto:user03 at example.com
-ischedule-version:1.0
-dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
- ),
- # Exact count on Recipient
- ("""Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-Recipient:\t\t mailto:user04 at example.com
-iSchedule-Version: 1.0
-DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
-Cache-Control:no-cache
-Connection:close
-""",
- """content-type:text/calendar ; charset = "utf-8"
-originator:mailto:user01 at example.com
-recipient:mailto:user04 at example.com
-recipient:mailto:user02 at example.com , mailto:user03 at example.com
-ischedule-version:1.0
-dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
- ),
- # Under count on Recipient
- ("""Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-iSchedule-Version: 1.0
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-Recipient:\t\t mailto:user04 at example.com
-Recipient:\t\t mailto:user05 at example.com
-DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
-Cache-Control:no-cache
-Connection:close
-""",
- """content-type:text/calendar ; charset = "utf-8"
-originator:mailto:user01 at example.com
-recipient:mailto:user05 at example.com
-recipient:mailto:user04 at example.com
-ischedule-version:1.0
-dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
- ),
- # Re-ordered Content-Type
- ("""Host:example.com
-iSchedule-Version: 1.0
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
-Content-Type: text/calendar ; charset = "utf-8"
-Cache-Control:no-cache
-Connection:close
-""",
- """content-type:text/calendar ; charset = "utf-8"
-originator:mailto:user01 at example.com
-recipient:mailto:user02 at example.com , mailto:user03 at example.com
-ischedule-version:1.0
-dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
- ),
- )
-
- for hdrs, result in data:
- headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
- request = self.StubRequest("POST", "/", headers, "")
- verifier = DKIMVerifier(request)
- verifier.processDKIMHeader()
- extracted = verifier.extractSignedHeaders()
- self.assertEqual(extracted, result.replace("\n", "\r\n"))
-
-
- def test_locate_public_key(self):
- """
- L{DKIMVerifier.locatePublicKey} correctly finds key matching headers.
- """
-
- data = (
- # Valid
- ("""Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
-Cache-Control:no-cache
-Connection:close
-""",
- [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
- True,
- ),
- # Invalid - no method
- ("""Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
-Cache-Control:no-cache
-Connection:close
-""",
- [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
- False,
- ),
- # Invalid - wrong algorithm
- ("""Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
-Cache-Control:no-cache
-Connection:close
-""",
- [DKIMUtils.extractTags("v=DKIM1; h=sha-1; p=%s" % (self.public_key_data,))],
- False,
- ),
- )
-
- for hdrs, keys, result in data:
- headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
- request = self.StubRequest("POST", "/", headers, "")
- TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
- verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
- verifier.processDKIMHeader()
- pkey = (yield verifier.locatePublicKey())
- if result:
- self.assertNotEqual(pkey, None)
- else:
- self.assertEqual(pkey, None)
-
-
- @inlineCallbacks
- def test_verify(self):
- """
- L{DKIMVerifier.verify} correctly finds key matching headers.
- """
-
- @inlineCallbacks
- def _verify(hdrs, body, keys, result, sign_headers=("Originator", "Recipient", "Content-Type",), manipulate_request=None):
- for algorithm in ("rsa-sha1", "rsa-sha256",):
- # Create signature
- stream = MemoryStream(body)
- headers = Headers()
- for name, value in [hdr.split(":", 1) for hdr in hdrs.splitlines()]:
- headers.addRawHeader(name, value)
- request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, sign_headers, True, True, True, 3600)
- yield request.sign()
-
- # Possibly munge the request after the signature is done
- if manipulate_request is not None:
- manipulate_request(request)
-
- # Verify signature
- TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
- verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
- TestPublicKeyLookup.PublicKeyLookup_Testing({}).flushCache()
- try:
- yield verifier.verify()
- except Exception, e:
- if result:
- self.fail("DKIMVerifier:verify failed: %s" % (e,))
- else:
- if not result:
- self.fail("DKIMVerifier:verify did not fail")
-
- # Valid
- yield _verify(
- """Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-Cache-Control:no-cache
-Connection:close
-""",
- """BEGIN:DATA
-END:DATA
-""",
- [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
- True,
- )
-
- # Invalid - key revoked
- yield _verify(
- """Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-Cache-Control:no-cache
-Connection:close
-""",
- """BEGIN:DATA
-END:DATA
-""",
- [DKIMUtils.extractTags("v=DKIM1; p=")],
- False,
- )
-
- # Invalid - missing header
- yield _verify(
- """Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-Cache-Control:no-cache
-Connection:close
-""",
- """BEGIN:DATA
-END:DATA
-""",
- [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
- False,
- manipulate_request=lambda request: request.headers.removeHeader("Originator")
- )
-
- # Invalid - changed header
- yield _verify(
- """Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-Cache-Control:no-cache
-Connection:close
-""",
- """BEGIN:DATA
-END:DATA
-""",
- [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
- False,
- manipulate_request=lambda request: request.headers.setRawHeaders("Originator", ("mailto:user04 at example.com",))
- )
-
- # Invalid - changed body
- yield _verify(
- """Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-Cache-Control:no-cache
-Connection:close
-""",
- """BEGIN:DATA
-END:DATA
-""",
- [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
- False,
- manipulate_request=lambda request: setattr(request, "stream", MemoryStream("BEGIN:DATA\n")),
- )
-
- # Valid - extra header no over sign
- yield _verify(
- """Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-Cache-Control:no-cache
-Connection:close
-""",
- """BEGIN:DATA
-END:DATA
-""",
- [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
- True,
- manipulate_request=lambda request: request.headers.getRawHeaders("Recipient").insert(0, "mailto:user04 at example.com"),
- )
-
- # Valid - over sign header
- yield _verify(
- """Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-Cache-Control:no-cache
-Connection:close
-""",
- """BEGIN:DATA
-END:DATA
-""",
- [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
- False,
- sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
- )
-
- # Invalid - over sign header extra header
- yield _verify(
- """Host:example.com
-Content-Type: text/calendar ; charset = "utf-8"
-Originator: mailto:user01 at example.com
-Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
-Cache-Control:no-cache
-Connection:close
-""",
- """BEGIN:DATA
-END:DATA
-""",
- [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
- False,
- sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
- manipulate_request=lambda request: request.headers.addRawHeader("Recipient", ("mailto:user04 at example.com",))
- )
-
-
-
-class TestPublicKeyLookup (TestDKIMBase):
- """
- L{PublicKeyLookup} support tests.
- """
-
- def tearDown(self):
- """
- By setting the resolver to None, it will be recreated next time a name
- lookup is done.
- """
- client.theResolver = None
- utils.DebugResolver = None
-
-
- def test_selector_key(self):
-
- for lookup, d, result in (
- (PublicKeyLookup_DNSTXT, "example.com", "dkim._domainkey.example.com"),
- (PublicKeyLookup_DNSTXT, "calendar.example.com", "dkim._domainkey.calendar.example.com"),
- (PublicKeyLookup_HTTP_WellKnown, "example.com", "https://example.com/.well-known/domainkey/example.com/dkim"),
- (PublicKeyLookup_HTTP_WellKnown, "calendar.example.com", "https://example.com/.well-known/domainkey/calendar.example.com/dkim"),
- (PublicKeyLookup_PrivateExchange, "example.com", "example.com#dkim"),
- (PublicKeyLookup_PrivateExchange, "calendar.example.com", "calendar.example.com#dkim"),
- ):
- dkim = "v=1; d=%s; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d,)
- tester = lookup(DKIMUtils.extractTags(dkim))
- self.assertEqual(tester._getSelectorKey(), result)
-
-
- @inlineCallbacks
- def test_get_key(self):
-
- # Valid
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))]
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is not None)
-
- # Valid with more tags
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=ischedule ; p=%s" % (self.public_key_data,))]
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is not None)
-
- # Invalid - key type
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=dsa ; p=%s" % (self.public_key_data,))]
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is None)
-
- # Invalid - hash
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; h=sha512 ; p=%s" % (self.public_key_data,))]
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is None)
-
- # Invalid - service
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p=%s" % (self.public_key_data,))]
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is None)
-
- # Invalid - revoked
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p=")]
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is None)
-
- # Multiple valid
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = [
- DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,)),
- DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=ischedule ; p=%s" % (self.public_key_data,)),
- DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=* ; p=%s" % (self.public_key_data,)),
- ]
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is not None)
-
- # Multiple - some valid, some invalid
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = [
- DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
- DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,)),
- DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=ischedule ; p=%s" % (self.public_key_data,)),
- DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=* ; p=%s" % (self.public_key_data,)),
- ]
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is not None)
-
- # Multiple - invalid
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = [
- DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
- DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
- ]
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is None)
-
-
- @inlineCallbacks
- def test_cached_key(self):
-
- # Create cache entry
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))]
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is not None)
-
- # Cache valid
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.keys = []
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is not None)
-
- # Cache invalid
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- lookup.keys = []
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is None)
-
-
- @inlineCallbacks
- def test_TXT_key(self):
-
- # Need to setup a fake resolver
- module = getModule(__name__)
- dataPath = module.filePath.sibling("data")
- bindPath = dataPath.child("db.example.com")
- self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
- utils.DebugResolver = None
- utils._initResolver()
-
- for d, s, result in (
- ("example.com", "_ischedule", True),
- ("example.com", "_revoked", False),
- ("example.com", "dkim", False),
- ("calendar.example.com", "_ischedule", False),
- ("example.org", "_ischedule", False),
- ):
- dkim = "v=1; d=%s; s = %s; t = 1234; a=rsa-sha1; q=dns/txt ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d, s,)
- tester = PublicKeyLookup_DNSTXT(DKIMUtils.extractTags(dkim))
- pkey = yield tester.getPublicKey(False)
- self.assertEqual(pkey is not None, result)
-
-
- @inlineCallbacks
- def test_HTTP_URI_key(self):
-
- # Need to setup a fake resolver
- module = getModule(__name__)
- dataPath = module.filePath.sibling("data")
- bindPath = dataPath.child("db.example.com")
- self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
- utils.DebugResolver = None
- utils._initResolver()
-
- for d, s, result in (
- ("example.com", "_ischedule", "https://key.example.com:8443/.well-known/domainkey/example.com/_ischedule"),
- ("www.example.com", "_ischedule", "http://key.example.com/.well-known/domainkey/www.example.com/_ischedule"),
- ("example.org", "_ischedule", "https://example.org/.well-known/domainkey/example.org/_ischedule"),
- ):
- dkim = "v=1; d=%s; s = %s; t = 1234; a=rsa-sha1; q=http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d, s,)
- tester = PublicKeyLookup_HTTP_WellKnown(DKIMUtils.extractTags(dkim))
- uri = (yield tester._getURI())
- self.assertEqual(uri, result)
-
-
- @inlineCallbacks
- def test_private_exchange(self):
-
- keydir = self.mktemp()
- PublicKeyLookup_PrivateExchange.directory = keydir
- os.mkdir(keydir)
- keyfile = os.path.join(keydir, "example.com#dkim")
- with open(keyfile, "w") as f:
- f.write("""v=DKIM1; p=%s
-""" % (self.public_key_data,))
-
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is not None)
-
- dkim = "v=1; d=example.com; s = dkim2; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is None)
-
- with open(keyfile, "w") as f:
- f.write("""v=DKIM1; s=email; p=%s
-""" % (self.public_key_data,))
-
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is None)
-
- with open(keyfile, "w") as f:
- f.write("""v=DKIM1; s=email; p=%s
-v=DKIM1; s=ischedule; p=%s
-""" % (self.public_key_data, self.public_key_data,))
-
- dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
- lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
- lookup.flushCache()
- pubkey = (yield lookup.getPublicKey())
- self.assertTrue(pubkey is not None)
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_dkim.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_dkim.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_dkim.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_dkim.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,862 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from Crypto.PublicKey import RSA
+
+from twext.web2.http_headers import Headers, MimeType
+from twext.web2.stream import MemoryStream
+
+from twisted.internet.defer import inlineCallbacks, succeed
+from twisted.names import client
+from twisted.python.modules import getModule
+
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule import utils
+from twistedcaldav.scheduling.ischedule.dkim import DKIMRequest, DKIMVerifier, \
+ DKIMVerificationError, DKIMUtils, PublicKeyLookup_DNSTXT, \
+ PublicKeyLookup_HTTP_WellKnown, PublicKeyLookup_PrivateExchange
+
+import base64
+import hashlib
+import os
+import time
+import twistedcaldav.test.util
+
+class TestDKIMBase (twistedcaldav.test.util.TestCase):
+ """
+ DKIM support tests
+ """
+
+ class PublicKeyLookup_Testing(PublicKeyLookup_HTTP_WellKnown):
+
+ keys = []
+
+ def _lookupKeys(self):
+ """
+ Do the key lookup using the actual lookup method.
+ """
+ return succeed(self.keys)
+
+
+ def setUp(self):
+ super(TestDKIMBase, self).setUp()
+
+ self.private_keyfile = self.mktemp()
+ f = open(self.private_keyfile, "w")
+ f.write("""-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAw7bJxD1k5VSA5AqdfmJ7vj99oKQ4qYtSeJ5HiK6W40dzC++k
+LweUWLzeUErgXwcJlyOC6rqVVPBfSJq4l7yPdVqpWUo6s2jnUsSWOfhpre22yc4B
+K0QY2Euc3R+gT59eM0mtJPtWaQw5BmQ2GrV6f0OUiKi17jEPasKcxf1qZrWU0+Ik
+D2DhUCuRrNb/baUkuIkxoit6M7k7s5X9swT1hE/Eso0gS79FSti1fkDeoPZ296Gu
+5uYWdpaLl03Nr0w65Gbw+2v79AcwOyvbZD6y9xYGLWubic0dUeWuhUipZdmQf8Bd
+t7cZVgjQX/giQQqqLDFhfNFwapUZDhS7TCtujQIDAQABAoIBADfFuzHFHR+NOT3D
+GKaPghvxE+fXZJ5MKbBdypzUxAL4tXxNSkhsrIWtLN1MuSvbYYxEfmZNzYhrB3w1
+Oy1ieq9CqsfbM2c1GdaoVvcmJ1d9Sn2vyv19ZmcdBRKulIycKcgL0t+bEEDXTtjX
+beOmm8XwiD95dH7wVChkVTDGyq+BxtSY6wav9y15zWnBH7+BAeq3OnKaNIQB0iTI
+UA41jWocKYI18/6D5gQTDSoYvKB7saFVGw9IgmmHA/3rYztcHCxUoE15x7wWuwtF
+vzQanEt/QwEEFMibNTjvfIUPoeIeQH7MzcD56AL9u/cs8LNeSbappWE7BneQ0ll3
+CfTsAQECgYEA/eoDkpPMWxuoSrZ1oXLxeImEAB2EHNs4UV9dmcUkhNeYZP0rv7pL
+4jpkNHTRvFeeovy5khXhykb9BUYDuZy6rcELlFxpCKYRw3d+PPWM+wfqmJp+fIN7
+Z4F1Kpznt0F2e+9LXF1Qi5bM2dHy1maxEjaBUIOIoczbjJJDmNN8zR0CgYEAxVJg
+2VCpjaRoJtaZYeserkVgB8SFffBnm/8XQv8uTbKrz104t9nFyezbINmKrQs3cxT3
+1+PiVbLJpPRcik129x4xIlz3zapsMqwXL97Lz92vXm/nELRnV8d+F9SxVzlijRDL
+rvl3X3Vayq2zKb6euBOwOu8UnQO3xJkTtLPtHDECgYAptxuVJkEJqtaQR7+1oZu4
+UOdl2XOOBhoPjFplW/Uu+fiohst8OVAkP7GcyKB4j/CZGGoobP3mbJk/F4yfHvew
+eim72x7Kc/YxJd2QiEr8JwXMwn0LWdKZY7RrJtIO0mtz2xGHgDEubb0EADEkNkTb
+GCdQoft9kZl0U8dVQVGcpQKBgHsvjIre0ps8slDc1HDO6h597Q+sXnJbLTO0Mv9+
+c5fKHXydhBUy/UmsdrixVuPlBr7vrjK3b8t0jHJQo50r80MfNClxxLo+1MFlsiwO
+eUrR6POaBLTnC0U/o7aY8AW2K5JJk/8uepm7l+zEN/+to0Tj9bc1HrdPZOB1eFnt
+oe9hAoGAEwwDhNrmSlZjmZMT8WehCdyS7zQgI8OCLAlu9KTiwFzoSDcnhVbS4nd4
+iblllHCLZ2Q/rHSH3cQor94kxePm+b3KH9ZwAgInMModuSPcScrR5/vsORZCtJEO
+CAXnxZHhrExMGIIa7KV33W5v7Hstl7SnPWKFgCvlBH2QoMTjoUE=
+-----END RSA PRIVATE KEY-----
+""")
+ f.close()
+
+ pkey_data = """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw7bJxD1k5VSA5AqdfmJ7
+vj99oKQ4qYtSeJ5HiK6W40dzC++kLweUWLzeUErgXwcJlyOC6rqVVPBfSJq4l7yP
+dVqpWUo6s2jnUsSWOfhpre22yc4BK0QY2Euc3R+gT59eM0mtJPtWaQw5BmQ2GrV6
+f0OUiKi17jEPasKcxf1qZrWU0+IkD2DhUCuRrNb/baUkuIkxoit6M7k7s5X9swT1
+hE/Eso0gS79FSti1fkDeoPZ296Gu5uYWdpaLl03Nr0w65Gbw+2v79AcwOyvbZD6y
+9xYGLWubic0dUeWuhUipZdmQf8Bdt7cZVgjQX/giQQqqLDFhfNFwapUZDhS7TCtu
+jQIDAQAB
+"""
+ self.public_keyfile = self.mktemp()
+ f = open(self.public_keyfile, "w")
+ f.write("""-----BEGIN PUBLIC KEY-----
+%s-----END PUBLIC KEY-----
+""" % (pkey_data,))
+ f.close()
+ self.public_key_data = pkey_data.replace("\n", "")
+
+
+
+class TestDKIMRequest (TestDKIMBase):
+ """
+ L{DKIMRequest} support tests.
+ """
+
+ @inlineCallbacks
+ def test_body_hash(self):
+
+ data = "Hello World!"
+ for algorithm, hash_method in (
+ ("rsa-sha1", hashlib.sha1,),
+ ("rsa-sha256", hashlib.sha256,),
+ ):
+ stream = str(data)
+ headers = Headers()
+ headers.addRawHeader("Originator", "mailto:user01 at example.com")
+ headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+ headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
+ request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", "/tmp/key", algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
+ hash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
+ result = (yield request.bodyHash())
+ self.assertEqual(result, hash)
+
+
+ def test_generateSignature(self):
+
+ data = "Hello World!"
+
+ for algorithm, hash_method in (
+ ("rsa-sha1", hashlib.sha1,),
+ ("rsa-sha256", hashlib.sha256,),
+ ):
+ stream = MemoryStream(data)
+ headers = Headers()
+ headers.addRawHeader("Originator", "mailto:user01 at example.com")
+ headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+ headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
+ request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
+
+ # Manually create what should be the correct thing to sign
+ bodyhash = base64.b64encode(hash_method(data).digest())
+ sign_this = """originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com
+content-type:%s
+ischedule-version:1.0
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], str(int(time.time())), str(int(time.time() + 3600)), algorithm, bodyhash)
+
+ result = request.generateSignature(sign_this)
+
+ key = RSA.importKey(open(self.private_keyfile).read())
+ signature = DKIMUtils.sign(sign_this, key, DKIMUtils.hash_func(algorithm))
+
+ self.assertEqual(result, signature)
+
+
+ @inlineCallbacks
+ def test_signatureHeaders(self):
+
+ data = "Hello World!"
+
+ for algorithm, hash_method in (
+ ("rsa-sha1", hashlib.sha1,),
+ ("rsa-sha256", hashlib.sha256,),
+ ):
+ stream = MemoryStream(data)
+ headers = Headers()
+ headers.addRawHeader("Originator", "mailto:user01 at example.com")
+ headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+ headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
+ request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
+ result, _ignore_tags = (yield request.signatureHeaders())
+
+ # Manually create what should be the correct thing to sign
+ bodyhash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
+ sign_this = """originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com
+content-type:%s
+ischedule-version:1.0
+ischedule-message-id:%s
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.message_id, request.time, request.expire, algorithm, bodyhash)
+
+ self.assertEqual(result, sign_this)
+
+
+ @inlineCallbacks
+ def test_sign(self):
+
+ data = "Hello World!"
+ for algorithm, hash_method in (
+ ("rsa-sha1", hashlib.sha1,),
+ ("rsa-sha256", hashlib.sha256,),
+ ):
+ stream = MemoryStream(data)
+ headers = Headers()
+ headers.addRawHeader("Originator", "mailto:user01 at example.com")
+ headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+ headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component": "VEVENT", "charset": "utf-8"}))
+ request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, True, 3600)
+ result = (yield request.sign())
+
+ # Manually create what should be the correct thing to sign and make sure signatures match
+ bodyhash = base64.b64encode(hash_method(DKIMUtils.canonicalizeBody(data)).digest())
+ sign_this = """originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com
+content-type:%s
+ischedule-version:1.0
+ischedule-message-id:%s
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.message_id, request.time, request.expire, algorithm, bodyhash)
+ key = RSA.importKey(open(self.private_keyfile).read())
+ signature = DKIMUtils.sign(sign_this, key, DKIMUtils.hash_func(algorithm))
+
+ self.assertEqual(result, signature)
+
+ # Make sure header is updated in the request
+ updated_header = "v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=%s" % (request.time, request.expire, algorithm, bodyhash, signature,)
+ self.assertEqual(request.headers.getRawHeaders("DKIM-Signature")[0], updated_header)
+
+ # Try to verify result using public key
+ pubkey = RSA.importKey(open(self.public_keyfile).read())
+ self.assertEqual(DKIMUtils.verify(sign_this, result, pubkey, DKIMUtils.hash_func(algorithm)), None)
+
+
+
+class TestDKIMVerifier (TestDKIMBase):
+ """
+ L{DKIMVerifier} support tests.
+ """
+
+ class StubRequest(object):
+
+ def __init__(self, method, uri, headers, body):
+ self.method = method
+ self.uri = uri
+ self.headers = Headers()
+ for name, value in headers:
+ self.headers.addRawHeader(name, value)
+ self.stream = MemoryStream(body)
+
+
+ def test_valid_dkim_headers(self):
+ """
+ L{DKIMVerifier.processDKIMHeader} correctly validates DKIM-Signature headers.
+ """
+
+ data = (
+ # Bogus
+ ((("DKIM-Signature", "v=1"),), False,),
+
+ # More than one
+ ((
+ ("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),
+ ("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha256; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),
+ ), False,),
+
+ # Valid
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), True,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha256; q=dns/txt; http=UE9TVDov; c=relaxed; h=Originator:Recipient; bh=abc; b=def"),), True,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=UE9TVDov; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() + 30),)),), True,),
+
+ # Invalid
+ ((("DKIM-Signature", "v=2; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha512; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/relaxed; h=Originator:Recipient; bh=abc; b=def"),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; t=1234; a=rsa-sha1; q=dns/txt:http/well-known; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=UE9TVDov; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=UE9TVDovaXNjaGVkdWxl; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; http=POST:/; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
+ )
+
+ for headers, result in data:
+ request = self.StubRequest("POST", "/", headers, "")
+ verifier = DKIMVerifier(request)
+ if result:
+ verifier.processDKIMHeader()
+ else:
+ self.assertRaises(DKIMVerificationError, verifier.processDKIMHeader)
+
+
+ def test_canonicalize_header(self):
+ """
+ L{DKIMVerifier.canonicalizeHeader} correctly canonicalizes headers.
+ """
+
+ data = (
+ ("Content-Type", " text/calendar ; charset = \"utf-8\" ", "content-type:text/calendar ; charset = \"utf-8\"\r\n"),
+ ("Originator", " mailto:user01 at example.com ", "originator:mailto:user01 at example.com\r\n"),
+ ("Recipient", " mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t ", "recipient:mailto:user02 at example.com , mailto:user03 at example.com\r\n"),
+ ("iSchedule-Version", " 1.0 ", "ischedule-version:1.0\r\n"),
+ (
+ "DKIM-Signature",
+ " v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def",
+ "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=",
+ ),
+ (
+ "DKIM-Signature",
+ " v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; b= def ; http=\tUE9TVDov ; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc",
+ "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; b= ; http= UE9TVDov ; c=relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc",
+ ),
+ )
+
+ for name, value, result in data:
+ request = self.StubRequest("POST", "/", ((name, value,),), "")
+ verifier = DKIMVerifier(request)
+ if name == "DKIM-Signature":
+ verifier.processDKIMHeader()
+ canonicalized = DKIMUtils.canonicalizeHeader(name, value, remove_b=verifier.dkim_tags["b"] if name == "DKIM-Signature" else None)
+ self.assertEqual(canonicalized, result)
+
+
+ def test_extract_headers(self):
+ """
+ L{DKIMVerifier.extractSignedHeaders} correctly extracts canonicalizes headers.
+ """
+
+ data = (
+ # Over count on Recipient
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+iSchedule-Version: 1.0
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ """content-type:text/calendar ; charset = "utf-8"
+originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com , mailto:user03 at example.com
+ischedule-version:1.0
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
+ ),
+ # Exact count on Recipient
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Recipient:\t\t mailto:user04 at example.com
+iSchedule-Version: 1.0
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ """content-type:text/calendar ; charset = "utf-8"
+originator:mailto:user01 at example.com
+recipient:mailto:user04 at example.com
+recipient:mailto:user02 at example.com , mailto:user03 at example.com
+ischedule-version:1.0
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
+ ),
+ # Under count on Recipient
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+iSchedule-Version: 1.0
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Recipient:\t\t mailto:user04 at example.com
+Recipient:\t\t mailto:user05 at example.com
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ """content-type:text/calendar ; charset = "utf-8"
+originator:mailto:user01 at example.com
+recipient:mailto:user05 at example.com
+recipient:mailto:user04 at example.com
+ischedule-version:1.0
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
+ ),
+ # Re-ordered Content-Type
+ ("""Host:example.com
+iSchedule-Version: 1.0
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+Content-Type: text/calendar ; charset = "utf-8"
+Cache-Control:no-cache
+Connection:close
+""",
+ """content-type:text/calendar ; charset = "utf-8"
+originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com , mailto:user03 at example.com
+ischedule-version:1.0
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="""
+ ),
+ )
+
+ for hdrs, result in data:
+ headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
+ request = self.StubRequest("POST", "/", headers, "")
+ verifier = DKIMVerifier(request)
+ verifier.processDKIMHeader()
+ extracted = verifier.extractSignedHeaders()
+ self.assertEqual(extracted, result.replace("\n", "\r\n"))
+
+
+ def test_locate_public_key(self):
+ """
+ L{DKIMVerifier.locatePublicKey} correctly finds key matching headers.
+ """
+
+ data = (
+ # Valid
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ True,
+ ),
+ # Invalid - no method
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ ),
+ # Invalid - wrong algorithm
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http/well-known\t\t; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ [DKIMUtils.extractTags("v=DKIM1; h=sha-1; p=%s" % (self.public_key_data,))],
+ False,
+ ),
+ )
+
+ for hdrs, keys, result in data:
+ headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
+ request = self.StubRequest("POST", "/", headers, "")
+ TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
+ verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
+ verifier.processDKIMHeader()
+ pkey = (yield verifier.locatePublicKey())
+ if result:
+ self.assertNotEqual(pkey, None)
+ else:
+ self.assertEqual(pkey, None)
+
+
+ @inlineCallbacks
+ def test_verify(self):
+ """
+ L{DKIMVerifier.verify} correctly finds key matching headers.
+ """
+
+ @inlineCallbacks
+ def _verify(hdrs, body, keys, result, sign_headers=("Originator", "Recipient", "Content-Type",), manipulate_request=None):
+ for algorithm in ("rsa-sha1", "rsa-sha256",):
+ # Create signature
+ stream = MemoryStream(body)
+ headers = Headers()
+ for name, value in [hdr.split(":", 1) for hdr in hdrs.splitlines()]:
+ headers.addRawHeader(name, value)
+ request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, sign_headers, True, True, True, 3600)
+ yield request.sign()
+
+ # Possibly munge the request after the signature is done
+ if manipulate_request is not None:
+ manipulate_request(request)
+
+ # Verify signature
+ TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
+ verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
+ TestPublicKeyLookup.PublicKeyLookup_Testing({}).flushCache()
+ try:
+ yield verifier.verify()
+ except Exception, e:
+ if result:
+ self.fail("DKIMVerifier:verify failed: %s" % (e,))
+ else:
+ if not result:
+ self.fail("DKIMVerifier:verify did not fail")
+
+ # Valid
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ True,
+ )
+
+ # Invalid - key revoked
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=")],
+ False,
+ )
+
+ # Invalid - missing header
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ manipulate_request=lambda request: request.headers.removeHeader("Originator")
+ )
+
+ # Invalid - changed header
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ manipulate_request=lambda request: request.headers.setRawHeaders("Originator", ("mailto:user04 at example.com",))
+ )
+
+ # Invalid - changed body
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ manipulate_request=lambda request: setattr(request, "stream", MemoryStream("BEGIN:DATA\n")),
+ )
+
+ # Valid - extra header no over sign
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ True,
+ manipulate_request=lambda request: request.headers.getRawHeaders("Recipient").insert(0, "mailto:user04 at example.com"),
+ )
+
+ # Valid - over sign header
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
+ )
+
+ # Invalid - over sign header extra header
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
+ manipulate_request=lambda request: request.headers.addRawHeader("Recipient", ("mailto:user04 at example.com",))
+ )
+
+
+
+class TestPublicKeyLookup (TestDKIMBase):
+ """
+ L{PublicKeyLookup} support tests.
+ """
+
+ def tearDown(self):
+ """
+ By setting the resolver to None, it will be recreated next time a name
+ lookup is done.
+ """
+ client.theResolver = None
+ utils.DebugResolver = None
+
+
+ def test_selector_key(self):
+
+ for lookup, d, result in (
+ (PublicKeyLookup_DNSTXT, "example.com", "dkim._domainkey.example.com"),
+ (PublicKeyLookup_DNSTXT, "calendar.example.com", "dkim._domainkey.calendar.example.com"),
+ (PublicKeyLookup_HTTP_WellKnown, "example.com", "https://example.com/.well-known/domainkey/example.com/dkim"),
+ (PublicKeyLookup_HTTP_WellKnown, "calendar.example.com", "https://example.com/.well-known/domainkey/calendar.example.com/dkim"),
+ (PublicKeyLookup_PrivateExchange, "example.com", "example.com#dkim"),
+ (PublicKeyLookup_PrivateExchange, "calendar.example.com", "calendar.example.com#dkim"),
+ ):
+ dkim = "v=1; d=%s; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d,)
+ tester = lookup(DKIMUtils.extractTags(dkim))
+ self.assertEqual(tester._getSelectorKey(), result)
+
+
+ @inlineCallbacks
+ def test_get_key(self):
+
+ # Valid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is not None)
+
+ # Valid with more tags
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=ischedule ; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is not None)
+
+ # Invalid - key type
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=dsa ; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is None)
+
+ # Invalid - hash
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; h=sha512 ; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is None)
+
+ # Invalid - service
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is None)
+
+ # Invalid - revoked
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p=")]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is None)
+
+ # Multiple valid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [
+ DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,)),
+ DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=ischedule ; p=%s" % (self.public_key_data,)),
+ DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=* ; p=%s" % (self.public_key_data,)),
+ ]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is not None)
+
+ # Multiple - some valid, some invalid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [
+ DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
+ DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,)),
+ DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=ischedule ; p=%s" % (self.public_key_data,)),
+ DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=* ; p=%s" % (self.public_key_data,)),
+ ]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is not None)
+
+ # Multiple - invalid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [
+ DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
+ DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
+ ]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is None)
+
+
+ @inlineCallbacks
+ def test_cached_key(self):
+
+ # Create cache entry
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is not None)
+
+ # Cache valid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.keys = []
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is not None)
+
+ # Cache invalid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = []
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is None)
+
+
+ @inlineCallbacks
+ def test_TXT_key(self):
+
+ # Need to setup a fake resolver
+ module = getModule(__name__)
+ dataPath = module.filePath.sibling("data")
+ bindPath = dataPath.child("db.example.com")
+ self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+ utils.DebugResolver = None
+ utils._initResolver()
+
+ for d, s, result in (
+ ("example.com", "_ischedule", True),
+ ("example.com", "_revoked", False),
+ ("example.com", "dkim", False),
+ ("calendar.example.com", "_ischedule", False),
+ ("example.org", "_ischedule", False),
+ ):
+ dkim = "v=1; d=%s; s = %s; t = 1234; a=rsa-sha1; q=dns/txt ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d, s,)
+ tester = PublicKeyLookup_DNSTXT(DKIMUtils.extractTags(dkim))
+ pkey = yield tester.getPublicKey(False)
+ self.assertEqual(pkey is not None, result)
+
+
+ @inlineCallbacks
+ def test_HTTP_URI_key(self):
+
+ # Need to setup a fake resolver
+ module = getModule(__name__)
+ dataPath = module.filePath.sibling("data")
+ bindPath = dataPath.child("db.example.com")
+ self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+ utils.DebugResolver = None
+ utils._initResolver()
+
+ for d, s, result in (
+ ("example.com", "_ischedule", "https://key.example.com:8443/.well-known/domainkey/example.com/_ischedule"),
+ ("www.example.com", "_ischedule", "http://key.example.com/.well-known/domainkey/www.example.com/_ischedule"),
+ ("example.org", "_ischedule", "https://example.org/.well-known/domainkey/example.org/_ischedule"),
+ ):
+ dkim = "v=1; d=%s; s = %s; t = 1234; a=rsa-sha1; q=http/well-known ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=" % (d, s,)
+ tester = PublicKeyLookup_HTTP_WellKnown(DKIMUtils.extractTags(dkim))
+ uri = (yield tester._getURI())
+ self.assertEqual(uri, result)
+
+
+ @inlineCallbacks
+ def test_private_exchange(self):
+
+ keydir = self.mktemp()
+ PublicKeyLookup_PrivateExchange.directory = keydir
+ os.mkdir(keydir)
+ keyfile = os.path.join(keydir, "example.com#dkim")
+ with open(keyfile, "w") as f:
+ f.write("""v=DKIM1; p=%s
+""" % (self.public_key_data,))
+
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is not None)
+
+ dkim = "v=1; d=example.com; s = dkim2; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is None)
+
+ with open(keyfile, "w") as f:
+ f.write("""v=DKIM1; s=email; p=%s
+""" % (self.public_key_data,))
+
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is None)
+
+ with open(keyfile, "w") as f:
+ f.write("""v=DKIM1; s=email; p=%s
+v=DKIM1; s=ischedule; p=%s
+""" % (self.public_key_data, self.public_key_data,))
+
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known:private-exchange ; http=UE9TVDov; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b="
+ lookup = PublicKeyLookup_PrivateExchange(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ pubkey = (yield lookup.getPublicKey())
+ self.assertTrue(pubkey is not None)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_localservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_localservers.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_localservers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,184 +0,0 @@
-##
-# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.web2.test.test_server import SimpleRequest
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule.localservers import Servers, SERVER_SECRET_HEADER
-from twistedcaldav.test.util import TestCase
-import StringIO as StringIO
-
-class ServerTests(TestCase):
-
- data1 = """<?xml version="1.0" encoding="utf-8"?>
-<servers>
- <server>
- <id>00001</id>
- <uri>http://caldav1.example.com:8008</uri>
- <allowed-from>127.0.0.1</allowed-from>
- <shared-secret>foobar</shared-secret>
- </server>
- <server>
- <id>00002</id>
- <uri>https://caldav2.example.com:8843</uri>
- <partitions>
- <partition>
- <id>A</id>
- <uri>https://machine1.example.com:8443</uri>
- </partition>
- <partition>
- <id>B</id>
- <uri>https://machine2.example.com:8443</uri>
- </partition>
- </partitions>
- </server>
-</servers>
-"""
-
- data2 = """<?xml version="1.0" encoding="utf-8"?>
-<servers>
- <server>
- <id>00001</id>
- <uri>http://caldav1.example.com:8008</uri>
- <allowed-from>localhost</allowed-from>
- <shared-secret>foobar</shared-secret>
- </server>
- <server>
- <id>00002</id>
- <uri>https://caldav2.example.com:8843</uri>
- <partitions>
- <partition>
- <id>A</id>
- <uri>https://machine1.example.com:8443</uri>
- </partition>
- <partition>
- <id>B</id>
- <uri>https://machine2.example.com:8443</uri>
- </partition>
- </partitions>
- </server>
-</servers>
-"""
-
- def _setupServers(self, data=data1):
- self.patch(config, "ServerHostName", "caldav1.example.com")
- self.patch(config, "HTTPPort", 8008)
-
- xmlFile = StringIO.StringIO(data)
- servers = Servers
- servers.load(xmlFile, ignoreIPLookupFailures=True)
-
- return servers
-
-
- def test_read_ok(self):
-
- servers = self._setupServers()
-
- self.assertTrue(servers.getServerById("00001") is not None)
- self.assertTrue(servers.getServerById("00002") is not None)
-
- self.assertEqual(servers.getServerById("00001").uri, "http://caldav1.example.com:8008")
- self.assertEqual(servers.getServerById("00002").uri, "https://caldav2.example.com:8843")
-
- self.assertEqual(servers.getServerById("00001").allowed_from_ips, set(("127.0.0.1",)))
- self.assertEqual(servers.getServerById("00002").allowed_from_ips, set())
-
- self.assertEqual(servers.getServerById("00001").shared_secret, "foobar")
- self.assertEqual(servers.getServerById("00002").shared_secret, None)
-
- self.assertEqual(len(servers.getServerById("00001").partitions), 0)
- self.assertEqual(len(servers.getServerById("00002").partitions), 2)
-
- self.assertEqual(servers.getServerById("00002").getPartitionURIForId("A"), "https://machine1.example.com:8443")
- self.assertEqual(servers.getServerById("00002").getPartitionURIForId("B"), "https://machine2.example.com:8443")
-
-
- def test_this_server(self):
-
- servers = self._setupServers()
-
- self.assertTrue(servers.getServerById("00001").thisServer)
- self.assertFalse(servers.getServerById("00002").thisServer)
-
- self.patch(config, "ServerHostName", "caldav2.example.com")
- self.patch(config, "SSLPort", 8443)
- self.patch(config, "BindSSLPorts", [8843])
-
- xmlFile = StringIO.StringIO(ServerTests.data1)
- servers = Servers
- servers.load(xmlFile, ignoreIPLookupFailures=True)
-
- self.assertFalse(servers.getServerById("00001").thisServer)
- self.assertTrue(servers.getServerById("00002").thisServer)
-
-
- def test_check_is_partitioned(self):
-
- servers = self._setupServers()
-
- self.assertFalse(servers.getServerById("00001").isPartitioned())
- self.assertTrue(servers.getServerById("00002").isPartitioned())
-
-
- def test_check_this_ip(self):
-
- servers = self._setupServers()
- servers.getServerById("00001").ips = set(("127.0.0.2",))
- servers.getServerById("00002").ips = set(("127.0.0.3",))
-
- self.assertTrue(servers.getServerById("00001").checkThisIP("127.0.0.2"))
- self.assertFalse(servers.getServerById("00001").checkThisIP("127.0.0.3"))
-
-
- def test_check_allowed_from(self):
-
- for servers in (self._setupServers(), self._setupServers(data=self.data2),):
- self.assertTrue(servers.getServerById("00001").hasAllowedFromIP())
- self.assertFalse(servers.getServerById("00002").hasAllowedFromIP())
-
- self.assertTrue(servers.getServerById("00001").checkAllowedFromIP("127.0.0.1"))
- self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.2"))
- self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.3"))
- self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.1"))
- self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.2"))
- self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.3"))
-
-
- def test_check_shared_secret(self):
-
- servers = self._setupServers()
-
- request = SimpleRequest(None, "POST", "/ischedule")
- request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
- self.assertTrue(servers.getServerById("00001").checkSharedSecret(request))
-
- request = SimpleRequest(None, "POST", "/ischedule")
- request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
- self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
-
- request = SimpleRequest(None, "POST", "/ischedule")
- self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
-
- request = SimpleRequest(None, "POST", "/ischedule")
- request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
- self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
-
- request = SimpleRequest(None, "POST", "/ischedule")
- request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
- self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
-
- request = SimpleRequest(None, "POST", "/ischedule")
- self.assertTrue(servers.getServerById("00002").checkSharedSecret(request))
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_localservers.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_localservers.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_localservers.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_localservers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,184 @@
+##
+# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.web2.test.test_server import SimpleRequest
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule.localservers import Servers, SERVER_SECRET_HEADER
+from twistedcaldav.test.util import TestCase
+import StringIO as StringIO
+
+class ServerTests(TestCase):
+
+ data1 = """<?xml version="1.0" encoding="utf-8"?>
+<servers>
+ <server>
+ <id>00001</id>
+ <uri>http://caldav1.example.com:8008</uri>
+ <allowed-from>127.0.0.1</allowed-from>
+ <shared-secret>foobar</shared-secret>
+ </server>
+ <server>
+ <id>00002</id>
+ <uri>https://caldav2.example.com:8843</uri>
+ <partitions>
+ <partition>
+ <id>A</id>
+ <uri>https://machine1.example.com:8443</uri>
+ </partition>
+ <partition>
+ <id>B</id>
+ <uri>https://machine2.example.com:8443</uri>
+ </partition>
+ </partitions>
+ </server>
+</servers>
+"""
+
+ data2 = """<?xml version="1.0" encoding="utf-8"?>
+<servers>
+ <server>
+ <id>00001</id>
+ <uri>http://caldav1.example.com:8008</uri>
+ <allowed-from>localhost</allowed-from>
+ <shared-secret>foobar</shared-secret>
+ </server>
+ <server>
+ <id>00002</id>
+ <uri>https://caldav2.example.com:8843</uri>
+ <partitions>
+ <partition>
+ <id>A</id>
+ <uri>https://machine1.example.com:8443</uri>
+ </partition>
+ <partition>
+ <id>B</id>
+ <uri>https://machine2.example.com:8443</uri>
+ </partition>
+ </partitions>
+ </server>
+</servers>
+"""
+
+ def _setupServers(self, data=data1):
+ self.patch(config, "ServerHostName", "caldav1.example.com")
+ self.patch(config, "HTTPPort", 8008)
+
+ xmlFile = StringIO.StringIO(data)
+ servers = Servers
+ servers.load(xmlFile, ignoreIPLookupFailures=True)
+
+ return servers
+
+
+ def test_read_ok(self):
+
+ servers = self._setupServers()
+
+ self.assertTrue(servers.getServerById("00001") is not None)
+ self.assertTrue(servers.getServerById("00002") is not None)
+
+ self.assertEqual(servers.getServerById("00001").uri, "http://caldav1.example.com:8008")
+ self.assertEqual(servers.getServerById("00002").uri, "https://caldav2.example.com:8843")
+
+ self.assertEqual(servers.getServerById("00001").allowed_from_ips, set(("127.0.0.1",)))
+ self.assertEqual(servers.getServerById("00002").allowed_from_ips, set())
+
+ self.assertEqual(servers.getServerById("00001").shared_secret, "foobar")
+ self.assertEqual(servers.getServerById("00002").shared_secret, None)
+
+ self.assertEqual(len(servers.getServerById("00001").partitions), 0)
+ self.assertEqual(len(servers.getServerById("00002").partitions), 2)
+
+ self.assertEqual(servers.getServerById("00002").getPartitionURIForId("A"), "https://machine1.example.com:8443")
+ self.assertEqual(servers.getServerById("00002").getPartitionURIForId("B"), "https://machine2.example.com:8443")
+
+
+ def test_this_server(self):
+
+ servers = self._setupServers()
+
+ self.assertTrue(servers.getServerById("00001").thisServer)
+ self.assertFalse(servers.getServerById("00002").thisServer)
+
+ self.patch(config, "ServerHostName", "caldav2.example.com")
+ self.patch(config, "SSLPort", 8443)
+ self.patch(config, "BindSSLPorts", [8843])
+
+ xmlFile = StringIO.StringIO(ServerTests.data1)
+ servers = Servers
+ servers.load(xmlFile, ignoreIPLookupFailures=True)
+
+ self.assertFalse(servers.getServerById("00001").thisServer)
+ self.assertTrue(servers.getServerById("00002").thisServer)
+
+
+ def test_check_is_partitioned(self):
+
+ servers = self._setupServers()
+
+ self.assertFalse(servers.getServerById("00001").isPartitioned())
+ self.assertTrue(servers.getServerById("00002").isPartitioned())
+
+
+ def test_check_this_ip(self):
+
+ servers = self._setupServers()
+ servers.getServerById("00001").ips = set(("127.0.0.2",))
+ servers.getServerById("00002").ips = set(("127.0.0.3",))
+
+ self.assertTrue(servers.getServerById("00001").checkThisIP("127.0.0.2"))
+ self.assertFalse(servers.getServerById("00001").checkThisIP("127.0.0.3"))
+
+
+ def test_check_allowed_from(self):
+
+ for servers in (self._setupServers(), self._setupServers(data=self.data2),):
+ self.assertTrue(servers.getServerById("00001").hasAllowedFromIP())
+ self.assertFalse(servers.getServerById("00002").hasAllowedFromIP())
+
+ self.assertTrue(servers.getServerById("00001").checkAllowedFromIP("127.0.0.1"))
+ self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.2"))
+ self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.3"))
+ self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.1"))
+ self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.2"))
+ self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.3"))
+
+
+ def test_check_shared_secret(self):
+
+ servers = self._setupServers()
+
+ request = SimpleRequest(None, "POST", "/ischedule")
+ request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
+ self.assertTrue(servers.getServerById("00001").checkSharedSecret(request))
+
+ request = SimpleRequest(None, "POST", "/ischedule")
+ request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
+ self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
+
+ request = SimpleRequest(None, "POST", "/ischedule")
+ self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
+
+ request = SimpleRequest(None, "POST", "/ischedule")
+ request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
+ self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
+
+ request = SimpleRequest(None, "POST", "/ischedule")
+ request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
+ self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
+
+ request = SimpleRequest(None, "POST", "/ischedule")
+ self.assertTrue(servers.getServerById("00002").checkSharedSecret(request))
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,49 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.python.filepath import FilePath
-from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServersParser
-import twistedcaldav.test.util
-
-class Test_IScheduleServersParser(twistedcaldav.test.util.TestCase):
- """
- Test L{IScheduleServersParser} implementation.
- """
-
- def test_readXML(self):
-
- fp = FilePath(self.mktemp())
- fp.open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE servers SYSTEM "servertoserver.dtd">
-<servers>
- <server>
- <uri>https://localhost:8543/inbox</uri>
- <allow-requests-from/>
- <allow-requests-to/>
- <domains>
- <domain>example.org</domain>
- </domains>
- <hosts>
- <host>127.0.0.1</host>
- </hosts>
- </server>
-</servers>
-"""
-)
-
- parser = IScheduleServersParser(fp)
- self.assertEqual(len(parser.servers), 1)
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_remoteservers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,49 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.python.filepath import FilePath
+from twistedcaldav.scheduling.ischedule.remoteservers import IScheduleServersParser
+import twistedcaldav.test.util
+
+class Test_IScheduleServersParser(twistedcaldav.test.util.TestCase):
+ """
+ Test L{IScheduleServersParser} implementation.
+ """
+
+ def test_readXML(self):
+
+ fp = FilePath(self.mktemp())
+ fp.open("w").write(
+"""<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE servers SYSTEM "servertoserver.dtd">
+<servers>
+ <server>
+ <uri>https://localhost:8543/inbox</uri>
+ <allow-requests-from/>
+ <allow-requests-to/>
+ <domains>
+ <domain>example.org</domain>
+ </domains>
+ <hosts>
+ <host>127.0.0.1</host>
+ </hosts>
+ </server>
+</servers>
+"""
+)
+
+ parser = IScheduleServersParser(fp)
+ self.assertEqual(len(parser.servers), 1)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_resource.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,73 +0,0 @@
-##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.web2 import http_headers, responsecode
-from twext.web2.test.test_server import SimpleRequest
-from twisted.internet.defer import inlineCallbacks
-from twistedcaldav.config import config
-from twistedcaldav.memcachelock import MemcacheLock
-from twistedcaldav.scheduling.ischedule.resource import IScheduleInboxResource
-from twistedcaldav.test.util import TestCase
-
-class iSchedulePOST (TestCase):
-
- def setUp(self):
- super(iSchedulePOST, self).setUp()
- self.createStockDirectoryService()
- self.setupCalendars()
- self.site.resource.putChild("ischedule", IScheduleInboxResource(self.site.resource, self._newStore))
-
-
- @inlineCallbacks
- def test_deadlock(self):
- """
- Make calendar
- """
-
- request = SimpleRequest(
- self.site,
- "POST",
- "/ischedule",
- headers=http_headers.Headers(rawHeaders={
- "Originator": ("mailto:wsanchez at example.com",),
- "Recipient": ("mailto:cdaboo at example.com",),
- }),
- content="""BEGIN:VCALENDAR
-CALSCALE:GREGORIAN
-PRODID:-//Example Inc.//Example Calendar//EN
-VERSION:2.0
-BEGIN:VEVENT
-DTSTAMP:20051222T205953Z
-CREATED:20060101T150000Z
-DTSTART:20060101T100000Z
-DURATION:PT1H
-SUMMARY:event 1
-UID:deadlocked
-ORGANIZER:mailto:wsanchez at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:wsanchez at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:cdaboo at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
- )
-
- # Lock the UID here to force a deadlock - but adjust the timeout so the test does not wait too long
- self.patch(config.Scheduling.Options, "UIDLockTimeoutSeconds", 1)
- lock = MemcacheLock("ImplicitUIDLock", "deadlocked", timeout=60, expire_time=60)
- yield lock.acquire()
-
- response = (yield self.send(request))
- self.assertEqual(response.code, responsecode.CONFLICT)
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_resource.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,73 @@
+##
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.web2 import http_headers, responsecode
+from twext.web2.test.test_server import SimpleRequest
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.config import config
+from twistedcaldav.memcachelock import MemcacheLock
+from twistedcaldav.scheduling.ischedule.resource import IScheduleInboxResource
+from twistedcaldav.test.util import TestCase
+
+class iSchedulePOST (TestCase):
+
+ def setUp(self):
+ super(iSchedulePOST, self).setUp()
+ self.createStockDirectoryService()
+ self.setupCalendars()
+ self.site.resource.putChild("ischedule", IScheduleInboxResource(self.site.resource, self._newStore))
+
+
+ @inlineCallbacks
+ def test_deadlock(self):
+ """
+ Make calendar
+ """
+
+ request = SimpleRequest(
+ self.site,
+ "POST",
+ "/ischedule",
+ headers=http_headers.Headers(rawHeaders={
+ "Originator": ("mailto:wsanchez at example.com",),
+ "Recipient": ("mailto:cdaboo at example.com",),
+ }),
+ content="""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20060101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:deadlocked
+ORGANIZER:mailto:wsanchez at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:wsanchez at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:cdaboo at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+ )
+
+ # Lock the UID here to force a deadlock - but adjust the timeout so the test does not wait too long
+ self.patch(config.Scheduling.Options, "UIDLockTimeoutSeconds", 1)
+ lock = MemcacheLock("ImplicitUIDLock", "deadlocked", timeout=60, expire_time=60)
+ yield lock.acquire()
+
+ response = (yield self.send(request))
+ self.assertEqual(response.code, responsecode.CONFLICT)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_utils.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_utils.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_utils.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,116 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks
-from twistedcaldav.config import config
-from twistedcaldav.scheduling.ischedule import utils
-from twistedcaldav.test.util import TestCase
-from twisted.python.modules import getModule
-from twisted.names.authority import BindAuthority
-from twisted.names import client
-from twisted.names.test.test_client import FakeResolver
-
-class LookupService (TestCase):
-
-
- def setUp(self):
- """
- Replace the resolver with a FakeResolver
- """
- client.theResolver = FakeResolver()
-
-
- def tearDown(self):
- """
- By setting the resolver to None, it will be recreated next time a name
- lookup is done.
- """
- client.theResolver = None
- utils.DebugResolver = None
-
-
- def test_initResolver(self):
- """
- Test L{lookupServerViaSRV} with a local Bind find
- """
-
- # Default resolver
- utils.DebugResolver = None
- utils._initResolver()
- self.assertNotEqual(utils.DebugResolver, None)
- self.assertFalse(isinstance(utils.DebugResolver, BindAuthority))
-
- # Patch config for Bind resolver
- for zonefile in ("db.example.com", "db.two.zones",):
- module = getModule(__name__)
- dataPath = module.filePath.sibling("data")
- bindPath = dataPath.child(zonefile)
- self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
- utils.DebugResolver = None
- utils._initResolver()
- self.assertNotEqual(utils.DebugResolver, None)
- self.assertTrue(isinstance(utils.DebugResolver, BindAuthority))
-
-
- @inlineCallbacks
- def test_lookupServerViaSRV(self):
- """
- Test L{lookupServerViaSRV} with a local Bind find
- """
-
- # Patch config
- for zonefile, checks in (
- ("db.example.com", (("example.com", "example.com", 8443,),),),
- ("db.two.zones", (
- ("example.com", "example.com", 8443,),
- ("example.org", "example.org", 8543,),
- ),),
- ):
- module = getModule(__name__)
- dataPath = module.filePath.sibling("data")
- bindPath = dataPath.child(zonefile)
- self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
- utils.DebugResolver = None
-
- for domain, result_host, result_port in checks:
- host, port = (yield utils.lookupServerViaSRV(domain))
- self.assertEqual(host, result_host)
- self.assertEqual(port, result_port)
-
-
- @inlineCallbacks
- def test_lookupDataViaTXT(self):
- """
- Test L{lookupDataViaTXT} with a local Bind find
- """
-
- # Patch config
- for zonefile, checks in (
- ("db.example.com", (("example.com", "_ischedule._domainkey", "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjUfDqd8ICAL0dyq2KdjKN6LS8O/Y4yMxOxgATqtSIMi7baKXEs1w5Wj9efOC2nU+aqyhP2/J6AzfFJfSB+GV5gcIT+LAC4btJKPGjPUyXcQFJV4a73y0jIgCTBzWxdaP6qD9P9rzYlvMPcdrrKiKoAOtI3JZqAAdZudOmGlc4QQIDAQAB"),),),
- ("db.two.zones", (
- ("example.com", "_ischedule._domainkey", "v=DKIM1; p="),
- ("example.org", "_ischedule2._domainkey", "v=DKIM1; s=ischedule; p="),
- )),
- ):
- module = getModule(__name__)
- dataPath = module.filePath.sibling("data")
- bindPath = dataPath.child(zonefile)
- self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
- utils.DebugResolver = None
-
- for domain, prefix, result in checks:
- texts = (yield utils.lookupDataViaTXT(domain, prefix))
- self.assertEqual(texts, [result])
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_utils.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_utils.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_utils.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_utils.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,116 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.config import config
+from twistedcaldav.scheduling.ischedule import utils
+from twistedcaldav.test.util import TestCase
+from twisted.python.modules import getModule
+from twisted.names.authority import BindAuthority
+from twisted.names import client
+from twisted.names.test.test_client import FakeResolver
+
+class LookupService (TestCase):
+
+
+ def setUp(self):
+ """
+ Replace the resolver with a FakeResolver
+ """
+ client.theResolver = FakeResolver()
+
+
+ def tearDown(self):
+ """
+ By setting the resolver to None, it will be recreated next time a name
+ lookup is done.
+ """
+ client.theResolver = None
+ utils.DebugResolver = None
+
+
+ def test_initResolver(self):
+ """
+ Test L{lookupServerViaSRV} with a local Bind find
+ """
+
+ # Default resolver
+ utils.DebugResolver = None
+ utils._initResolver()
+ self.assertNotEqual(utils.DebugResolver, None)
+ self.assertFalse(isinstance(utils.DebugResolver, BindAuthority))
+
+ # Patch config for Bind resolver
+ for zonefile in ("db.example.com", "db.two.zones",):
+ module = getModule(__name__)
+ dataPath = module.filePath.sibling("data")
+ bindPath = dataPath.child(zonefile)
+ self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+ utils.DebugResolver = None
+ utils._initResolver()
+ self.assertNotEqual(utils.DebugResolver, None)
+ self.assertTrue(isinstance(utils.DebugResolver, BindAuthority))
+
+
+ @inlineCallbacks
+ def test_lookupServerViaSRV(self):
+ """
+ Test L{lookupServerViaSRV} with a local Bind find
+ """
+
+ # Patch config
+ for zonefile, checks in (
+ ("db.example.com", (("example.com", "example.com", 8443,),),),
+ ("db.two.zones", (
+ ("example.com", "example.com", 8443,),
+ ("example.org", "example.org", 8543,),
+ ),),
+ ):
+ module = getModule(__name__)
+ dataPath = module.filePath.sibling("data")
+ bindPath = dataPath.child(zonefile)
+ self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+ utils.DebugResolver = None
+
+ for domain, result_host, result_port in checks:
+ host, port = (yield utils.lookupServerViaSRV(domain))
+ self.assertEqual(host, result_host)
+ self.assertEqual(port, result_port)
+
+
+ @inlineCallbacks
+ def test_lookupDataViaTXT(self):
+ """
+ Test L{lookupDataViaTXT} with a local Bind find
+ """
+
+ # Patch config
+ for zonefile, checks in (
+ ("db.example.com", (("example.com", "_ischedule._domainkey", "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjUfDqd8ICAL0dyq2KdjKN6LS8O/Y4yMxOxgATqtSIMi7baKXEs1w5Wj9efOC2nU+aqyhP2/J6AzfFJfSB+GV5gcIT+LAC4btJKPGjPUyXcQFJV4a73y0jIgCTBzWxdaP6qD9P9rzYlvMPcdrrKiKoAOtI3JZqAAdZudOmGlc4QQIDAQAB"),),),
+ ("db.two.zones", (
+ ("example.com", "_ischedule._domainkey", "v=DKIM1; p="),
+ ("example.org", "_ischedule2._domainkey", "v=DKIM1; s=ischedule; p="),
+ )),
+ ):
+ module = getModule(__name__)
+ dataPath = module.filePath.sibling("data")
+ bindPath = dataPath.child(zonefile)
+ self.patch(config.Scheduling.iSchedule, "DNSDebug", bindPath.path)
+ utils.DebugResolver = None
+
+ for domain, prefix, result in checks:
+ texts = (yield utils.lookupDataViaTXT(domain, prefix))
+ self.assertEqual(texts, [result])
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/utils.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,182 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.python.log import Logger
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.names import dns
-from twisted.names.authority import BindAuthority
-from twisted.names.client import getResolver
-from twisted.names.error import DomainError, AuthoritativeDomainError
-
-from twistedcaldav.config import config
-
-import socket
-
-log = Logger()
-
-DebugResolver = None
-
-
-def getIPsFromHost(host):
- """
- Map a hostname to an IPv4 or IPv6 address.
-
- @param host: the hostname
- @type host: C{str}
-
- @return: a C{set} of IPs
- """
- ips = set()
- for family in (socket.AF_INET, socket.AF_INET6):
- results = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
- for _ignore_family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in results:
- ips.add(sockaddr[0])
-
- return ips
-
-
-
- at inlineCallbacks
-def lookupServerViaSRV(domain, service="_ischedules"):
-
- _initResolver()
-
- lookup = "%s._tcp.%s" % (service, domain,)
- log.debug("DNS SRV: lookup: %s" % (lookup,))
- try:
- answers = (yield DebugResolver.lookupService(lookup))
- except (DomainError, AuthoritativeDomainError), e:
- log.debug("DNS SRV: lookup failed: %s" % (e,))
- returnValue(None)
-
- if len(answers) == 1 and answers[0].type == dns.SRV \
- and answers[0].payload \
- and answers[0].payload.target == dns.Name('.'):
- # decidedly not available
- log.debug("DNS SRV: disabled: %s" % (lookup,))
- returnValue(None)
-
- servers = []
- for a in answers:
-
- if a.type != dns.SRV or not a.payload:
- continue
-
- servers.append((a.payload.priority, a.payload.weight, str(a.payload.target), a.payload.port))
-
- log.debug("DNS SRV: lookup results: %s\n%s" % (lookup, servers,))
-
-
- def _serverCmp(a, b):
- if a[0] != b[0]:
- return cmp(a[0], b[0])
- else:
- return cmp(a[1], b[1])
-
- servers.sort(_serverCmp)
- minPriority = servers[0][0]
-
- weightIndex = zip(xrange(len(servers)), [x[1] for x in servers if x[0] == minPriority])
- weightSum = reduce(lambda x, y: (None, x[1] + y[1]), weightIndex, (None, 0))[1]
-
- for index, weight in weightIndex:
- weightSum -= weight
- if weightSum <= 0:
- chosen = servers[index]
- _ignore_p, _ignore_w, host, port = chosen
- host = host.rstrip(".")
- break
- else:
- log.debug("DNS SRV: unable to determine best record to use: %s" % (lookup,))
- returnValue(None)
-
- log.debug("DNS SRV: lookup chosen service: %s %s %s" % (lookup, host, port,))
- returnValue((host, port,))
-
-
-
- at inlineCallbacks
-def lookupDataViaTXT(domain, prefix=""):
-
- _initResolver()
-
- lookup = "%s.%s" % (prefix, domain,) if prefix else domain
- log.debug("DNS TXT: lookup: %s" % (lookup,))
- try:
- answers = (yield DebugResolver.lookupText(lookup))
- except (DomainError, AuthoritativeDomainError), e:
- log.debug("DNS TXT: lookup failed: %s" % (e,))
- answers = ()
-
- results = []
- for a in answers:
-
- if a.type != dns.TXT or not a.payload:
- continue
-
- results.append("".join(a.payload.data))
-
- log.debug("DNS TXT: lookup results: %s\n%s" % (lookup, "\n".join(results),))
- returnValue(results)
-
-
-
-class FakeBindAuthority(BindAuthority):
-
- @inlineCallbacks
- def _lookup(self, name, cls, type, timeout=None):
- log.debug("DNS FakeBindAuthority: lookup: %s %s %s" % (name, cls, type,))
- result = yield BindAuthority._lookup(self, name, cls, type, timeout)
- log.debug("DNS FakeBindAuthority: lookup results: %s %s %s\n%s" % (name, cls, type, result[0]))
- returnValue(result[0])
-
-
- def stripComments(self, lines):
- """
- Work around a bug in the base implementation that causes parsing of TXT RRs with
- a ; in the RDATA to fail because the ; is treated as the start of a comment. Here
- we simply ignore all comments.
- """
- return [
- (a.find(';') == -1 or "TXT" in a) and a or a[:a.find(';')] for a in [
- b.strip() for b in lines
- ]
- ]
-
-
- def parseLines(self, lines):
- """
- Work around a bug in the base implementation that causes parsing of TXT RRs with
- spaces in the RDATA to be broken into multiple fragments and for quotes around the
- data to not be removed.
- """
- for line in lines:
- if line[3] == "TXT":
- line[4] = " ".join(line[4:])[1:-1]
- del line[5:]
-
- BindAuthority.parseLines(self, lines)
-
-
-
-def _initResolver():
- global DebugResolver
- if DebugResolver is None:
- if config.Scheduling.iSchedule.DNSDebug:
- DebugResolver = FakeBindAuthority(config.Scheduling.iSchedule.DNSDebug)
- else:
- DebugResolver = getResolver()
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/utils.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/utils.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,182 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.python.log import Logger
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.names import dns
+from twisted.names.authority import BindAuthority
+from twisted.names.client import getResolver
+from twisted.names.error import DomainError, AuthoritativeDomainError
+
+from twistedcaldav.config import config
+
+import socket
+
+log = Logger()
+
+DebugResolver = None
+
+
+def getIPsFromHost(host):
+ """
+ Map a hostname to an IPv4 or IPv6 address.
+
+ @param host: the hostname
+ @type host: C{str}
+
+ @return: a C{set} of IPs
+ """
+ ips = set()
+ for family in (socket.AF_INET, socket.AF_INET6):
+ results = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
+ for _ignore_family, _ignore_socktype, _ignore_proto, _ignore_canonname, sockaddr in results:
+ ips.add(sockaddr[0])
+
+ return ips
+
+
+
+ at inlineCallbacks
+def lookupServerViaSRV(domain, service="_ischedules"):
+
+ _initResolver()
+
+ lookup = "%s._tcp.%s" % (service, domain,)
+ log.debug("DNS SRV: lookup: %s" % (lookup,))
+ try:
+ answers = (yield DebugResolver.lookupService(lookup))
+ except (DomainError, AuthoritativeDomainError), e:
+ log.debug("DNS SRV: lookup failed: %s" % (e,))
+ returnValue(None)
+
+ if len(answers) == 1 and answers[0].type == dns.SRV \
+ and answers[0].payload \
+ and answers[0].payload.target == dns.Name('.'):
+ # decidedly not available
+ log.debug("DNS SRV: disabled: %s" % (lookup,))
+ returnValue(None)
+
+ servers = []
+ for a in answers:
+
+ if a.type != dns.SRV or not a.payload:
+ continue
+
+ servers.append((a.payload.priority, a.payload.weight, str(a.payload.target), a.payload.port))
+
+ log.debug("DNS SRV: lookup results: %s\n%s" % (lookup, servers,))
+
+
+ def _serverCmp(a, b):
+ if a[0] != b[0]:
+ return cmp(a[0], b[0])
+ else:
+ return cmp(a[1], b[1])
+
+ servers.sort(_serverCmp)
+ minPriority = servers[0][0]
+
+ weightIndex = zip(xrange(len(servers)), [x[1] for x in servers if x[0] == minPriority])
+ weightSum = reduce(lambda x, y: (None, x[1] + y[1]), weightIndex, (None, 0))[1]
+
+ for index, weight in weightIndex:
+ weightSum -= weight
+ if weightSum <= 0:
+ chosen = servers[index]
+ _ignore_p, _ignore_w, host, port = chosen
+ host = host.rstrip(".")
+ break
+ else:
+ log.debug("DNS SRV: unable to determine best record to use: %s" % (lookup,))
+ returnValue(None)
+
+ log.debug("DNS SRV: lookup chosen service: %s %s %s" % (lookup, host, port,))
+ returnValue((host, port,))
+
+
+
+ at inlineCallbacks
+def lookupDataViaTXT(domain, prefix=""):
+
+ _initResolver()
+
+ lookup = "%s.%s" % (prefix, domain,) if prefix else domain
+ log.debug("DNS TXT: lookup: %s" % (lookup,))
+ try:
+ answers = (yield DebugResolver.lookupText(lookup))
+ except (DomainError, AuthoritativeDomainError), e:
+ log.debug("DNS TXT: lookup failed: %s" % (e,))
+ answers = ()
+
+ results = []
+ for a in answers:
+
+ if a.type != dns.TXT or not a.payload:
+ continue
+
+ results.append("".join(a.payload.data))
+
+ log.debug("DNS TXT: lookup results: %s\n%s" % (lookup, "\n".join(results),))
+ returnValue(results)
+
+
+
+class FakeBindAuthority(BindAuthority):
+
+ @inlineCallbacks
+ def _lookup(self, name, cls, type, timeout=None):
+ log.debug("DNS FakeBindAuthority: lookup: %s %s %s" % (name, cls, type,))
+ result = yield BindAuthority._lookup(self, name, cls, type, timeout)
+ log.debug("DNS FakeBindAuthority: lookup results: %s %s %s\n%s" % (name, cls, type, result[0]))
+ returnValue(result[0])
+
+
+ def stripComments(self, lines):
+ """
+ Work around a bug in the base implementation that causes parsing of TXT RRs with
+ a ; in the RDATA to fail because the ; is treated as the start of a comment. Here
+ we simply ignore all comments.
+ """
+ return [
+ (a.find(';') == -1 or "TXT" in a) and a or a[:a.find(';')] for a in [
+ b.strip() for b in lines
+ ]
+ ]
+
+
+ def parseLines(self, lines):
+ """
+ Work around a bug in the base implementation that causes parsing of TXT RRs with
+ spaces in the RDATA to be broken into multiple fragments and for quotes around the
+ data to not be removed.
+ """
+ for line in lines:
+ if line[3] == "TXT":
+ line[4] = " ".join(line[4:])[1:-1]
+ del line[5:]
+
+ BindAuthority.parseLines(self, lines)
+
+
+
+def _initResolver():
+ global DebugResolver
+ if DebugResolver is None:
+ if config.Scheduling.iSchedule.DNSDebug:
+ DebugResolver = FakeBindAuthority(config.Scheduling.iSchedule.DNSDebug)
+ else:
+ DebugResolver = getResolver()
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py 2012-10-24 19:57:08 UTC (rev 9979)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,281 +0,0 @@
-##
-# Copyright (c) 2011-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-This module provides XML definitions for use with Timezone Standard Service.
-"""
-
-from twistedcaldav.config import config
-from twistedcaldav.ical import Component as iComponent
-from txdav.xml.element import PCDATAElement, WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
-from txdav.xml.element import registerElement
-
-
-##
-# iSchedule XML Definitions
-##
-
-ischedule_namespace = "urn:ietf:params:xml:ns:ischedule"
-
-
- at registerElement
-class QueryResult (WebDAVElement):
- namespace = ischedule_namespace
- name = "query-result"
-
- allowed_children = {
- (ischedule_namespace, "capability-set"): (0, None),
- }
-
-
-
- at registerElement
-class Capabilities (WebDAVElement):
- namespace = ischedule_namespace
- name = "capabilities"
-
- allowed_children = {
- (ischedule_namespace, "versions"): (1, 1),
- (ischedule_namespace, "scheduling-messages"): (1, 1),
- (ischedule_namespace, "calendar-data-types"): (1, 1),
- (ischedule_namespace, "attachments"): (1, 1),
- (ischedule_namespace, "supported-recipient-uri-scheme-set"): (1, 1),
- (ischedule_namespace, "max-content-length"): (1, 1),
- (ischedule_namespace, "min-date-time"): (1, 1),
- (ischedule_namespace, "max-date-time"): (1, 1),
- (ischedule_namespace, "max-instances"): (1, 1),
- (ischedule_namespace, "max-recipients"): (1, 1),
- (ischedule_namespace, "administrator"): (1, 1),
- }
-
-
-
- at registerElement
-class Versions (WebDAVElement):
- namespace = ischedule_namespace
- name = "versions"
-
- allowed_children = {
- (ischedule_namespace, "version"): (1, None),
- }
-
-
-
- at registerElement
-class Version (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "version"
-
-
-
- at registerElement
-class SchedulingMessages (WebDAVElement):
- namespace = ischedule_namespace
- name = "scheduling-messages"
-
- allowed_children = {
- (ischedule_namespace, "component"): (1, None),
- }
-
-
-
- at registerElement
-class Component (WebDAVElement):
- namespace = ischedule_namespace
- name = "component"
-
- allowed_children = {
- (ischedule_namespace, "method"): (0, None),
- }
- allowed_attributes = {"name": True}
-
-
-
- at registerElement
-class Method (WebDAVEmptyElement):
- namespace = ischedule_namespace
- name = "method"
-
- allowed_attributes = {"name": True}
-
-
-
- at registerElement
-class CalendarDataTypes (WebDAVElement):
- namespace = ischedule_namespace
- name = "calendar-data-types"
-
- allowed_children = {
- (ischedule_namespace, "calendar-data-type"): (1, None),
- }
-
-
-
- at registerElement
-class CalendarDataType (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "calendar-data-type"
-
- allowed_attributes = {
- "content-type": True,
- "version": True,
- }
-
-
-
- at registerElement
-class Attachments (WebDAVElement):
- namespace = ischedule_namespace
- name = "attachments"
-
- allowed_children = {
- (ischedule_namespace, "inline"): (0, 1),
- (ischedule_namespace, "external"): (0, 1),
- }
-
-
-
- at registerElement
-class Inline (WebDAVEmptyElement):
- namespace = ischedule_namespace
- name = "inline"
-
-
-
- at registerElement
-class External (WebDAVEmptyElement):
- namespace = ischedule_namespace
- name = "external"
-
-
-
- at registerElement
-class MaxContentLength (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "max-content-length"
-
-
-
- at registerElement
-class MinDateTime (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "min-date-time"
-
-
-
- at registerElement
-class MaxDateTime (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "max-date-time"
-
-
-
- at registerElement
-class MaxInstances (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "max-instances"
-
-
-
- at registerElement
-class MaxRecipients (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "max-recipients"
-
-
-
- at registerElement
-class Administrator (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "administrator"
-
-
-
- at registerElement
-class ScheduleResponse (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "schedule-response"
-
- allowed_children = {
- (ischedule_namespace, "response"): (0, None),
- }
-
-
-
- at registerElement
-class Response (WebDAVElement):
- namespace = ischedule_namespace
- name = "response"
-
- allowed_children = {
- (ischedule_namespace, "recipient"): (1, 1),
- (ischedule_namespace, "request-status"): (1, 1),
- (ischedule_namespace, "calendar-data"): (0, 1),
- (ischedule_namespace, "error"): (0, 1),
- (ischedule_namespace, "response-description"): (0, 1),
- }
-
-
-
- at registerElement
-class Recipient (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "recipient"
-
-
-
- at registerElement
-class RequestStatus (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "request-status"
-
-
-
- at registerElement
-class CalendarData (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "calendar-data"
-
-
- @classmethod
- def fromCalendar(clazz, calendar):
- if isinstance(calendar, str):
- if not calendar:
- raise ValueError("Missing calendar data")
- return clazz(PCDATAElement(calendar))
- elif isinstance(calendar, iComponent):
- assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
- return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
- else:
- raise ValueError("Not a calendar: %s" % (calendar,))
-
- fromTextData = fromCalendar
-
-
-
- at registerElement
-class Error (WebDAVElement):
- namespace = ischedule_namespace
- name = "error"
-
- allowed_children = {WebDAVElement: (0, None)}
-
-
-
- at registerElement
-class ResponseDescription (WebDAVTextElement):
- namespace = ischedule_namespace
- name = "response-description"
Copied: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py (from rev 9979, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/xml.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -0,0 +1,281 @@
+##
+# Copyright (c) 2011-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+This module provides XML definitions for use with Timezone Standard Service.
+"""
+
+from twistedcaldav.config import config
+from twistedcaldav.ical import Component as iComponent
+from txdav.xml.element import PCDATAElement, WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
+from txdav.xml.element import registerElement
+
+
+##
+# iSchedule XML Definitions
+##
+
+ischedule_namespace = "urn:ietf:params:xml:ns:ischedule"
+
+
+ at registerElement
+class QueryResult (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "query-result"
+
+ allowed_children = {
+ (ischedule_namespace, "capability-set"): (0, None),
+ }
+
+
+
+ at registerElement
+class Capabilities (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "capabilities"
+
+ allowed_children = {
+ (ischedule_namespace, "versions"): (1, 1),
+ (ischedule_namespace, "scheduling-messages"): (1, 1),
+ (ischedule_namespace, "calendar-data-types"): (1, 1),
+ (ischedule_namespace, "attachments"): (1, 1),
+ (ischedule_namespace, "supported-recipient-uri-scheme-set"): (1, 1),
+ (ischedule_namespace, "max-content-length"): (1, 1),
+ (ischedule_namespace, "min-date-time"): (1, 1),
+ (ischedule_namespace, "max-date-time"): (1, 1),
+ (ischedule_namespace, "max-instances"): (1, 1),
+ (ischedule_namespace, "max-recipients"): (1, 1),
+ (ischedule_namespace, "administrator"): (1, 1),
+ }
+
+
+
+ at registerElement
+class Versions (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "versions"
+
+ allowed_children = {
+ (ischedule_namespace, "version"): (1, None),
+ }
+
+
+
+ at registerElement
+class Version (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "version"
+
+
+
+ at registerElement
+class SchedulingMessages (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "scheduling-messages"
+
+ allowed_children = {
+ (ischedule_namespace, "component"): (1, None),
+ }
+
+
+
+ at registerElement
+class Component (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "component"
+
+ allowed_children = {
+ (ischedule_namespace, "method"): (0, None),
+ }
+ allowed_attributes = {"name": True}
+
+
+
+ at registerElement
+class Method (WebDAVEmptyElement):
+ namespace = ischedule_namespace
+ name = "method"
+
+ allowed_attributes = {"name": True}
+
+
+
+ at registerElement
+class CalendarDataTypes (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "calendar-data-types"
+
+ allowed_children = {
+ (ischedule_namespace, "calendar-data-type"): (1, None),
+ }
+
+
+
+ at registerElement
+class CalendarDataType (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "calendar-data-type"
+
+ allowed_attributes = {
+ "content-type": True,
+ "version": True,
+ }
+
+
+
+ at registerElement
+class Attachments (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "attachments"
+
+ allowed_children = {
+ (ischedule_namespace, "inline"): (0, 1),
+ (ischedule_namespace, "external"): (0, 1),
+ }
+
+
+
+ at registerElement
+class Inline (WebDAVEmptyElement):
+ namespace = ischedule_namespace
+ name = "inline"
+
+
+
+ at registerElement
+class External (WebDAVEmptyElement):
+ namespace = ischedule_namespace
+ name = "external"
+
+
+
+ at registerElement
+class MaxContentLength (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "max-content-length"
+
+
+
+ at registerElement
+class MinDateTime (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "min-date-time"
+
+
+
+ at registerElement
+class MaxDateTime (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "max-date-time"
+
+
+
+ at registerElement
+class MaxInstances (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "max-instances"
+
+
+
+ at registerElement
+class MaxRecipients (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "max-recipients"
+
+
+
+ at registerElement
+class Administrator (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "administrator"
+
+
+
+ at registerElement
+class ScheduleResponse (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "schedule-response"
+
+ allowed_children = {
+ (ischedule_namespace, "response"): (0, None),
+ }
+
+
+
+ at registerElement
+class Response (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "response"
+
+ allowed_children = {
+ (ischedule_namespace, "recipient"): (1, 1),
+ (ischedule_namespace, "request-status"): (1, 1),
+ (ischedule_namespace, "calendar-data"): (0, 1),
+ (ischedule_namespace, "error"): (0, 1),
+ (ischedule_namespace, "response-description"): (0, 1),
+ }
+
+
+
+ at registerElement
+class Recipient (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "recipient"
+
+
+
+ at registerElement
+class RequestStatus (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "request-status"
+
+
+
+ at registerElement
+class CalendarData (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "calendar-data"
+
+
+ @classmethod
+ def fromCalendar(clazz, calendar):
+ if isinstance(calendar, str):
+ if not calendar:
+ raise ValueError("Missing calendar data")
+ return clazz(PCDATAElement(calendar))
+ elif isinstance(calendar, iComponent):
+ assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
+ return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
+ else:
+ raise ValueError("Not a calendar: %s" % (calendar,))
+
+ fromTextData = fromCalendar
+
+
+
+ at registerElement
+class Error (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "error"
+
+ allowed_children = {WebDAVElement: (0, None)}
+
+
+
+ at registerElement
+class ResponseDescription (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "response-description"
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,343 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from StringIO import StringIO
-
-from twisted.internet.defer import inlineCallbacks, DeferredList, succeed
-from twisted.internet.protocol import Factory
-
-from twisted.python.failure import Failure
-
-from twext.web2 import responsecode
-from twext.web2.client.http import ClientRequest
-from twext.web2.client.http import HTTPClientProtocol
-from twext.web2.dav.util import davXMLFromStream, joinURL, allDataFromStream
-from twext.web2.http import HTTPError
-from twext.web2.http_headers import Headers
-from twext.web2.http_headers import MimeType
-from twext.web2.stream import MemoryStream
-
-from twext.python.log import Logger, logLevels
-from twext.web2.dav.http import ErrorResponse
-
-from twistedcaldav.client.pool import _configuredClientContextFactory
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServerRecord
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.util import utf8String, normalizationLookup
-from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser, RemoteCalendarUser,\
- OtherServerCalendarUser
-from twext.internet.gaiendpoint import GAIEndpoint
-
-"""
-Handles the sending of iSchedule scheduling messages. Used for both cross-domain scheduling,
-as well as internal partitioning or podding.
-"""
-
-__all__ = [
- "ScheduleViaISchedule",
-]
-
-log = Logger()
-
-class ScheduleViaISchedule(DeliveryService):
-
- @classmethod
- def serviceType(cls):
- return DeliveryService.serviceType_ischedule
-
- @classmethod
- def matchCalendarUserAddress(cls, cuaddr):
-
- # TODO: here is where we would attempt service discovery based on the cuaddr.
-
- # Do default match
- return super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr)
-
- def generateSchedulingResponses(self, refreshOnly=False):
- """
- Generate scheduling responses for remote recipients.
- """
-
- # Group recipients by server so that we can do a single request with multiple recipients
- # to each different server.
- groups = {}
- servermgr = IScheduleServers()
- for recipient in self.recipients:
- if isinstance(recipient, RemoteCalendarUser):
- # Map the recipient's domain to a server
- server = servermgr.mapDomain(recipient.domain)
- elif isinstance(recipient, PartitionedCalendarUser):
- server = self._getServerForPartitionedUser(recipient)
- elif isinstance(recipient, OtherServerCalendarUser):
- server = self._getServerForOtherServerUser(recipient)
- else:
- assert False, "Incorrect calendar user address class"
- if not server:
- # Cannot do server-to-server for this recipient.
- err = HTTPError(ErrorResponse(
- responsecode.NOT_FOUND,
- (caldav_namespace, "recipient-allowed"),
- "No server for recipient",
- ))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
-
- # Process next recipient
- continue
-
- if not server.allow_to:
- # Cannot do server-to-server outgoing requests for this server.
- err = HTTPError(ErrorResponse(
- responsecode.NOT_FOUND,
- (caldav_namespace, "recipient-allowed"),
- "Cannot send to recipient's server",
- ))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
- # Process next recipient
- continue
-
- groups.setdefault(server, []).append(recipient)
-
- if len(groups) == 0:
- return
-
- # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
- # we will generate for each request. That way we can have parallel requests in progress
- # rather than serialize them.
- deferreds = []
- for server, recipients in groups.iteritems():
- requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses, refreshOnly)
- deferreds.append(requestor.doRequest())
-
- return DeferredList(deferreds)
-
- def _getServerForPartitionedUser(self, recipient):
-
- if not hasattr(self, "partitionedServers"):
- self.partitionedServers = {}
-
- partition = recipient.principal.partitionURI()
- if partition not in self.partitionedServers:
- self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
- self.partitionedServers[partition].unNormalizeAddresses = False
- self.partitionedServers[partition].moreHeaders.append(recipient.principal.server().secretHeader())
-
- return self.partitionedServers[partition]
-
- def _getServerForOtherServerUser(self, recipient):
-
- if not hasattr(self, "otherServers"):
- self.otherServers = {}
-
- serverURI = recipient.principal.serverURI()
- if serverURI not in self.otherServers:
- self.otherServers[serverURI] = IScheduleServerRecord(uri=joinURL(serverURI, "/ischedule"))
- self.otherServers[serverURI].unNormalizeAddresses = not recipient.principal.server().isImplicit
- self.otherServers[serverURI].moreHeaders.append(recipient.principal.server().secretHeader())
-
- return self.otherServers[serverURI]
-
-class IScheduleRequest(object):
-
- def __init__(self, scheduler, server, recipients, responses, refreshOnly=False):
-
- self.scheduler = scheduler
- self.server = server
- self.recipients = recipients
- self.responses = responses
- self.refreshOnly = refreshOnly
-
- self._generateHeaders()
- self._prepareData()
-
- @inlineCallbacks
- def doRequest(self):
-
- # Generate an HTTP client request
- try:
- if not hasattr(self.scheduler.request, "extendedLogItems"):
- self.scheduler.request.extendedLogItems = {}
- if "itip.ischedule" not in self.scheduler.request.extendedLogItems:
- self.scheduler.request.extendedLogItems["itip.ischedule"] = 0
- self.scheduler.request.extendedLogItems["itip.ischedule"] += 1
-
- from twisted.internet import reactor
- f = Factory()
- f.protocol = HTTPClientProtocol
- if self.server.ssl:
- ep = GAIEndpoint(reactor, self.server.host, self.server.port,
- _configuredClientContextFactory())
- else:
- ep = GAIEndpoint(reactor, self.server.host, self.server.port)
- proto = (yield ep.connect(f))
-
- request = ClientRequest("POST", self.server.path, self.headers, self.data)
- yield self.logRequest("debug", "Sending server-to-server POST request:", request)
- response = (yield proto.submitRequest(request))
-
- yield self.logResponse("debug", "Received server-to-server POST response:", response)
- xml = (yield davXMLFromStream(response.stream))
-
- self._parseResponse(xml)
-
- except Exception, e:
- # Generated failed responses for each recipient
- log.err("Could not do server-to-server request : %s %s" % (self, e))
- for recipient in self.recipients:
- err = HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "recipient-failed"),
- "Server-to-server request failed",
- ))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
- def logRequest(self, level, message, request, **kwargs):
- """
- Log an HTTP request.
- """
-
- assert level in logLevels
-
- if log.willLogAtLevel(level):
- iostr = StringIO()
- iostr.write("%s\n" % (message,))
- if hasattr(request, "clientproto"):
- protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
- else:
- protocol = "HTTP/1.1"
- iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
- for name, valuelist in request.headers.getAllRawHeaders():
- for value in valuelist:
- # Do not log authorization details
- if name not in ("Authorization",):
- iostr.write("%s: %s\n" % (name, value))
- else:
- iostr.write("%s: xxxxxxxxx\n" % (name,))
- iostr.write("\n")
-
- # We need to play a trick with the request stream as we can only read it once. So we
- # read it, store the value in a MemoryStream, and replace the request's stream with that,
- # so the data can be read again.
- def _gotData(data):
- iostr.write(data)
-
- request.stream = MemoryStream(data if data is not None else "")
- request.stream.doStartReading = None
-
- log.emit(level, iostr.getvalue(), **kwargs)
-
- d = allDataFromStream(request.stream)
- d.addCallback(_gotData)
- return d
-
- else:
- return succeed(None)
-
- def logResponse(self, level, message, response, **kwargs):
- """
- Log an HTTP request.
- """
- assert level in logLevels
-
- if log.willLogAtLevel(level):
- iostr = StringIO()
- iostr.write("%s\n" % (message,))
- code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
- iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
- for name, valuelist in response.headers.getAllRawHeaders():
- for value in valuelist:
- # Do not log authorization details
- if name not in ("WWW-Authenticate",):
- iostr.write("%s: %s\n" % (name, value))
- else:
- iostr.write("%s: xxxxxxxxx\n" % (name,))
- iostr.write("\n")
-
- # We need to play a trick with the response stream to ensure we don't mess it up. So we
- # read it, store the value in a MemoryStream, and replace the response's stream with that,
- # so the data can be read again.
- def _gotData(data):
- iostr.write(data)
-
- response.stream = MemoryStream(data if data is not None else "")
- response.stream.doStartReading = None
-
- log.emit(level, iostr.getvalue(), **kwargs)
-
- d = allDataFromStream(response.stream)
- d.addCallback(_gotData)
- return d
-
- def _generateHeaders(self):
- self.headers = Headers()
- self.headers.setHeader('Host', utf8String(self.server.host + ":%s" % (self.server.port,)))
-
- # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
- self.headers.addRawHeader('Originator', utf8String(self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee))
- self._doAuthentication()
- for recipient in self.recipients:
- self.headers.addRawHeader('Recipient', utf8String(recipient.cuaddr))
- self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
-
- # Add any additional headers
- for name, value in self.server.moreHeaders:
- self.headers.addRawHeader(name, value)
-
- if self.refreshOnly:
- self.headers.addRawHeader("X-CALENDARSERVER-ITIP-REFRESHONLY", "T")
-
- def _doAuthentication(self):
- if self.server.authentication and self.server.authentication[0] == "basic":
- self.headers.setHeader(
- 'Authorization',
- ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
- )
-
- def _prepareData(self):
- if self.server.unNormalizeAddresses and self.scheduler.method == "PUT":
- normalizedCalendar = self.scheduler.calendar.duplicate()
- normalizedCalendar.normalizeCalendarUserAddresses(
- normalizationLookup,
- self.scheduler.resource.principalForCalendarUserAddress,
- toUUID=False)
- else:
- normalizedCalendar = self.scheduler.calendar
- self.data = str(normalizedCalendar)
-
- def _parseResponse(self, xml):
-
- # Check for correct root element
- schedule_response = xml.root_element
- if not isinstance(schedule_response, caldavxml.ScheduleResponse) or not schedule_response.children:
- raise HTTPError(responsecode.BAD_REQUEST)
-
- # Parse each response - do this twice: once looking for errors that will
- # result in all recipients shown as failures; the second loop adds all the
- # valid responses to the actual result.
- for response in schedule_response.children:
- if not isinstance(response, caldavxml.Response) or not response.children:
- raise HTTPError(responsecode.BAD_REQUEST)
- recipient = response.childOfType(caldavxml.Recipient)
- request_status = response.childOfType(caldavxml.RequestStatus)
- if not recipient or not request_status:
- raise HTTPError(responsecode.BAD_REQUEST)
- for response in schedule_response.children:
- self.responses.clone(response)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,195 +0,0 @@
-##
-# Copyright (c) 2006-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.python.filepath import CachingFilePath as FilePath
-
-from twext.python.log import Logger
-
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.xmlutil import readXML
-
-"""
-XML based iSchedule configuration file handling.
-"""
-
-__all__ = [
- "IScheduleServers",
-]
-
-log = Logger()
-
-class IScheduleServers(object):
-
- _fileInfo = None
- _xmlFile = None
- _servers = None
- _domainMap = None
-
- def __init__(self):
-
- self._loadConfig()
-
- def _loadConfig(self):
- if IScheduleServers._servers is None:
- IScheduleServers._xmlFile = FilePath(
- fullServerPath(
- config.ConfigRoot,
- config.Scheduling[DeliveryService.serviceType_ischedule]["Servers"]
- )
- )
- IScheduleServers._xmlFile.restat()
- fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
- if fileInfo != IScheduleServers._fileInfo:
- parser = IScheduleServersParser(IScheduleServers._xmlFile)
- IScheduleServers._servers = parser.servers
- self._mapDomains()
- IScheduleServers._fileInfo = fileInfo
-
- def _mapDomains(self):
- IScheduleServers._domainMap = {}
- for server in IScheduleServers._servers:
- for domain in server.domains:
- IScheduleServers._domainMap[domain] = server
-
- def mapDomain(self, domain):
- """
- Map a calendar user address domain to a suitable server that can
- handle server-to-server requests for that user.
- """
- return IScheduleServers._domainMap.get(domain)
-
-ELEMENT_SERVERS = "servers"
-ELEMENT_SERVER = "server"
-ELEMENT_URI = "uri"
-ELEMENT_AUTHENTICATION = "authentication"
-ATTRIBUTE_TYPE = "type"
-ATTRIBUTE_BASICAUTH = "basic"
-ELEMENT_USER = "user"
-ELEMENT_PASSWORD = "password"
-ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
-ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
-ELEMENT_DOMAINS = "domains"
-ELEMENT_DOMAIN = "domain"
-ELEMENT_CLIENT_HOSTS = "hosts"
-ELEMENT_HOST = "host"
-
-class IScheduleServersParser(object):
- """
- Server-to-server configuration file parser.
- """
- def __repr__(self):
- return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
-
- def __init__(self, xmlFile):
-
- self.servers = []
-
- # Read in XML
- try:
- _ignore_tree, servers_node = readXML(xmlFile.path, ELEMENT_SERVERS)
- except ValueError:
- log.error("Ignoring file %r because it is not a server-to-server config file" % (xmlFile,))
-
- self._parseXML(servers_node)
-
- def _parseXML(self, node):
- """
- Parse the XML root node from the server-to-server configuration document.
- @param node: the L{Node} to parse.
- """
- for child in node.getchildren():
- if child.tag == ELEMENT_SERVER:
- self.servers.append(IScheduleServerRecord())
- self.servers[-1].parseXML(child)
-
-class IScheduleServerRecord (object):
- """
- Contains server-to-server details.
- """
- def __init__(self, uri=None):
- """
- @param recordType: record type for directory entry.
- """
- self.uri = ""
- self.authentication = None
- self.allow_from = False
- self.allow_to = True
- self.domains = []
- self.client_hosts = []
- self.unNormalizeAddresses = True
- self.moreHeaders = []
-
- if uri:
- self.uri = uri
- self._parseDetails()
-
- def parseXML(self, node):
- for child in node.getchildren():
- if child.tag == ELEMENT_URI:
- self.uri = child.text
- elif child.tag == ELEMENT_AUTHENTICATION:
- self._parseAuthentication(child)
- elif child.tag == ELEMENT_ALLOW_REQUESTS_FROM:
- self.allow_from = True
- elif child.tag == ELEMENT_ALLOW_REQUESTS_TO:
- self.allow_to = True
- elif child.tag == ELEMENT_DOMAINS:
- self._parseList(child, ELEMENT_DOMAIN, self.domains)
- elif child.tag == ELEMENT_CLIENT_HOSTS:
- self._parseList(child, ELEMENT_HOST, self.client_hosts)
- else:
- raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child.tag,))
-
- self._parseDetails()
-
- def _parseList(self, node, element_name, appendto):
- for child in node.getchildren():
- if child.tag == element_name:
- appendto.append(child.text)
-
- def _parseAuthentication(self, node):
- atype = node.getAttribute(ATTRIBUTE_TYPE, "")
- if atype != ATTRIBUTE_BASICAUTH:
- return
-
- for child in node.getchildren():
- if child.tag == ELEMENT_USER:
- user = child.text
- elif child.tag == ELEMENT_PASSWORD:
- password = child.text
-
- self.authentication = ("basic", user, password,)
-
- def _parseDetails(self):
- # Extract scheme, host, port and path
- if self.uri.startswith("http://"):
- self.ssl = False
- rest = self.uri[7:]
- elif self.uri.startswith("https://"):
- self.ssl = True
- rest = self.uri[8:]
-
- splits = rest.split("/", 1)
- hostport = splits[0].split(":")
- self.host = hostport[0]
- if len(hostport) > 1:
- self.port = int(hostport[1])
- else:
- self.port = {False:80, True:443}[self.ssl]
- self.path = "/"
- if len(splits) > 1:
- self.path += splits[1]
Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -37,8 +37,6 @@
log = Logger()
-__version__ = "0.0"
-
__all__ = [
"iTipProcessing",
"iTipGenerator",
Modified: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -57,15 +57,18 @@
log = Logger()
class ImplicitProcessorException(Exception):
-
+
def __init__(self, msg):
self.msg = msg
+
+
class ImplicitProcessor(object):
-
+
def __init__(self):
pass
+
@inlineCallbacks
def doImplicitProcessing(self, request, message, originator, recipient):
"""
@@ -78,7 +81,7 @@
@type originator:
@param recipient:
@type recipient:
-
+
@return: a C{tuple} of (C{bool}, C{bool}) indicating whether the message was processed, and if it was whether
auto-processing has taken place.
"""
@@ -87,10 +90,10 @@
self.message = message
self.originator = originator
self.recipient = recipient
-
+
# TODO: for now going to assume that the originator is local - i.e. the scheduling message sent
# represents the actual organizer's view.
-
+
# First see whether this is the organizer or attendee sending the message
self.extractCalendarData()
@@ -126,24 +129,28 @@
returnValue(result)
+
def extractCalendarData(self):
-
+
# Some other useful things
self.method = self.message.propertyValue("METHOD")
self.uid = self.message.resourceUID()
-
+
+
def isOrganizerReceivingMessage(self):
return self.method in ("REPLY", "REFRESH")
+
def isAttendeeReceivingMessage(self):
return self.method in ("REQUEST", "ADD", "CANCEL")
+
@inlineCallbacks
def getRecipientsCopy(self):
"""
Get the Recipient's copy of the event being processed.
"""
-
+
self.recipient_calendar = None
self.recipient_calendar_collection = None
self.recipient_calendar_collection_uri = None
@@ -154,7 +161,8 @@
self.recipient_calendar_collection = calendar_collection
self.recipient_calendar_collection_uri = calendar_collection_uri
self.recipient_calendar_name = resource_name
-
+
+
@inlineCallbacks
def doImplicitOrganizer(self):
@@ -169,30 +177,31 @@
result = (yield self.doImplicitOrganizerUpdate())
elif self.method == "REFRESH":
# With implicit we ignore refreshes.
- # TODO: for iMIP etc we do need to handle them
+ # TODO: for iMIP etc we do need to handle them
result = (True, True, False, None,)
returnValue(result)
+
@inlineCallbacks
def doImplicitOrganizerUpdate(self):
-
+
# Check to see if this is a valid reply
result, processed = iTipProcessing.processReply(self.message, self.recipient_calendar)
if result:
-
+
# Let the store know that no time-range info has changed
self.recipient_calendar.noInstanceIndexing = True
# Update the organizer's copy of the event
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REPLY, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
self.organizer_calendar_resource = (yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar))
-
+
# Build the schedule-changes XML element
attendeeReplying, rids = processed
partstatChanged = False
reply_details = (customxml.Attendee.fromString(attendeeReplying),)
-
+
for rid, partstatChanged, privateCommentChanged in sorted(rids):
recurrence = []
if rid == "":
@@ -201,7 +210,7 @@
recurrence.append(customxml.RecurrenceID.fromString(rid))
changes = []
if partstatChanged:
- changes.append(customxml.ChangedProperty(customxml.ChangedParameter(name="PARTSTAT"), name="ATTENDEE" ))
+ changes.append(customxml.ChangedProperty(customxml.ChangedParameter(name="PARTSTAT"), name="ATTENDEE"))
partstatChanged = True
if privateCommentChanged:
changes.append(customxml.ChangedProperty(name="X-CALENDARSERVER-PRIVATE-COMMENT"))
@@ -229,15 +238,16 @@
returnValue(result)
+
@inlineCallbacks
def queueAttendeeUpdate(self, exclude_attendees):
"""
Queue up an update to attendees and use a memcache lock to ensure we don't update too frequently.
-
+
@param exclude_attendees: list of attendees who should not be refreshed (e.g., the one that triggeed the refresh)
@type exclude_attendees: C{list}
"""
-
+
# When doing auto-processing of replies, only refresh attendees when the last auto-accept is done.
# Note that when we do this we also need to refresh the attendee that is generating the reply because they
# are no longer up to date with changes of other auto-accept attendees.
@@ -251,7 +261,7 @@
# Check for batched refreshes
if config.Scheduling.Options.AttendeeRefreshBatch:
-
+
# Need to lock whilst manipulating the batch list
lock = MemcacheLock(
"BatchRefreshUIDLock",
@@ -264,14 +274,14 @@
except MemcacheLockTimeoutError:
# If we could not lock then just fail the refresh - not sure what else to do
returnValue(None)
-
+
try:
# Get all attendees to refresh
allAttendees = sorted(list(self.recipient_calendar.getAllUniqueAttendees()))
-
+
# Always need to refresh every attendee
exclude_attendees = ()
-
+
# See if there is already a pending refresh and merge current attendees into that list,
# otherwise just mark all attendees as pending
cache = Memcacher("BatchRefreshAttendees", pickle=True)
@@ -285,16 +295,17 @@
firstTime = True
pendingAttendees = allAttendees
yield cache.set(self.uid, pendingAttendees)
-
+
# Now start the first batch off
if firstTime:
reactor.callLater(config.Scheduling.Options.AttendeeRefreshBatchDelaySeconds, self._doBatchRefresh)
finally:
yield lock.clean()
-
+
else:
yield self._doRefresh(self.organizer_calendar_resource, exclude_attendees)
+
@inlineCallbacks
def _doRefresh(self, organizer_resource, exclude_attendees=(), only_attendees=None):
"""
@@ -304,7 +315,7 @@
@type organizer_resource: L{DAVResource}
@param exclude_attendees: list of attendees to not refresh
@type exclude_attendees: C{tuple}
- @param only_attendees: list of attendees to refresh (C{None} - refresh all)
+ @param only_attendees: list of attendees to refresh (C{None} - refresh all)
@type only_attendees: C{tuple}
"""
log.debug("ImplicitProcessing - refreshing UID: '%s', Attendees: %s" % (self.uid, ", ".join(only_attendees) if only_attendees else "all"))
@@ -316,7 +327,8 @@
exclude_attendees,
only_attendees=only_attendees,
)
-
+
+
@inlineCallbacks
def _doDelayedRefresh(self, attendeesToProcess):
"""
@@ -366,6 +378,7 @@
finally:
yield uidlock.clean()
+
@inlineCallbacks
def _doBatchRefresh(self):
"""
@@ -391,7 +404,7 @@
cache = Memcacher("BatchRefreshAttendees", pickle=True)
pendingAttendees = yield cache.get(self.uid)
if pendingAttendees:
-
+
# Get the next batch of attendees to process and update the cache value or remove it if
# no more processing is needed
attendeesToProcess = pendingAttendees[:config.Scheduling.Options.AttendeeRefreshBatch]
@@ -400,13 +413,13 @@
yield cache.set(self.uid, pendingAttendees)
else:
yield cache.delete(self.uid)
-
+
# Make sure we release this here to avoid potential deadlock when grabbing the ImplicitUIDLock in the next call
yield lock.release()
-
+
# Now do the batch refresh
yield self._doDelayedRefresh(attendeesToProcess)
-
+
# Queue the next refresh if needed
if pendingAttendees:
reactor.callLater(config.Scheduling.Options.AttendeeRefreshBatchIntervalSeconds, self._doBatchRefresh)
@@ -415,7 +428,8 @@
yield lock.release()
finally:
yield lock.clean()
-
+
+
@inlineCallbacks
def doImplicitAttendee(self):
@@ -428,12 +442,13 @@
result = (True, True, False, None)
else:
result = (yield self.doImplicitAttendeeUpdate())
-
+
returnValue(result)
+
@inlineCallbacks
def doImplicitAttendeeUpdate(self):
-
+
# Do security check: ORGANZIER in iTIP MUST match existing resource value
if self.recipient_calendar:
existing_organizer = self.recipient_calendar.getOrganizer()
@@ -455,9 +470,10 @@
else:
# NB We should never get here as we will have rejected unsupported METHODs earlier.
result = (True, True, False, None,)
-
+
returnValue(result)
+
@inlineCallbacks
def doImplicitAttendeeRequest(self):
"""
@@ -466,14 +482,14 @@
# If there is no existing copy, then look for default calendar and copy it here
if self.new_resource:
-
+
# Check if the incoming data has the recipient declined in all instances. In that case we will not create
# a new resource as chances are the recipient previously deleted the resource and we want to keep it deleted.
attendees = self.message.getAttendeeProperties((self.recipient.cuaddr,))
if all([attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED" for attendee in attendees]):
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - ignoring all declined" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
returnValue((True, False, False, None,))
-
+
# Check for default calendar
default = (yield self.recipient.inbox.defaultCalendar(self.request, self.message.mainType()))
if default is None:
@@ -482,11 +498,11 @@
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - new processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
new_calendar = iTipProcessing.processNewRequest(self.message, self.recipient.cuaddr, creating=True)
-
+
# Handle auto-reply behavior
if self.recipient.principal.canAutoSchedule():
send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar, self.recipient.principal.getAutoScheduleMode()))
-
+
# Only store inbox item when reply is not sent or always for users
store_inbox = store_inbox or self.recipient.principal.getCUType() == "INDIVIDUAL"
else:
@@ -494,7 +510,7 @@
store_inbox = True
new_resource = (yield self.writeCalendarResource(default.url(), default, None, new_calendar))
-
+
if send_reply:
# Track outstanding auto-reply processing
if not hasattr(self.request, "auto_reply_processing_count"):
@@ -515,11 +531,11 @@
# Processing update to existing event
new_calendar, rids = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr)
if new_calendar:
-
+
# Handle auto-reply behavior
if self.recipient.principal.canAutoSchedule():
send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar, self.recipient.principal.getAutoScheduleMode()))
-
+
# Only store inbox item when reply is not sent or always for users
store_inbox = store_inbox or self.recipient.principal.getCUType() == "INDIVIDUAL"
else:
@@ -529,11 +545,11 @@
# Let the store know that no time-range info has changed for a refresh
if hasattr(self.request, "doing_attendee_refresh"):
new_calendar.noInstanceIndexing = True
-
+
# Update the attendee's copy of the event
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
new_resource = (yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, new_calendar))
-
+
if send_reply:
# Track outstanding auto-reply processing
if not hasattr(self.request, "auto_reply_processing_count"):
@@ -544,16 +560,16 @@
# Build the schedule-changes XML element
update_details = []
- for rid, props_changed in sorted(rids.iteritems(), key=lambda x:x[0]):
+ for rid, props_changed in sorted(rids.iteritems(), key=lambda x: x[0]):
recurrence = []
if rid == "":
recurrence.append(customxml.Master())
else:
recurrence.append(customxml.RecurrenceID.fromString(rid))
changes = []
- for propName, paramNames in sorted(props_changed.iteritems(), key=lambda x:x[0]):
+ for propName, paramNames in sorted(props_changed.iteritems(), key=lambda x: x[0]):
params = tuple([customxml.ChangedParameter(name=param) for param in paramNames])
- changes.append(customxml.ChangedProperty(*params, **{"name":propName}))
+ changes.append(customxml.ChangedProperty(*params, **{"name": propName}))
recurrence.append(customxml.Changes(*changes))
update_details += (customxml.Recurrence(*recurrence),)
@@ -563,13 +579,13 @@
customxml.Update(*update_details),
),
)
-
+
# Refresh from another Attendee should not have Inbox item
if hasattr(self.request, "doing_attendee_refresh"):
store_inbox = False
result = (True, send_reply, store_inbox, changes,)
-
+
else:
# Request needs to be ignored
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
@@ -597,7 +613,7 @@
processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar, autoprocessing=autoprocessed)
if processed_message:
if delete_original:
-
+
# Delete the attendee's copy of the event
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - deleting entire event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
yield self.deleteCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name)
@@ -610,9 +626,9 @@
),
)
result = (True, autoprocessed, store_inbox, changes,)
-
+
else:
-
+
# Update the attendee's copy of the event
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar)
@@ -635,6 +651,7 @@
returnValue(result)
+
@inlineCallbacks
def sendAttendeeAutoReply(self, calendar, resource, partstat):
"""
@@ -646,7 +663,7 @@
@return: L{Component} for the new calendar data to write
"""
-
+
# We need to get the UID lock for implicit processing whilst we send the auto-reply
# as the Organizer processing will attempt to write out data to other attendees to
# refresh them. To prevent a race we need a lock.
@@ -695,6 +712,7 @@
if hasattr(self.request, "auto_reply_processing_count"):
self.request.auto_reply_processing_count -= 1
+
@inlineCallbacks
def checkAttendeeAutoReply(self, calendar, automode):
"""
@@ -702,7 +720,7 @@
A reply will either be positive (accepted invitation) or negative (denied invitation).
In addition we will modify calendar to reflect
any new state (e.g. set PARTSTAT to ACCEPTED or DECLINED).
-
+
BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
At the moment we will treat a failure on one instance as a DECLINE of the entire set.
@@ -714,7 +732,7 @@
@return: C{tuple} of C{bool}, C{bool}, C{str} indicating whether changes were made, whether the inbox item
should be added, and the new PARTSTAT.
"""
-
+
# First ignore the none mode
if automode == "none":
returnValue((False, True, "",))
@@ -727,26 +745,26 @@
if automode in ("accept-always", "decline-always",):
all_accepted = automode == "accept-always"
all_declined = automode == "decline-always"
-
+
# Other modes need freebusy check
else:
# First expand current one to get instances (only go 1 year into the future)
- default_future_expansion_duration = PyCalendarDuration(days=356*1)
+ default_future_expansion_duration = PyCalendarDuration(days=356 * 1)
expand_max = PyCalendarDateTime.getToday() + default_future_expansion_duration
instances = calendar.expandTimeRanges(expand_max, ignoreInvalidInstances=True)
instance_states = dict([(instance, True) for instance in instances.instances.itervalues()])
-
+
# Extract UID from primary component as we want to ignore this one if we match it
# in any calendars.
comp = calendar.mainComponent(allow_multiple=True)
uid = comp.propertyValue("UID")
-
+
# Now compare each instance time-range with the index and see if there is an overlap
calendars = (yield self._getCalendarsToMatch())
-
+
for calURL in calendars:
testcal = (yield self.request.locateResource(calURL))
-
+
# Get the timezone property from the collection, and store in the query filter
# for use during the query itself.
has_prop = (yield testcal.hasProperty((caldav_namespace, "calendar-timezone"), self.request))
@@ -755,14 +773,14 @@
tzinfo = tz.calendar().gettimezone()
else:
tzinfo = PyCalendarTimezone(utc=True)
-
+
# Now do search for overlapping time-range
for instance in instances.instances.itervalues():
if instance_states[instance]:
try:
# First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
fbinfo = ([], [], [])
-
+
def makeTimedUTC(dt):
dt = dt.duplicate()
if dt.isDateOnly():
@@ -772,28 +790,28 @@
dt.setTimezone(tzinfo)
dt.adjustToUTC()
return dt
-
+
tr = caldavxml.TimeRange(
start=str(makeTimedUTC(instance.start)),
end=str(makeTimedUTC(instance.end)),
)
-
+
yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid, servertoserver=True)
-
+
# If any fbinfo entries exist we have an overlap
if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
instance_states[instance] = False
except NumberOfMatchesWithinLimits:
instance_states[instance] = False
log.info("Exceeded number of matches whilst trying to find free-time.")
-
+
# If everything is declined we can exit now
if not any(instance_states.itervalues()):
break
-
+
# TODO: here we should do per-instance ACCEPT/DECLINE behavior
# For now we will assume overall ACCEPT/DECLINE
-
+
# Collect all the accepted and declined states
all_accepted = all(instance_states.itervalues())
all_declined = not any(instance_states.itervalues())
@@ -805,7 +823,7 @@
attendeeProps = calendar.getAttendeeProperties(cuas)
if not attendeeProps:
returnValue((False, True, "",))
-
+
if automode == "accept-always":
freePartstat = busyPartstat = "ACCEPTED"
elif automode == "decline-always":
@@ -817,26 +835,26 @@
partstat = freePartstat if all_accepted else busyPartstat
calendar.replacePropertyInAllComponents(Property("TRANSP", "OPAQUE" if all_accepted and freeStateOpaque else "TRANSPARENT"))
-
+
made_changes = self.changeAttendeePartstat(attendeeProps, partstat)
store_inbox = partstat == "NEEDS-ACTION"
-
+
else:
# Hard case: some accepted some declined
# What we will do is mark any master instance as accepted, then mark each existing
# overridden instance as accepted or declined, and generate new overridden instances for
# any other declines.
-
+
made_changes = False
store_inbox = False
partstat = "MIXED RESPONSE"
-
+
freePartstat = "ACCEPTED" if automode in ("accept-if-free", "automatic",) else "NEEDS-ACTION"
busyPartstat = "DECLINED" if automode in ("decline-if-busy", "automatic",) else "NEEDS-ACTION"
freeStateOpaque = freePartstat == "ACCEPTED"
# Default state is whichever of free or busy has most instances
- defaultStateFree = len(filter(lambda x:x, instance_states.values())) >= len(instance_states.keys()) / 2
+ defaultStateFree = len(filter(lambda x: x, instance_states.values())) >= len(instance_states.keys()) / 2
# See if there is a master component first
hadMasterRsvp = False
@@ -853,12 +871,12 @@
# Look at expanded instances and change partstat accordingly
for instance, free in sorted(instance_states.iteritems(), key=lambda x: x[0].rid):
-
+
overridden = calendar.overriddenComponent(instance.rid)
if not overridden and free == defaultStateFree:
# Nothing to do as state matches the master
- continue
-
+ continue
+
if overridden:
# Change ATTENDEE property to match new state
attendee = overridden.getAttendeeProperty(cuas)
@@ -870,7 +888,7 @@
overridden.replaceProperty(Property("TRANSP", "OPAQUE" if free and freeStateOpaque else "TRANSPARENT"))
else:
# Derive a new overridden component and change partstat. We also need to make sure we restore any RSVP
- # value that may have been overwritten by any change to the master itself.
+ # value that may have been overwritten by any change to the master itself.
derived = calendar.deriveInstance(instance.rid)
if derived:
attendee = derived.getAttendeeProperty(cuas)
@@ -882,56 +900,58 @@
derived.replaceProperty(Property("TRANSP", "OPAQUE" if free and freeStateOpaque else "TRANSPARENT"))
calendar.addComponent(derived)
made_changes = True
-
+
# Fake a SCHEDULE-STATUS on the ORGANIZER property
if made_changes:
calendar.setParameterToValueForPropertyWithValue("SCHEDULE-STATUS", iTIPRequestStatus.MESSAGE_DELIVERED_CODE, "ORGANIZER", None)
-
+
returnValue((made_changes, store_inbox, partstat,))
+
def _getCalendarsToMatch(self):
# Determine the set of calendar URIs for a principal need to be searched.
-
+
# Find the current recipients calendar-free-busy-set
return self.recipient.principal.calendarFreeBusyURIs(self.request)
+
@inlineCallbacks
def writeCalendarResource(self, collURL, collection, name, calendar):
"""
Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
resource or by creating a new one.
-
+
@param collURL: the C{str} containing the URL of the calendar collection.
@param collection: the L{CalDAVResource} for the calendar collection to store the resource in.
@param name: the C{str} for the resource name to write into, or {None} to write a new resource.
@param calendar: the L{Component} calendar to write.
@return: L{Deferred} -> L{CalDAVResource}
"""
-
+
# Create a new name if one was not provided
if name is None:
- name = "%s-%s.ics" % (hashlib.md5(calendar.resourceUID()).hexdigest(), str(uuid.uuid4())[:8],)
-
+ name = "%s-%s.ics" % (hashlib.md5(calendar.resourceUID()).hexdigest(), str(uuid.uuid4())[:8],)
+
# Get a resource for the new item
newchildURL = joinURL(collURL, name)
newchild = yield self.request.locateResource(newchildURL)
newchild._url = newchildURL
-
+
# Now write it to the resource
from twistedcaldav.method.put_common import StoreCalendarObjectResource
yield StoreCalendarObjectResource(
request=self.request,
- destination = newchild,
- destination_uri = newchildURL,
- destinationparent = collection,
- destinationcal = True,
- calendar = calendar,
- isiTIP = False,
- allowImplicitSchedule = False,
- internal_request = True,
- processing_organizer = self.isOrganizerReceivingMessage(),
+ destination=newchild,
+ destination_uri=newchildURL,
+ destinationparent=collection,
+ destinationcal=True,
+ calendar=calendar,
+ isiTIP=False,
+ allowImplicitSchedule=False,
+ internal_request=True,
+ processing_organizer=self.isOrganizerReceivingMessage(),
).run()
-
+
returnValue(newchild)
@@ -939,7 +959,7 @@
def deleteCalendarResource(self, collURL, collection, name):
"""
Delete the calendar resource in the specified calendar.
-
+
@param collURL: the URL of the calendar collection.
@type name: C{str}
@param collection: the calendar collection to delete the resource from.
@@ -963,7 +983,7 @@
@type partstat: C{str}
@param hadRSVP: indicates whether RSVP should be added when changing to NEEDS-ACTION
@type hadRSVP: C{bool}
-
+
@return: C{True} if any change was made, C{False} otherwise
"""
@@ -986,15 +1006,15 @@
except KeyError:
pass
-
return madeChanges
+
@inlineCallbacks
def doImplicitAttendeeEventFix(self, ex):
# Only certain types of exception should be handled - ones related to calendar data errors.
# All others should result in the scheduling response coming back as a 5.x code
-
+
if type(ex) not in (InvalidOverriddenInstanceError, HTTPError):
raise ImplicitProcessorException("5.1;Service unavailable")
@@ -1013,12 +1033,12 @@
# Locate the attendee's copy of the event if it exists.
recipient_resource, recipient_resource_name, recipient_collection, recipient_collection_uri = (yield getCalendarObjectForPrincipals(self.request, self.recipient.principal, self.uid))
-
+
# We only need to fix data that already exists
if recipient_resource:
if originator_calendar.mainType() != None:
yield self.writeCalendarResource(recipient_collection_uri, recipient_collection, recipient_resource_name, originator_calendar)
else:
yield self.deleteCalendarResource(recipient_collection_uri, recipient_collection, recipient_resource_name)
-
+
returnValue(True)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -14,12 +14,6 @@
# limitations under the License.
##
-import itertools
-import re
-import socket
-import urlparse
-
-from twisted.internet.abstract import isIPAddress
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.python.failure import Failure
@@ -28,30 +22,26 @@
from twext.web2.http import HTTPError, Response, StatusResponse
from twext.web2.http_headers import MimeType
from txdav.xml import element as davxml
-from twext.web2.dav.http import errorForFailure, messageForFailure, statusForFailure
-from twext.web2.dav.http import ErrorResponse
+from twext.web2.dav.http import messageForFailure, statusForFailure, \
+ ErrorResponse
from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.customxml import calendarserver_namespace
from twistedcaldav.accounting import accountingEnabled, emitAccounting
from twistedcaldav.config import config
-from twistedcaldav.ical import Component, normalizeCUAddress
+from twistedcaldav.ical import Component
from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
from twistedcaldav.scheduling import addressmapping
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
-from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
+from twistedcaldav.scheduling.caldav.delivery import ScheduleViaCalDAV
+from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser, \
calendarUserFromPrincipal, OtherServerCalendarUser
from twistedcaldav.scheduling.cuaddress import LocalCalendarUser
from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
from twistedcaldav.scheduling.cuaddress import EmailCalendarUser
from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser
-from twistedcaldav.scheduling.imip import ScheduleViaIMip
-from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
+from twistedcaldav.scheduling.imip.delivery import ScheduleViaIMip
+from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.servers import Servers
-from twistedcaldav.util import normalizationLookup
"""
CalDAV/Server-to-Server scheduling behavior.
@@ -88,7 +78,7 @@
\_L{ImplicitProcessor} - dispatches iTIP message (also auto-accept)
\
\_L{iTipProcessing} - processes iTIP message
-
+
Here is a typical flow of activity for a iTIP between an organizer on the server and an iMIP attendee:
iTIP PUT request
@@ -115,9 +105,7 @@
__all__ = [
"Scheduler",
- "CalDAVScheduler",
- "IScheduleScheduler",
- "IMIPScheduler",
+ "RemoteScheduler",
"DirectScheduler",
]
@@ -125,7 +113,25 @@
log = Logger()
class Scheduler(object):
-
+
+ scheduleResponse = None
+
+ errorResponse = None # The class used for generating an HTTP XML error response
+
+ errorElements = {
+ "originator-missing": (),
+ "originator-invalid": (),
+ "originator-denied": (),
+ "recipient-missing": (),
+ "recipient-invalid": (),
+ "organizer-denied": (),
+ "attendee-denied": (),
+ "invalid-calendar-data-type": (),
+ "invalid-calendar-data": (),
+ "invalid-scheduling-message": (),
+ "max-recipients": (),
+ }
+
def __init__(self, request, resource):
self.request = request
self.resource = resource
@@ -140,18 +146,19 @@
self.fakeTheResult = False
self.method = "Unknown"
self.internal_request = False
-
+
+
@inlineCallbacks
def doSchedulingViaPOST(self, transaction, use_request_headers=False):
"""
The Scheduling POST operation on an Outbox.
"""
-
+
self.method = "POST"
# Load various useful bits doing some basic checks on those
yield self.loadCalendarFromRequest()
-
+
if use_request_headers:
self.loadFromRequestHeaders()
else:
@@ -161,7 +168,7 @@
self.request.extendedLogItems = {}
self.request.extendedLogItems["recipients"] = len(self.recipients)
self.request.extendedLogItems["cl"] = str(len(str(self.calendar)))
-
+
# Do some extra authorization checks
self.checkAuthorization()
@@ -184,15 +191,16 @@
# Release lock after commit or abort
transaction.postCommit(lock.clean)
transaction.postAbort(lock.clean)
-
+
result = (yield self.doScheduling())
returnValue(result)
+
def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
"""
The implicit scheduling PUT operation.
"""
-
+
self.method = "PUT"
# Load various useful bits doing some basic checks on those
@@ -206,23 +214,24 @@
return self.doScheduling()
+
@inlineCallbacks
def doScheduling(self):
# Check validity of Originator header.
yield self.checkOriginator()
-
+
# Get recipient details.
yield self.checkRecipients()
-
+
# Check calendar data.
self.checkCalendarData()
-
+
# Check validity of ORGANIZER
yield self.checkOrganizer()
-
+
# Do security checks (e.g. spoofing)
yield self.securityChecks()
-
+
# Generate accounting information
self.doAccounting()
@@ -234,11 +243,13 @@
returnValue(result)
+
@inlineCallbacks
def loadFromRequestData(self):
yield self.loadOriginatorFromRequestDetails()
self.loadRecipientsFromCalendarData()
-
+
+
@inlineCallbacks
def loadOriginatorFromRequestDetails(self):
# Get the originator who is the authenticated user
@@ -255,14 +266,15 @@
if not originator:
log.err("%s request must have Originator" % (self.method,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "originator-specified"),
+ self.errorElements["originator-missing"],
"Missing originator",
))
else:
self.originator = originator
+
def loadRecipientsFromCalendarData(self):
# Get the ATTENDEEs
@@ -272,17 +284,18 @@
if attendee not in unique_set:
attendees.append(attendee)
unique_set.add(attendee)
-
+
if not attendees:
log.err("%s request must have at least one Recipient" % (self.method,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "recipient-specified"),
+ self.errorElements["recipient-missing"],
"Must have recipients",
))
else:
self.recipients = list(attendees)
+
def loadFromRequestHeaders(self):
"""
Load Originator and Recipient from request headers.
@@ -290,30 +303,32 @@
self.loadOriginatorFromRequestHeaders()
self.loadRecipientsFromRequestHeaders()
+
def loadOriginatorFromRequestHeaders(self):
# Must have Originator header
originator = self.request.headers.getRawHeaders("originator")
if originator is None or (len(originator) != 1):
log.err("%s request must have Originator header" % (self.method,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "originator-specified"),
+ self.errorElements["originator-missing"],
"Missing originator",
))
else:
self.originator = originator[0]
-
+
+
def loadRecipientsFromRequestHeaders(self):
# Get list of Recipient headers
rawRecipients = self.request.headers.getRawHeaders("recipient")
if rawRecipients is None or (len(rawRecipients) == 0):
log.err("%s request must have at least one Recipient header" % (self.method,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "recipient-specified"),
+ self.errorElements["recipient-missing"],
"No recipients",
))
-
+
# Recipient header may be comma separated list
self.recipients = []
for rawRecipient in rawRecipients:
@@ -321,33 +336,35 @@
r = r.strip()
if len(r):
self.recipients.append(r)
-
+
+
@inlineCallbacks
def loadCalendarFromRequest(self):
# Must be content-type text/calendar
contentType = self.request.headers.getHeader("content-type")
if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
log.err("MIME type %s not allowed in calendar collection" % (contentType,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "supported-calendar-data"),
+ self.errorElements["invalid-calendar-data-type"],
"Data is not calendar data",
))
-
+
# Parse the calendar object from the HTTP request stream
try:
self.calendar = (yield Component.fromIStream(self.request.stream))
-
+
self.preProcessCalendarData()
except:
# FIXME: Bare except
log.err("Error while handling %s: %s" % (self.method, Failure(),))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
+ self.errorElements["invalid-calendar-data"],
description="Can't parse calendar data"
))
+
def preProcessCalendarData(self):
"""
After loading calendar data from the request, do some optional processing of it. This method will be
@@ -355,68 +372,75 @@
"""
pass
+
def checkAuthorization(self):
raise NotImplementedError
+
def checkOriginator(self):
raise NotImplementedError
+
def checkRecipients(self):
raise NotImplementedError
+
def checkOrganizer(self):
raise NotImplementedError
+
def checkOrganizerAsOriginator(self):
raise NotImplementedError
+
def checkAttendeeAsOriginator(self):
raise NotImplementedError
+
def checkCalendarData(self):
-
+
# Skip all the valid data checks for an internal request as we are going to assume all the internal
# request data has been generated properly.
-
+
if not self.internal_request:
# Must be a valid calendar
try:
self.calendar.validCalendarData()
except ValueError, e:
log.err("%s request calendar component is not valid:%s %s" % (self.method, e, self.calendar,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
+ self.errorElements["invalid-calendar-data"],
description="Calendar component is not valid"
))
-
+
# Must have a METHOD
if not self.calendar.isValidMethod():
log.err("%s request must have valid METHOD property in calendar component: %s" % (self.method, self.calendar,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
+ self.errorElements["invalid-scheduling-message"],
description="Must have valid METHOD property"
))
-
+
# Verify iTIP behavior
if not self.calendar.isValidITIP():
log.err("%s request must have a calendar component that satisfies iTIP requirements: %s" % (self.method, self.calendar,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
+ self.errorElements["invalid-scheduling-message"],
description="Must have a calendar component that satisfies iTIP requirements"
))
-
+
# X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component %s request: %s" % (self.method, self.calendar,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
(calendarserver_namespace, "no-access-restrictions"),
"Private events cannot be scheduled",
))
-
+
# Determine iTIP method mode
if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
self.isiTIPRequest = True
@@ -426,13 +450,13 @@
# Verify that there is a single ATTENDEE property
attendees = self.calendar.getAttendees()
-
+
# Must have only one
if len(attendees) != 1:
log.err("Wrong number of ATTENDEEs in calendar data: %s" % (str(self.calendar),))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "attendee-allowed"),
+ self.errorElements["invalid-scheduling-message"],
"Wrong number of attendees",
))
self.attendee = attendees[0]
@@ -440,12 +464,13 @@
else:
msg = "Unknown iTIP METHOD: %s" % (self.calendar.propertyValue("METHOD"),)
log.err(msg)
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
+ self.errorElements["invalid-scheduling-message"],
description=msg
))
+
def checkForFreeBusy(self):
if not hasattr(self, "isfreebusy"):
if (self.calendar.propertyValue("METHOD") == "REQUEST") and (self.calendar.mainType() == "VFREEBUSY"):
@@ -453,48 +478,50 @@
vfreebusies = [v for v in self.calendar.subcomponents() if v.name() == "VFREEBUSY"]
if len(vfreebusies) != 1:
log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
+ self.errorElements["invalid-scheduling-message"],
"iTIP data is not valid for a VFREEBUSY request",
))
dtstart = vfreebusies[0].getStartDateUTC()
dtend = vfreebusies[0].getEndDateUTC()
if dtstart is None or dtend is None:
log.err("VFREEBUSY start/end not valid: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
+ self.errorElements["invalid-scheduling-message"],
"VFREEBUSY start/end not valid",
))
# Some clients send floating instead of UTC - coerce to UTC
if not dtstart.utc() or not dtend.utc():
log.err("VFREEBUSY start or end not UTC: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
+ raise HTTPError(self.errorResponse(
responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
+ self.errorElements["invalid-scheduling-message"],
"VFREEBUSY start or end not UTC",
))
self.timeRange = caldavxml.TimeRange(start=dtstart.getText(), end=dtend.getText())
self.timeRange.start = dtstart
self.timeRange.end = dtend
-
+
# Look for masked UID
self.excludeUID = self.calendar.getMaskUID()
-
+
# Do free busy operation
self.isfreebusy = True
else:
# Do regular invite (fan-out)
self.isfreebusy = False
-
+
return self.isfreebusy
-
+
+
def securityChecks(self):
raise NotImplementedError
+
def doAccounting(self):
#
# Accounting
@@ -519,12 +546,14 @@
)
)
+
def finalChecks(self):
"""
Final checks before doing the actual scheduling.
"""
pass
+
@inlineCallbacks
def generateSchedulingResponse(self):
@@ -534,8 +563,8 @@
freebusy = self.checkForFreeBusy()
# Prepare for multiple responses
- responses = ScheduleResponseQueue(self.method, responsecode.OK)
-
+ responses = self.scheduleResponse(self.method, responsecode.OK)
+
# Loop over each recipient and aggregate into lists by service types.
caldav_recipients = []
partitioned_recipients = []
@@ -543,20 +572,20 @@
remote_recipients = []
imip_recipients = []
for ctr, recipient in enumerate(self.recipients):
-
+
# Check for freebusy limit
if freebusy and config.Scheduling.Options.LimitFreeBusyAttendees and ctr >= config.Scheduling.Options.LimitFreeBusyAttendees:
- err = HTTPError(ErrorResponse(
+ err = HTTPError(self.errorResponse(
responsecode.NOT_FOUND,
- (caldav_namespace, "recipient-limit"),
+ self.errorElements["max-recipients"],
"Too many attendees",
))
responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
continue
-
+
if self.fakeTheResult:
responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS if freebusy else iTIPRequestStatus.MESSAGE_DELIVERED)
-
+
elif isinstance(recipient, LocalCalendarUser):
caldav_recipients.append(recipient)
@@ -573,9 +602,9 @@
imip_recipients.append(recipient)
else:
- err = HTTPError(ErrorResponse(
+ err = HTTPError(self.errorResponse(
responsecode.NOT_FOUND,
- (caldav_namespace, "recipient-exists"),
+ self.errorElements["recipient-invalid"],
"Unknown recipient",
))
responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.INVALID_CALENDAR_USER)
@@ -605,7 +634,8 @@
# Return with final response if we are done
returnValue(responses)
-
+
+
def generateLocalSchedulingResponses(self, recipients, responses, freebusy):
"""
Generate scheduling responses for CalDAV recipients.
@@ -615,6 +645,7 @@
requestor = ScheduleViaCalDAV(self, recipients, responses, freebusy)
return requestor.generateSchedulingResponses()
+
def generateRemoteSchedulingResponses(self, recipients, responses, freebusy, refreshOnly=False):
"""
Generate scheduling responses for remote recipients.
@@ -624,6 +655,7 @@
requestor = ScheduleViaISchedule(self, recipients, responses, freebusy)
return requestor.generateSchedulingResponses(refreshOnly)
+
def generateIMIPSchedulingResponses(self, recipients, responses, freebusy):
"""
Generate scheduling responses for iMIP recipients.
@@ -633,222 +665,8 @@
requestor = ScheduleViaIMip(self, recipients, responses, freebusy)
return requestor.generateSchedulingResponses()
-class CalDAVScheduler(Scheduler):
- def __init__(self, request, resource):
- super(CalDAVScheduler, self).__init__(request, resource)
- self.doingPOST = False
- def doSchedulingViaPOST(self, transaction):
- """
- The Scheduling POST operation on an Outbox.
- """
- self.doingPOST = True
- return super(CalDAVScheduler, self).doSchedulingViaPOST(transaction)
-
- def checkAuthorization(self):
- # Must have an authenticated user
- if not self.internal_request and self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
- log.err("Unauthenticated originators not allowed: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "originator-allowed"),
- "Invalid originator",
- ))
-
- def checkOriginator(self):
- """
- Check the validity of the Originator header. Extract the corresponding principal.
- """
-
- # Verify that Originator is a valid calendar user
- originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
- if originatorPrincipal is None:
- # Local requests MUST have a principal.
- log.err("Could not find principal for originator: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "originator-allowed"),
- "No principal for originator",
- ))
- else:
- # Must have a valid Inbox.
- inboxURL = originatorPrincipal.scheduleInboxURL()
- if inboxURL is None:
- log.err("Could not find inbox for originator: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "originator-allowed"),
- "Originator cannot be scheduled",
- ))
-
- self.originator = LocalCalendarUser(self.originator, originatorPrincipal)
-
- @inlineCallbacks
- def checkRecipients(self):
- """
- Check the validity of the Recipient header values. Map these into local or
- remote CalendarUsers.
- """
-
- results = []
- for recipient in self.recipients:
- # Get the principal resource for this recipient
- principal = self.resource.principalForCalendarUserAddress(recipient)
-
- # If no principal we may have a remote recipient but we should check whether
- # the address is one that ought to be on our server and treat that as a missing
- # user. Also if server-to-server is not enabled then remote addresses are not allowed.
- if principal is None:
- address = (yield addressmapping.mapper.getCalendarUser(recipient, principal))
- if isinstance(address, InvalidCalendarUser):
- log.err("Unknown calendar user address: %s" % (recipient,))
- results.append(address)
- else:
- # Map recipient to their inbox
- inboxURL = principal.scheduleInboxURL()
- inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
-
- if inbox:
- results.append(calendarUserFromPrincipal(recipient, principal, inbox, inboxURL))
- else:
- log.err("No schedule inbox for principal: %s" % (principal,))
- results.append(InvalidCalendarUser(recipient))
-
- self.recipients = results
-
- @inlineCallbacks
- def checkOrganizer(self):
- """
- Check the validity of the ORGANIZER value. ORGANIZER must be local.
- """
-
- # Verify that the ORGANIZER's cu address maps to a valid user
- organizer = self.calendar.getOrganizer()
- if organizer:
- organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
- if organizerPrincipal:
- outboxURL = organizerPrincipal.scheduleOutboxURL()
- if outboxURL:
-
- # Only do this check for a freebusy request. A check for an invite needs
- # to be handled later when we know whether a new invite is being added
- # (which we reject) vs an update to an existing one (which we allow).
- if self.checkForFreeBusy() and not organizerPrincipal.enabledAsOrganizer():
- log.err("ORGANIZER not allowed to be an Organizer: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "organizer-allowed"),
- "Organizer cannot schedule",
- ))
-
- self.organizer = LocalCalendarUser(organizer, organizerPrincipal)
- else:
- log.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "organizer-allowed"),
- "Organizer cannot schedule",
- ))
- else:
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
- if localUser:
- log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "organizer-allowed"),
- "No principal for organizer",
- ))
- else:
- self.organizer = RemoteCalendarUser(organizer)
- else:
- log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "organizer-allowed"),
- "Missing organizer",
- ))
-
- def checkOrganizerAsOriginator(self):
-
- # Make sure that the ORGANIZER is local
- if not isinstance(self.organizer, LocalCalendarUser):
- log.err("ORGANIZER is not local to server in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "organizer-allowed"),
- "Organizer is not local to server",
- ))
-
- # Make sure that the ORGANIZER's Outbox is the request URI
- if self.doingPOST and self.organizer.principal.scheduleOutboxURL() != self.request.uri:
- log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "organizer-allowed"),
- "Outbox does not belong to organizer",
- ))
-
- def checkAttendeeAsOriginator(self):
- """
- Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
- Only local attendees are allowed for message originating from this server.
- """
-
- # Attendee's Outbox MUST be the request URI
- attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
- if attendeePrincipal:
- if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != self.request.uri:
- log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "attendee-allowed"),
- "Outbox does not belong to attendee",
- ))
- else:
- log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "attendee-allowed"),
- "No principal for attendee",
- ))
-
- def securityChecks(self):
- """
- Check that the originator has the appropriate rights to send this type of iTIP message.
- """
-
- # Prevent spoofing of ORGANIZER with specific METHODs when local
- if self.isiTIPRequest:
- self.checkOrganizerAsOriginator()
-
- # Prevent spoofing when doing reply-like METHODs
- else:
- self.checkAttendeeAsOriginator()
-
- def finalChecks(self):
- """
- Final checks before doing the actual scheduling.
- """
-
- # With implicit scheduling only certain types of iTIP operations are allowed for POST.
-
- if self.doingPOST:
- # Freebusy requests always processed
- if self.checkForFreeBusy():
- return
-
- # COUNTER and DECLINE-COUNTER allowed
- if self.calendar.propertyValue("METHOD") in ("COUNTER", "DECLINECOUNTER"):
- return
-
- # Anything else is not allowed. However, for compatibility we will optionally
- # return a success response for all attendees.
- if config.Scheduling.CalDAV.OldDraftCompatibility:
- self.fakeTheResult = True
- else:
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid iTIP message for implicit scheduling"))
-
class RemoteScheduler(Scheduler):
def checkOrganizer(self):
@@ -857,18 +675,19 @@
"""
pass
+
@inlineCallbacks
def checkRecipients(self):
"""
Check the validity of the Recipient header values. These must all be local as there
is no concept of server-to-server relaying.
"""
-
+
results = []
for recipient in self.recipients:
# Get the principal resource for this recipient
principal = self.resource.principalForCalendarUserAddress(recipient)
-
+
# If no principal we may have a remote recipient but we should check whether
# the address is one that ought to be on our server and treat that as a missing
# user. Also if server-to-server is not enabled then remote addresses are not allowed.
@@ -889,367 +708,52 @@
else:
log.err("No schedule inbox for principal: %s" % (principal,))
results.append(InvalidCalendarUser(recipient))
-
+
self.recipients = results
-class IScheduleScheduler(RemoteScheduler):
- def loadFromRequestHeaders(self):
- """
- Load Originator and Recipient from request headers.
- """
- super(IScheduleScheduler, self).loadFromRequestHeaders()
-
- if self.request.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
- self.request.doing_attendee_refresh = 1
-
- def preProcessCalendarData(self):
- """
- For data coming in from outside we need to normalize the calendar user addresses so that later iTIP
- processing will match calendar users against those in stored calendar data. Only do that for invites
- not freebusy.
- """
- if not self.checkForFreeBusy():
- self.calendar.normalizeCalendarUserAddresses(normalizationLookup,
- self.resource.principalForCalendarUserAddress)
-
- def loadRecipientsFromRequestHeaders(self):
- """
- Need to normalize the calendar data and recipient values to keep those in sync,
- as we might later try to match them
- """
- super(IScheduleScheduler, self).loadRecipientsFromRequestHeaders()
- self.recipients = [normalizeCUAddress(recipient, normalizationLookup, self.resource.principalForCalendarUserAddress) for recipient in self.recipients]
-
-
- def checkAuthorization(self):
- # Must have an unauthenticated user
- if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
- log.err("Authenticated originators not allowed: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "originator-allowed"),
- "Authentication not allowed",
- ))
-
- @inlineCallbacks
- def checkOriginator(self):
- """
- Check the validity of the Originator header.
- """
-
- # For remote requests we do not allow the originator to be a local user or one within our domain.
- originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
- if originatorPrincipal or localUser:
- if originatorPrincipal.locallyHosted():
- log.err("Cannot use originator that is on this server: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "originator-allowed"),
- "Originator cannot be local to server",
- ))
- else:
- self.originator = calendarUserFromPrincipal(self.originator, originatorPrincipal)
- self._validAlternateServer(originatorPrincipal)
- else:
- self.originator = RemoteCalendarUser(self.originator)
- self._validiScheduleServer()
-
- def _validiScheduleServer(self):
- """
- Check the validity of the iSchedule host.
- """
-
- # We will only accept originator in known domains.
- servermgr = IScheduleServers()
- server = servermgr.mapDomain(self.originator.domain)
- if not server or not server.allow_from:
- log.err("Originator not on recognized server: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "originator-allowed"),
- "Originator not recognized by server",
- ))
- else:
- # Get the request IP and map to hostname.
- clientip = self.request.remoteAddr.host
-
- # First compare as dotted IP
- matched = False
- compare_with = (server.host,) + tuple(server.client_hosts)
- if clientip in compare_with:
- matched = True
- else:
- # Now do hostname lookup
- try:
- host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
- for host in itertools.chain((host,), aliases):
- # Try simple match first
- if host in compare_with:
- matched = True
- break
-
- # Try pattern match next
- for pattern in compare_with:
- try:
- if re.match(pattern, host) is not None:
- matched = True
- break
- except re.error:
- log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
- else:
- continue
- break
- except socket.herror, e:
- log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-
- if not matched:
- log.err("Originator not on allowed server: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "originator-allowed"),
- "Originator not allowed to send to this server",
- ))
-
- def _validAlternateServer(self, principal):
- """
- Check the validity of the partitioned host.
- """
-
- # Extract expected host/port. This will be the partitionURI, or if no partitions,
- # the serverURI
- expected_uri = principal.partitionURI()
- if expected_uri is None:
- expected_uri = principal.serverURI()
- expected_uri = urlparse.urlparse(expected_uri)
-
- # Get the request IP and map to hostname.
- clientip = self.request.remoteAddr.host
-
- # Check against this server (or any of its partitions). We need this because an external iTIP message
- # may be addressed to users on different partitions, and the node receiving the iTIP message will need to
- # forward it to the partition nodes, thus the client ip seen by the partitions will in fact be the initial
- # receiving node.
- matched = False
- if Servers.getThisServer().checkThisIP(clientip):
- matched = True
-
- # Checked allowed IPs - if any were defined we only check against them, we do not
- # go on to check the expected server host ip
- elif Servers.getThisServer().hasAllowedFromIP():
- matched = Servers.getThisServer().checkAllowedFromIP(clientip)
- if not matched:
- log.error("Invalid iSchedule connection from client: %s" % (clientip,))
-
- # Next compare as dotted IP
- elif isIPAddress(expected_uri.hostname):
- if clientip == expected_uri.hostname:
- matched = True
- else:
- # Now do expected hostname -> IP lookup
- try:
- # So now try the lookup of the expected host
- _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(expected_uri.hostname)
- for ip in ips:
- if ip == clientip:
- matched = True
- break
- except socket.herror, e:
- log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-
- # Check possible shared secret
- if matched and not Servers.getThisServer().checkSharedSecret(self.request):
- log.err("Invalid iSchedule shared secret")
- matched = False
-
- if not matched:
- log.err("Originator not on allowed server: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "originator-allowed"),
- "Originator not allowed to send to this server",
- ))
-
- @inlineCallbacks
- def checkOrganizerAsOriginator(self):
- """
- Check the validity of the ORGANIZER value. ORGANIZER must not be local.
- """
-
- # Verify that the ORGANIZER's cu address does not map to a valid user
- organizer = self.calendar.getOrganizer()
- if organizer:
- organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
- if organizerPrincipal:
- if organizerPrincipal.locallyHosted():
- log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "organizer-allowed"),
- "Organizer is not local to server",
- ))
- else:
- # Check that the origin server is the correct partition
- self.organizer = calendarUserFromPrincipal(organizer, organizerPrincipal)
- self._validAlternateServer(self.organizer.principal)
- else:
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
- if localUser:
- log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "organizer-allowed"),
- "Organizer not allowed to be originator",
- ))
- else:
- self.organizer = RemoteCalendarUser(organizer)
- else:
- log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "organizer-allowed"),
- "No organizer in calendar data",
- ))
-
- @inlineCallbacks
- def checkAttendeeAsOriginator(self):
- """
- Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
- Only local attendees are allowed for message originating from this server.
- """
-
- # Attendee cannot be local.
- attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
- if attendeePrincipal:
- if attendeePrincipal.locallyHosted():
- log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "attendee-allowed"),
- "Local attendee cannot send to this server",
- ))
- else:
- self._validAlternateServer(attendeePrincipal)
- else:
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
- if localUser:
- log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "attendee-allowed"),
- "Attendee not allowed to schedule",
- ))
-
- # TODO: in this case we should check that the ORGANIZER is the sole recipient.
-
- @inlineCallbacks
- def securityChecks(self):
- """
- Check that the originator has the appropriate rights to send this type of iTIP message.
- """
-
- # Prevent spoofing of ORGANIZER with specific METHODs when local
- if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
- yield self.checkOrganizerAsOriginator()
-
- # Prevent spoofing when doing reply-like METHODs
- elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
- yield self.checkAttendeeAsOriginator()
-
- else:
- log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
- "Unknown iTIP method",
- ))
-
-
class DirectScheduler(Scheduler):
""" An implicit scheduler meant for use by local processes which don't
need to go through all these checks. """
+ errorResponse = ErrorResponse
+
def checkAuthorization(self):
pass
+
def checkOrganizer(self):
pass
+
def checkOrganizerAsOriginator(self):
pass
+
def checkAttendeeAsOriginator(self):
pass
+
def securityChecks(self):
pass
+
def checkOriginator(self):
pass
+
def checkRecipients(self):
pass
-class IMIPScheduler(RemoteScheduler):
- def checkAuthorization(self):
- pass
-
- @inlineCallbacks
- def checkOriginator(self):
- """
- Check the validity of the Originator header.
- """
-
- # For remote requests we do not allow the originator to be a local user or one within our domain.
- originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
- if originatorPrincipal or localUser:
- log.err("Cannot use originator that is on this server: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "originator-allowed"),
- "Originator cannot be local to server",
- ))
- else:
- self.originator = RemoteCalendarUser(self.originator)
-
- def checkOrganizerAsOriginator(self):
- pass
-
- def checkAttendeeAsOriginator(self):
- pass
-
- def securityChecks(self):
- """
- Check that the connection is from the mail gateway
- """
- allowed = config.Scheduling['iMIP']['MailGatewayServer']
- # Get the request IP and map to hostname.
- clientip = self.request.remoteAddr.host
- host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
- for host in itertools.chain((host, clientip), aliases):
- if host == allowed:
- break
- else:
- log.err("Only %s is allowed to submit internal scheduling requests, not %s" % (allowed, host))
- # TODO: verify this is the right response:
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "originator-allowed"),
- "Originator server not allowed to send to this server",
- ))
-
-
class ScheduleResponseResponse (Response):
"""
ScheduleResponse L{Response} object.
Renders itself as a CalDAV:schedule-response XML document.
"""
- def __init__(self, xml_responses, location=None):
+ def __init__(self, schedule_response_element, xml_responses, location=None):
"""
@param xml_responses: an iterable of davxml.Response objects.
@param location: the value of the location header to return in the response,
@@ -1257,29 +761,42 @@
"""
Response.__init__(self, code=responsecode.OK,
- stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+ stream=schedule_response_element(*xml_responses).toxml())
self.headers.setHeader("content-type", MimeType("text", "xml"))
-
+
if location is not None:
self.headers.setHeader("location", location)
+
+
class ScheduleResponseQueue (LoggingMixIn):
"""
Stores a list of (typically error) responses for use in a
L{ScheduleResponse}.
"""
+
+ schedule_response_element = caldavxml.ScheduleResponse
+ response_element = caldavxml.Response
+ recipient_element = caldavxml.Recipient
+ recipient_uses_href = True
+ request_status_element = caldavxml.RequestStatus
+ error_element = davxml.Error
+ response_description_element = davxml.ResponseDescription
+ calendar_data_element = caldavxml.CalendarData
+
def __init__(self, method, success_response):
"""
@param method: the name of the method generating the queue.
@param success_response: the response to return in lieu of a
L{ScheduleResponse} if no responses are added to this queue.
"""
- self.responses = []
- self.method = method
- self.success_response = success_response
- self.location = None
+ self.responses = []
+ self.method = method
+ self.success_response = success_response
+ self.location = None
+
def setLocation(self, location):
"""
@param location: the value of the location header to return in the response,
@@ -1287,6 +804,7 @@
"""
self.location = location
+
def add(self, recipient, what, reqstatus=None, calendar=None, suppressErrorLog=False):
"""
Add a response.
@@ -1299,12 +817,12 @@
supported.
"""
if type(what) is int:
- code = what
- error = None
+ code = what
+ error = None
message = responsecode.RESPONSES[code]
elif isinstance(what, Failure):
- code = statusForFailure(what)
- error = errorForFailure(what)
+ code = statusForFailure(what)
+ error = self.errorForFailure(what)
message = messageForFailure(what)
else:
raise AssertionError("Unknown data type: %r" % (what,))
@@ -1313,41 +831,42 @@
self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
children = []
- children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
- children.append(caldavxml.RequestStatus(reqstatus))
+ children.append(self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient))
+ children.append(self.request_status_element(reqstatus))
if calendar is not None:
- children.append(caldavxml.CalendarData.fromCalendar(calendar))
+ children.append(self.calendar_data_element.fromCalendar(calendar))
if error is not None:
children.append(error)
if message is not None:
- children.append(davxml.ResponseDescription(message))
- self.responses.append(caldavxml.Response(*children))
+ children.append(self.response_description_element(message))
+ self.responses.append(self.response_element(*children))
- def clone(self, clone):
+
+ def errorForFailure(self, failure):
+ if failure.check(HTTPError) and isinstance(failure.value.response, ErrorResponse):
+ return self.error_element(failure.value.response.error)
+ else:
+ return None
+
+
+ def clone(self, recipient, request_status, calendar_data, error, desc):
"""
- Add a response cloned from an existing caldavxml.Response element.
+ Add a response cloned from existing data.
@param clone: the response to clone.
"""
- if not isinstance(clone, caldavxml.Response):
- raise AssertionError("Incorrect element type: %r" % (clone,))
- recipient = clone.childOfType(caldavxml.Recipient)
- request_status = clone.childOfType(caldavxml.RequestStatus)
- calendar_data = clone.childOfType(caldavxml.CalendarData)
- error = clone.childOfType(davxml.Error)
- desc = clone.childOfType(davxml.ResponseDescription)
-
children = []
- children.append(recipient)
- children.append(request_status)
+ children.append(self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient))
+ children.append(self.request_status_element.fromString(request_status))
if calendar_data is not None:
- children.append(calendar_data)
+ children.append(self.calendar_data_element.fromCalendar(calendar_data))
if error is not None:
- children.append(error)
+ children.append(self.error_element(*error))
if desc is not None:
- children.append(desc)
- self.responses.append(caldavxml.Response(*children))
+ children.append(self.response_description_element.fromString(desc))
+ self.responses.append(self.response_element(*children))
+
def response(self):
"""
Generate a L{ScheduleResponseResponse} with the responses contained in the
@@ -1356,6 +875,6 @@
@return: the response.
"""
if self.responses:
- return ScheduleResponseResponse(self.responses, self.location)
+ return ScheduleResponseResponse(self.schedule_response_element, self.responses, self.location)
else:
return self.success_response
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/test/test_caldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_caldav.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_caldav.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,33 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import twistedcaldav.test.util
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
-from twistedcaldav.config import config
-
-class CalDAV (twistedcaldav.test.util.TestCase):
- """
- twistedcaldav.scheduling.caldav tests
- """
-
- def test_matchCalendarUserAddress(self):
- """
- Make sure we do an exact comparison on EmailDomain
- """
- config.Scheduling[ScheduleViaCalDAV.serviceType()]["EmailDomain"] = "example.com"
- self.assertTrue(ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at example.com"))
- self.assertFalse(ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at foo.example.com"))
- self.assertFalse(ScheduleViaCalDAV.matchCalendarUserAddress("mailto:user at xyzexample.com"))
Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -27,7 +27,7 @@
"""
def test_organizer_diff(self):
-
+
data = (
(
"#1.1 Simple component, no change",
@@ -477,7 +477,6 @@
def test_attendee_merge_simple(self):
-
data = (
(
"#1.1 Simple component, no change",
@@ -1185,8 +1184,9 @@
)
self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
+
def test_attendee_merge_complex(self):
-
+
data = (
(
"#1.1 Complex component, no change",
@@ -1684,7 +1684,7 @@
def test_attendee_merge_exdate(self):
-
+
data = (
(
"#1.1 Single component, one EXDATE",
@@ -2027,8 +2027,9 @@
)
self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
+
def test_attendee_merge_cancelled(self):
-
+
data = (
(
"#1.1 Remove EXDATE add CANCELLED",
@@ -2391,8 +2392,9 @@
)
self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
+
def test_attendee_merge_dropbox(self):
-
+
data = (
(
"#1.1 Remove dropbox",
@@ -2670,9 +2672,9 @@
)
self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
+
def test_attendee_merge_fake_master(self):
-
data = (
(
"#1.1 Single overridden component, partstat change - ok",
@@ -2857,7 +2859,7 @@
END:VCALENDAR
""",
"mailto:user2 at example.com",
- (True, True, ('20080601T120000Z','20080602T120000Z',), """BEGIN:VCALENDAR
+ (True, True, ('20080601T120000Z', '20080602T120000Z',), """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
@@ -2929,7 +2931,7 @@
END:VCALENDAR
""",
"mailto:user2 at example.com",
- (True, True, ('20080601T120000Z','20080602T120000Z',), """BEGIN:VCALENDAR
+ (True, True, ('20080601T120000Z', '20080602T120000Z',), """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
@@ -3220,8 +3222,9 @@
)
self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
+
def test_what_is_different(self):
-
+
data1 = (
(
"#1.1 Simple component, no change",
@@ -3279,7 +3282,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"SUMMARY":set(),}},
+ {"": {"SUMMARY": set(), }},
),
(
"#1.3 Simple component, one property change, one addition, one removal",
@@ -3307,7 +3310,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"SUMMARY":set(), "LOCATION":set(), "DESCRIPTION":set(),}},
+ {"": {"SUMMARY": set(), "LOCATION": set(), "DESCRIPTION": set(), }},
),
(
"#1.4 Simple component, add attendee",
@@ -3340,7 +3343,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"ATTENDEE":set(),}},
+ {"": {"ATTENDEE": set(), }},
),
(
"#1.5 Simple component, remove attendee",
@@ -3371,7 +3374,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"ATTENDEE":set(),}},
+ {"": {"ATTENDEE": set(), }},
),
(
"#1.6 Simple component, attendee PARTSTAT only",
@@ -3403,7 +3406,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"ATTENDEE":set(),}},
+ {"": {"ATTENDEE": set(), }},
),
(
"#1.7 Simple component, attendee PARTSTAT and addition",
@@ -3436,7 +3439,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"ATTENDEE":set(),}},
+ {"": {"ATTENDEE": set(), }},
),
(
"#1.8 Simple component, attendee RSVP only",
@@ -3468,7 +3471,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"ATTENDEE":set(),}},
+ {"": {"ATTENDEE": set(), }},
),
(
"#1.9 Simple component, DTSTART/DTEND VALUE",
@@ -3500,7 +3503,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"DTEND":set(("VALUE",)), "DTSTART":set(("VALUE",)),}},
+ {"": {"DTEND": set(("VALUE",)), "DTSTART": set(("VALUE",)), }},
),
(
"#1.10 Simple component, DTSTART/DTEND TZID",
@@ -3568,10 +3571,10 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"DTEND":set(("TZID",)), "DTSTART":set(("TZID",)),}},
+ {"": {"DTEND": set(("TZID",)), "DTSTART": set(("TZID",)), }},
),
)
-
+
data2 = (
(
"#2.1 Simple recurring component, no change",
@@ -3633,7 +3636,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"SUMMARY":set(),}},
+ {"": {"SUMMARY": set(), }},
),
(
"#2.3 Simple component, one property change, one addition, one removal",
@@ -3663,7 +3666,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"SUMMARY":set(), "LOCATION":set(), "DESCRIPTION":set(),}},
+ {"": {"SUMMARY": set(), "LOCATION": set(), "DESCRIPTION": set(), }},
),
(
"#2.4 Simple component, add attendee",
@@ -3698,7 +3701,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"ATTENDEE":set(),}},
+ {"": {"ATTENDEE": set(), }},
),
(
"#2.5 Simple component, remove attendee",
@@ -3731,7 +3734,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"ATTENDEE":set(),}},
+ {"": {"ATTENDEE": set(), }},
),
(
"#2.6 Simple component, attendee PARTSTAT only",
@@ -3765,7 +3768,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"ATTENDEE":set(),}},
+ {"": {"ATTENDEE": set(), }},
),
(
"#2.7 Simple component, attendee PARTSTAT and addition",
@@ -3800,7 +3803,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"ATTENDEE":set(),}},
+ {"": {"ATTENDEE": set(), }},
),
(
"#2.8 Simple recurring component, property order change",
@@ -3841,7 +3844,7 @@
{},
),
)
-
+
data3 = (
(
"#3.1 Complex recurring component, no change",
@@ -3943,7 +3946,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"20080602T120000Z":{"SUMMARY":set(),}},
+ {"20080602T120000Z": {"SUMMARY": set(), }},
),
(
"#3.3 Simple component, one property change in master",
@@ -3991,7 +3994,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"SUMMARY":set(),}},
+ {"": {"SUMMARY": set(), }},
),
(
"#3.4 Simple component, one property change in master and instance",
@@ -4039,7 +4042,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"SUMMARY":set(),}, "20080602T120000Z":{"SUMMARY":set(),}},
+ {"": {"SUMMARY": set(), }, "20080602T120000Z": {"SUMMARY": set(), }},
),
(
"#3.5 Simple component, different property change in master and instance",
@@ -4088,7 +4091,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"":{"SUMMARY":set()}, "20080602T120000Z":{"Description":set()}},
+ {"": {"SUMMARY": set()}, "20080602T120000Z": {"Description": set()}},
),
(
"#3.6 Simple component, instance added no change",
@@ -4158,7 +4161,7 @@
END:VEVENT
END:VCALENDAR
""",
- {"20080602T120000Z":{"DTSTART":set(), "DTEND":set(), }},
+ {"20080602T120000Z": {"DTSTART": set(), "DTEND": set(), }},
),
(
"#3.8 Simple component, instance removed no change",
@@ -4228,17 +4231,18 @@
END:VEVENT
END:VCALENDAR
""",
- {"20080602T120000Z":{"DTSTART":set(), "DTEND":set(), }},
+ {"20080602T120000Z": {"DTSTART": set(), "DTEND": set(), }},
),
)
-
+
for description, calendar1, calendar2, rids in itertools.chain(data1, data2, data3,):
differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
got_rids = differ.whatIsDifferent()
self.assertEqual(got_rids, rids, msg="%s expected R-IDs: '%s', got: '%s'" % (description, rids, got_rids,))
-
+
+
def test_organizer_smart_merge(self):
-
+
data1 = (
(
"#1.1 Simple component, no change",
@@ -4481,7 +4485,7 @@
""",
),
)
-
+
data2 = (
(
"#2.1 Simple recurring component, no change",
@@ -5036,7 +5040,7 @@
""",
),
)
-
+
for description, calendar1, calendar2, changed_calendar in itertools.chain(data1, data2,):
cal1 = Component.fromString(calendar1)
cal2 = Component.fromString(calendar2)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/test/test_imip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_imip.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_imip.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,70 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks
-from twext.web2 import responsecode
-from twistedcaldav.ical import Component
-from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
-from twistedcaldav.scheduling.imip import ScheduleViaIMip
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.scheduling.scheduler import ScheduleResponseQueue
-import twistedcaldav.test.util
-from twistedcaldav.config import config
-
-class iMIPProcessing (twistedcaldav.test.util.TestCase):
- """
- iCalendar support tests
- """
-
- class FakeSchedule(object):
-
- def __init__(self, calendar):
- self.calendar = calendar
-
- @inlineCallbacks
- def test_no_freebusy(self):
-
- data = """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REQUEST
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VFREEBUSY
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VFREEBUSY
-END:VCALENDAR
-"""
-
- scheduler = iMIPProcessing.FakeSchedule(Component.fromString(data))
- recipients = (RemoteCalendarUser("mailto:user1 at example.com"),)
- responses = ScheduleResponseQueue("REQUEST", responsecode.OK)
-
- delivery = ScheduleViaIMip(scheduler, recipients, responses, True)
- yield delivery.generateSchedulingResponses()
-
- self.assertEqual(len(responses.responses), 1)
- self.assertEqual(str(responses.responses[0].children[1]), iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
-
- def test_matchCalendarUserAddress(self):
- # iMIP not sensitive to case:
- self.patch(config.Scheduling[ScheduleViaIMip.serviceType()], "AddressPatterns", ["mailto:.*"])
- self.assertTrue(ScheduleViaIMip.matchCalendarUserAddress("mailto:user at xyzexample.com"))
- self.assertTrue(ScheduleViaIMip.matchCalendarUserAddress("MAILTO:user at xyzexample.com"))
Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -30,29 +30,35 @@
"""
A fake CalDAVScheduler that does nothing except track who messages were sent to.
"""
-
+
def __init__(self, recipients):
self.recipients = recipients
+
def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
self.recipients.extend(recipients)
return succeed(ScheduleResponseQueue("FAKE", responsecode.OK))
+
+
class FakePrincipal(object):
-
+
def __init__(self, cuaddr):
self.cuaddr = cuaddr
-
+
+
def calendarUserAddresses(self):
return (self.cuaddr,)
+
+
class Implicit (twistedcaldav.test.util.TestCase):
"""
iCalendar support tests
"""
def test_removed_attendees(self):
-
+
data = (
(
"#1.1 Simple component, no change",
@@ -785,7 +791,7 @@
self.assertEqual(scheduler.cancelledAttendees, set(result), msg=description)
- @inlineCallbacks
+ @inlineCallbacks
def test_process_request_excludes_includes(self):
"""
Test that processRequests correctly excludes or includes the specified attendees.
@@ -826,25 +832,27 @@
scheduler.only_refresh_attendees = includes
scheduler.changed_rids = None
scheduler.reinvites = None
-
+
# Get some useful information from the calendar
yield scheduler.extractCalendarData()
scheduler.organizerPrincipal = FakePrincipal(scheduler.organizer)
-
+
recipients = []
-
+
def makeFakeScheduler():
return FakeScheduler(recipients)
scheduler.makeScheduler = makeFakeScheduler
-
+
count = (yield scheduler.processRequests())
self.assertEqual(count, result_count)
self.assertEqual(len(recipients), result_count)
self.assertEqual(set(recipients), set(result_set))
+
+
class ImplicitRequests (HomeTestCase):
"""
- Test twistedcaldav.scheduyling.implicit with a Request object.
+ Test twistedcaldav.scheduyling.implicit with a Request object.
"""
@inlineCallbacks
@@ -852,7 +860,7 @@
"""
Test that checkImplicitState() always returns True for any organizer, valid or not.
"""
-
+
data = (
(
"""BEGIN:VCALENDAR
@@ -907,25 +915,26 @@
request = SimpleRequest(self.site, "PUT", "/calendar/1.ics")
calresource = yield request.locateResource("/calendar/1.ics")
self.assertEqual(calresource.isScheduleObject, None)
-
+
scheduler = ImplicitScheduler()
doAction, isScheduleObject = (yield scheduler.testImplicitSchedulingPUT(request, calresource, "/calendar/1.ics", calendar, False))
self.assertEqual(doAction, result)
self.assertEqual(isScheduleObject, result)
request._newStoreTransaction.abort()
+
@inlineCallbacks
def test_testImplicitSchedulingPUT_FixScheduleState(self):
"""
Test that testImplicitSchedulingPUT will fix an old cached schedule object state by
re-evaluating the calendar data.
"""
-
+
request = SimpleRequest(self.site, "PUT", "/calendar/1.ics")
calresource = yield request.locateResource("/calendar/1.ics")
self.assertEqual(calresource.isScheduleObject, None)
calresource.isScheduleObject = False
-
+
calendarOld = Component.fromString("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -940,7 +949,6 @@
END:VCALENDAR
""")
-
calendarNew = Component.fromString("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -955,9 +963,9 @@
END:VCALENDAR
""")
- calresource.exists = lambda :True
- calresource.iCalendarForUser = lambda request:succeed(calendarOld)
-
+ calresource.exists = lambda : True
+ calresource.iCalendarForUser = lambda request: succeed(calendarOld)
+
scheduler = ImplicitScheduler()
try:
doAction, isScheduleObject = (yield scheduler.testImplicitSchedulingPUT(request, calresource, "/calendars/users/user01/calendar/1.ics", calendarNew, False))
@@ -966,18 +974,19 @@
self.assertTrue(doAction)
self.assertTrue(isScheduleObject)
+
@inlineCallbacks
def test_testImplicitSchedulingPUT_NoChangeScheduleState(self):
"""
Test that testImplicitSchedulingPUT will prevent attendees from changing the
schedule object state.
"""
-
+
request = SimpleRequest(self.site, "PUT", "/calendar/1.ics")
calresource = yield request.locateResource("/calendar/1.ics")
self.assertEqual(calresource.isScheduleObject, None)
calresource.isScheduleObject = False
-
+
calendarOld = Component.fromString("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -989,7 +998,6 @@
END:VCALENDAR
""")
-
calendarNew = Component.fromString("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -1004,9 +1012,9 @@
END:VCALENDAR
""")
- calresource.exists = lambda :True
- calresource.iCalendarForUser = lambda request:succeed(calendarOld)
-
+ calresource.exists = lambda : True
+ calresource.iCalendarForUser = lambda request: succeed(calendarOld)
+
scheduler = ImplicitScheduler()
try:
yield scheduler.testImplicitSchedulingPUT(request, calresource, "/calendars/users/user01/calendar/1.ics", calendarNew, False)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/test/test_ischeduleservers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_ischeduleservers.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_ischeduleservers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,49 +0,0 @@
-##
-# Copyright (c) 2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.python.filepath import FilePath
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServersParser
-import twistedcaldav.test.util
-
-class Test_IScheduleServersParser(twistedcaldav.test.util.TestCase):
- """
- Test L{IScheduleServersParser} implementation.
- """
-
- def test_readXML(self):
-
- fp = FilePath(self.mktemp())
- fp.open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE servers SYSTEM "servertoserver.dtd">
-<servers>
- <server>
- <uri>https://localhost:8543/inbox</uri>
- <allow-requests-from/>
- <allow-requests-to/>
- <domains>
- <domain>example.org</domain>
- </domains>
- <hosts>
- <host>127.0.0.1</host>
- </hosts>
- </server>
-</servers>
-"""
-)
-
- parser = IScheduleServersParser(fp)
- self.assertEqual(len(parser.servers), 1)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -27,7 +27,7 @@
"""
def test_update_attendee_partstat(self):
-
+
data = (
(
"#1.1 Simple component, accepted",
@@ -888,7 +888,7 @@
msg=description
)
self.assertEqual(
- tuple(sorted(list(reply_rids), key=lambda x:x[0])),
+ tuple(sorted(list(reply_rids), key=lambda x: x[0])),
rids,
msg=description
)
@@ -899,11 +899,12 @@
msg=description
)
+
def test_sequenceComparison(self):
"""
Test iTIPProcessing.sequenceComparison
"""
-
+
data = (
(
"1.1 Simple Update - SEQUENCE change",
@@ -1385,15 +1386,16 @@
True,
),
)
-
+
for title, calendar_txt, itip_txt, expected in data:
calendar = Component.fromString(calendar_txt)
itip = Component.fromString(itip_txt)
result = iTipProcessing.sequenceComparison(itip, calendar)
self.assertEqual(result, expected, msg="Result mismatch: %s" % (title,))
-
+
+
class iTIPGenerator (twistedcaldav.test.util.TestCase):
"""
iCalendar support tests
@@ -1401,7 +1403,7 @@
data_dir = os.path.join(os.path.dirname(__file__), "data")
def test_request(self):
-
+
data = (
# Simple component, no Attendees - no filtering
(
@@ -1571,7 +1573,7 @@
END:VCALENDAR
""",
("mailto:user3 at example.com",)
- ),
+ ),
# Recurring component with one instance, master with one attendee, instance without attendee - filtering match
(
@@ -1704,7 +1706,7 @@
("mailto:user3 at example.com",)
),
)
-
+
for original, filtered, attendees in data:
component = Component.fromString(original)
itipped = iTipGenerator.generateAttendeeRequest(component, attendees, None)
@@ -1712,8 +1714,9 @@
itipped = "".join([line for line in itipped.splitlines(True) if not line.startswith("DTSTAMP:")])
self.assertEqual(filtered, itipped)
+
def test_cancel(self):
-
+
data = (
# Simple component, with two attendees - cancel one
(
@@ -1810,7 +1813,7 @@
END:VCALENDAR
""",
("mailto:user2 at example.com",),
- (PyCalendarDateTime(2008, 11, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)), ),
+ (PyCalendarDateTime(2008, 11, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)),),
),
# Recurring component with one instance, each with one attendee - cancel instance
@@ -1851,7 +1854,7 @@
END:VCALENDAR
""",
("mailto:user2 at example.com",),
- (PyCalendarDateTime(2008, 11, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)), ),
+ (PyCalendarDateTime(2008, 11, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)),),
),
# Recurring component with one instance, each with one attendee - cancel master
@@ -1892,7 +1895,7 @@
END:VCALENDAR
""",
("mailto:user2 at example.com",),
- (None, ),
+ (None,),
),
# Recurring component - cancel non-existent instance
@@ -1912,11 +1915,11 @@
""",
"",
("mailto:user2 at example.com",),
- (PyCalendarDateTime(2008, 12, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)), ),
+ (PyCalendarDateTime(2008, 12, 14, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)),),
),
)
-
+
for original, filtered, attendees, instances in data:
component = Component.fromString(original)
itipped = iTipGenerator.generateCancel(component, attendees, instances)
@@ -1924,6 +1927,7 @@
itipped = "".join([line for line in itipped.splitlines(True) if not line.startswith("DTSTAMP:")])
self.assertEqual(filtered, itipped)
+
def test_missingAttendee(self):
"""
When generating a reply, remove all components that are missing
Modified: CalendarServer/trunk/twistedcaldav/scheduling/utils.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/utils.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/scheduling/utils.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -43,7 +43,7 @@
# Get matching newstore objects
objectResources = (yield calendar_home.getCalendarResourcesForUID(uid, allow_shared))
-
+
# We really want only one or zero of these
if len(objectResources) == 1:
result["calendar_collection_uri"] = joinURL(calendar_home.url(), objectResources[0]._parentCollection.name())
@@ -54,4 +54,3 @@
log.debug("Should only have zero or one scheduling object resource with UID '%s' in calendar home: %s" % (uid, calendar_home,))
returnValue((result["resource"], result["resource_name"], result["calendar_collection"], result["calendar_collection_uri"],))
-
Deleted: CalendarServer/trunk/twistedcaldav/servers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/servers.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/servers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,295 +0,0 @@
-##
-# Copyright (c) 2011-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.python.log import Logger
-from twisted.internet.abstract import isIPAddress
-from twistedcaldav.client.pool import installPool
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.xmlutil import readXML
-import socket
-import urlparse
-
-"""
-XML based server configuration file handling.
-
-This is used in an environment where more than one server is being used within a single domain. i.e., all
-the principals across the whole domain need to be able to directly schedule each other and know of each others
-existence. A common scenario would be a production server and a development/test server.
-
-Each server is identified by an id and url. The id is used when assigning principals to a specific server. Each
-server can also support multiple partitions, and each of those is identified by an id and url, with the id also
-being used to assign principals to a specific partition.
-"""
-
-__all__ = [
- "Servers",
-]
-
-log = Logger()
-
-SERVER_SECRET_HEADER = "X-CALENDARSERVER-ISCHEDULE"
-
-class ServersDB(object):
- """
- Represents the set of servers within the same domain.
- """
-
- def __init__(self):
-
- self._servers = {}
- self._xmlFile = None
- self._thisServer = None
-
- def load(self, xmlFile=None, ignoreIPLookupFailures=False):
- if self._xmlFile is None or xmlFile is not None:
- self._servers = {}
- if xmlFile:
- self._xmlFile = xmlFile
- else:
- self._xmlFile = fullServerPath(
- config.ConfigRoot,
- config.Servers.ConfigFile
- )
- self._servers = ServersParser.parse(self._xmlFile, ignoreIPLookupFailures=ignoreIPLookupFailures)
- for server in self._servers.values():
- if server.thisServer:
- self._thisServer = server
- break
- else:
- raise ValueError("No server in %s matches this server." % (self._xmlFile,))
-
- def clear(self):
- self._servers = {}
- self._xmlFile = None
- self._thisServer = None
-
- def getServerById(self, id):
- return self._servers.get(id)
-
- def getServerURIById(self, id):
- try:
- return self._servers[id].uri
- except KeyError:
- return None
-
- def getThisServer(self):
- return self._thisServer
-
-Servers = ServersDB() # Global server DB
-
-class Server(object):
- """
- Represents a server which may itself be partitioned.
- """
-
- def __init__(self):
- self.id = None
- self.uri = None
- self.thisServer = False
- self.ips = set()
- self.allowed_from_ips = set()
- self.shared_secret = None
- self.partitions = {}
- self.partitions_ips = set()
- self.isImplicit = True
-
- def check(self, ignoreIPLookupFailures=False):
- # Check whether this matches the current server
- parsed_uri = urlparse.urlparse(self.uri)
- if parsed_uri.hostname == config.ServerHostName:
- if parsed_uri.scheme == "http":
- if config.HTTPPort:
- self.thisServer = parsed_uri.port in (config.HTTPPort,) + tuple(config.BindHTTPPorts)
- elif parsed_uri.scheme == "https":
- if config.SSLPort:
- self.thisServer = parsed_uri.port in (config.SSLPort,) + tuple(config.BindSSLPorts)
-
- # Need to cache IP addresses
- try:
- _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(parsed_uri.hostname)
- except socket.gaierror, e:
- msg = "Unable to lookup ip-addr for server '%s': %s" % (parsed_uri.hostname, str(e))
- log.error(msg)
- if ignoreIPLookupFailures:
- ips = ()
- else:
- raise ValueError(msg)
- self.ips = set(ips)
-
- actual_ips = set()
- for item in self.allowed_from_ips:
- if not isIPAddress(item):
- try:
- _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(item)
- except socket.gaierror, e:
- msg = "Unable to lookup ip-addr for allowed-from '%s': %s" % (item, str(e))
- log.error(msg)
- if not ignoreIPLookupFailures:
- raise ValueError(msg)
- else:
- actual_ips.update(ips)
- else:
- actual_ips.add(item)
- self.allowed_from_ips = actual_ips
-
- for uri in self.partitions.values():
- parsed_uri = urlparse.urlparse(uri)
- try:
- _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(parsed_uri.hostname)
- except socket.gaierror, e:
- msg = "Unable to lookup ip-addr for partition '%s': %s" % (parsed_uri.hostname, str(e))
- log.error(msg)
- if ignoreIPLookupFailures:
- ips = ()
- else:
- raise ValueError(msg)
- self.partitions_ips.update(ips)
-
- def checkThisIP(self, ip):
- """
- Check that the passed in IP address corresponds to this server or one of its partitions.
- """
- return (ip in self.ips) or (ip in self.partitions_ips)
-
- def hasAllowedFromIP(self):
- return len(self.allowed_from_ips) > 0
-
- def checkAllowedFromIP(self, ip):
- return ip in self.allowed_from_ips
-
- def checkSharedSecret(self, request):
-
- # Get header from the request
- request_secret = request.headers.getRawHeaders(SERVER_SECRET_HEADER)
-
- if request_secret is not None and self.shared_secret is None:
- log.error("iSchedule request included unexpected %s header" % (SERVER_SECRET_HEADER,))
- return False
- elif request_secret is None and self.shared_secret is not None:
- log.error("iSchedule request did not include required %s header" % (SERVER_SECRET_HEADER,))
- return False
- elif (request_secret[0] if request_secret else None) != self.shared_secret:
- log.error("iSchedule request %s header did not match" % (SERVER_SECRET_HEADER,))
- return False
- else:
- return True
-
- def secretHeader(self):
- """
- Return a tuple of header name, header value
- """
- return (SERVER_SECRET_HEADER, self.shared_secret,)
-
- def addPartition(self, id, uri):
- self.partitions[id] = uri
-
- def getPartitionURIForId(self, id):
- return self.partitions.get(id)
-
- def isPartitioned(self):
- return len(self.partitions) != 0
-
- def installReverseProxies(self, ownUID, maxClients):
-
- for partition, url in self.partitions.iteritems():
- if partition != ownUID:
- installPool(
- partition,
- url,
- maxClients,
- )
-
-
-
-ELEMENT_SERVERS = "servers"
-ELEMENT_SERVER = "server"
-ELEMENT_ID = "id"
-ELEMENT_URI = "uri"
-ELEMENT_ALLOWED_FROM = "allowed-from"
-ELEMENT_SHARED_SECRET = "shared-secret"
-ELEMENT_PARTITIONS = "partitions"
-ELEMENT_PARTITION = "partition"
-ATTR_IMPLICIT = "implicit"
-ATTR_VALUE_YES = "yes"
-ATTR_VALUE_NO = "no"
-
-class ServersParser(object):
- """
- Servers configuration file parser.
- """
- @staticmethod
- def parse(xmlFile, ignoreIPLookupFailures=False):
-
- results = {}
-
- # Read in XML
- try:
- _ignore_tree, servers_node = readXML(xmlFile, ELEMENT_SERVERS)
- except ValueError, e:
- log.error("XML parse error for '%s' because: %s" % (xmlFile, e,), raiseException=RuntimeError)
-
- for child in servers_node.getchildren():
-
- if child.tag != ELEMENT_SERVER:
- log.error("Unknown server type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
- server = Server()
- server.isImplicit = child.get(ATTR_IMPLICIT, ATTR_VALUE_YES) == ATTR_VALUE_YES
-
- for node in child.getchildren():
- if node.tag == ELEMENT_ID:
- server.id = node.text
- elif node.tag == ELEMENT_URI:
- server.uri = node.text
- elif node.tag == ELEMENT_ALLOWED_FROM:
- server.allowed_from_ips.add(node.text)
- elif node.tag == ELEMENT_SHARED_SECRET:
- server.shared_secret = node.text
- elif node.tag == ELEMENT_PARTITIONS:
- ServersParser._parsePartition(xmlFile, node, server)
- else:
- log.error("Invalid element '%s' in servers file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
-
- if server.id is None or server.uri is None:
- log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
- server.check(ignoreIPLookupFailures=ignoreIPLookupFailures)
- results[server.id] = server
-
- return results
-
- @staticmethod
- def _parsePartition(xmlFile, partitions, server):
-
- for child in partitions.getchildren():
-
- if child.tag != ELEMENT_PARTITION:
- log.error("Unknown partition type: '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
- id = None
- uri = None
- for node in child.getchildren():
- if node.tag == ELEMENT_ID:
- id = node.text
- elif node.tag == ELEMENT_URI:
- uri = node.text
- else:
- log.error("Invalid element '%s' in augment file: '%s'" % (node.tag, xmlFile,), raiseException=RuntimeError)
-
- if id is None or uri is None:
- log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
-
- server.addPartition(id, uri)
Modified: CalendarServer/trunk/twistedcaldav/simpleresource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/simpleresource.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/simpleresource.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -23,17 +23,20 @@
"SimpleResource",
"SimpleCalDAVResource",
"SimpleRedirectResource",
+ "SimpleDataResource",
]
from twext.web2 import http
-from txdav.xml import element as davxml
from twext.web2.dav.noneprops import NonePropertyStore
+from twext.web2.http import Response
from twisted.internet.defer import succeed
+from twistedcaldav.config import config
from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.config import config
+from txdav.xml import element as davxml
+
class SimpleResource (
CalDAVResource,
):
@@ -63,22 +66,28 @@
self._isDir = isdir
self.defaultACL = defaultACL
+
def isCollection(self):
return self._isDir
+
def deadProperties(self):
if not hasattr(self, "_dead_properties"):
self._dead_properties = NonePropertyStore(self)
return self._dead_properties
+
def etag(self):
return succeed(None)
+
def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
return succeed(self.defaultACL)
SimpleCalDAVResource = SimpleResource
+
+
class SimpleRedirectResource(SimpleResource):
"""
A L{SimpleResource} which always performs a redirect.
@@ -94,5 +103,34 @@
SimpleResource.__init__(self, principalCollections=principalCollections, isdir=isdir, defaultACL=defaultACL)
self._kwargs = kwargs
+
def renderHTTP(self, request):
return http.RedirectResponse(request.unparseURL(host=config.ServerHostName, **self._kwargs))
+
+
+
+class SimpleDataResource(SimpleResource):
+ """
+ A L{SimpleResource} which returns fixed content.
+ """
+
+ def __init__(self, principalCollections, content_type, data, defaultACL=SimpleResource.authReadACL):
+ """
+ @param content_type: the mime content-type of the data
+ @type content_type: L{MimeType}
+ @param data: the data
+ @type data: C{str}
+ """
+ SimpleResource.__init__(self, principalCollections=principalCollections, isdir=False, defaultACL=defaultACL)
+ self.content_type = content_type
+ self.data = data
+
+
+ def contentType(self):
+ return self.content_type
+
+
+ def render(self, request):
+ response = Response(200, {}, self.data)
+ response.headers.setHeader("content-type", self.content_type)
+ return response
Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -178,16 +178,16 @@
DEFAULT_AUGMENT_PARAMS = {
"twistedcaldav.directory.augment.AugmentXMLDB": {
- "xmlFiles": ["augments.xml",],
+ "xmlFiles": ["augments.xml", ],
"statSeconds" : 15,
},
"twistedcaldav.directory.augment.AugmentSqliteDB": {
"dbpath": "augments.sqlite",
},
"twistedcaldav.directory.augment.AugmentPostgreSQLDB": {
- "host": "localhost",
+ "host": "localhost",
"database": "augments",
- "user": "",
+ "user": "",
"password": "",
},
}
@@ -197,9 +197,9 @@
"dbpath": "proxies.sqlite",
},
"twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB": {
- "host": "localhost",
+ "host": "localhost",
"database": "proxies",
- "user": "",
+ "user": "",
"password": "",
"dbtype": "",
},
@@ -215,10 +215,10 @@
"peopleNode": "/Search/Contacts",
"queryUserRecords": True,
"userNode": "/Search/Contacts",
- "maxDSQueryRecords":0,
+ "maxDSQueryRecords": 0,
"queryDSLocal": False,
"ignoreSystemRecords": True,
- "dsLocalCacheTimeout":30,
+ "dsLocalCacheTimeout": 30,
"liveQuery": True,
"fakeETag": True,
"cacheQuery": False,
@@ -243,12 +243,12 @@
# default. For example, it may be the address of a load balancer or
# proxy which forwards connections to the server.
#
- "ServerHostName": "", # Network host name.
- "HTTPPort": 0, # HTTP port (0 to disable HTTP)
- "SSLPort" : 0, # SSL port (0 to disable HTTPS)
- "EnableSSL" : False, # Whether to listen on SSL port(s)
+ "ServerHostName": "", # Network host name.
+ "HTTPPort": 0, # HTTP port (0 to disable HTTP)
+ "SSLPort" : 0, # SSL port (0 to disable HTTPS)
+ "EnableSSL" : False, # Whether to listen on SSL port(s)
"RedirectHTTPToHTTPS" : False, # If True, all nonSSL requests redirected to an SSL Port
- "SSLMethod" : "SSLv3_METHOD", # SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD
+ "SSLMethod" : "SSLv3_METHOD", # SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD
"SSLCiphers" : "ALL:!aNULL:!ADH:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM",
#
@@ -256,38 +256,38 @@
#
# This configures the actual network address that the server binds to.
#
- "BindAddresses": [], # List of IP addresses to bind to [empty = all]
- "BindHTTPPorts": [], # List of port numbers to bind to for HTTP
+ "BindAddresses": [], # List of IP addresses to bind to [empty = all]
+ "BindHTTPPorts": [], # List of port numbers to bind to for HTTP
# [empty = same as "Port"]
- "BindSSLPorts" : [], # List of port numbers to bind to for SSL
+ "BindSSLPorts" : [], # List of port numbers to bind to for SSL
# [empty = same as "SSLPort"]
- "InheritFDs" : [], # File descriptors to inherit for HTTP requests
+ "InheritFDs" : [], # File descriptors to inherit for HTTP requests
# (empty = don't inherit)
- "InheritSSLFDs": [], # File descriptors to inherit for HTTPS requests
+ "InheritSSLFDs": [], # File descriptors to inherit for HTTPS requests
# (empty = don't inherit)
- "MetaFD" : 0, # Inherited file descriptor to call recvmsg() on to
+ "MetaFD" : 0, # Inherited file descriptor to call recvmsg() on to
# receive sockets (none = don't inherit)
"UseMetaFD" : True, # Use a 'meta' FD, i.e. an FD to transmit other FDs
# to slave processes.
"UseDatabase" : True, # True: database; False: files
-
+
"TransactionTimeoutSeconds" : 0, # Timeout transactions that take longer than
# the specified number of seconds. Zero means
# no timeouts
- "DBType" : "", # 2 possible values: empty, meaning 'spawn postgres
+ "DBType" : "", # 2 possible values: empty, meaning 'spawn postgres
# yourself', or 'postgres', meaning 'connect to a
# postgres database as specified by the 'DSN'
# configuration key. Will support more values in
# the future.
- "DSN" : "", # Data Source Name. Used to connect to an external
+ "DSN" : "", # Data Source Name. Used to connect to an external
# database if DBType is non-empty. Format varies
# depending on database type.
- "DBAMPFD" : 0, # Internally used by database to tell slave
+ "DBAMPFD" : 0, # Internally used by database to tell slave
# processes to inherit a file descriptor and use it
# as an AMP connection over a UNIX socket; see
# twext.enterprise.adbapi2.ConnectionPoolConnection
@@ -299,8 +299,8 @@
#
# Types of service provided
#
- "EnableCalDAV" : True, # Enable CalDAV service
- "EnableCardDAV" : True, # Enable CardDAV service
+ "EnableCalDAV" : True, # Enable CalDAV service
+ "EnableCardDAV" : True, # Enable CardDAV service
#
# Data store
@@ -314,20 +314,20 @@
"LogRoot" : "/var/log/caldavd",
"RunRoot" : "/var/run/caldavd",
"WebCalendarRoot" : "/Applications/Server.app/Contents/ServerRoot/usr/share/collabd",
-
+
#
# Quotas
#
-
+
# Attachments
"UserQuota" : 104857600, # User attachment quota (in bytes)
-
+
# Resource data
- "MaxCollectionsPerHome" : 50, # Maximum number of calendars/address books allowed in a home
- "MaxResourcesPerCollection" : 10000, # Maximum number of resources in a calendar/address book
+ "MaxCollectionsPerHome" : 50, # Maximum number of calendars/address books allowed in a home
+ "MaxResourcesPerCollection" : 10000, # Maximum number of resources in a calendar/address book
"MaxResourceSize" : 1048576, # Maximum resource size (in bytes)
- "MaxAttendeesPerInstance" : 100, # Maximum number of unique attendees
- "MaxAllowedInstances" : 3000, # Maximum number of instances the server will index
+ "MaxAttendeesPerInstance" : 100, # Maximum number of unique attendees
+ "MaxAllowedInstances" : 3000, # Maximum number of instances the server will index
# Set to URL path of wiki authentication service, e.g. "/auth", in order
# to use javascript authentication dialog. Empty string indicates standard
@@ -376,22 +376,22 @@
"type": "twistedcaldav.directory.calendaruserproxy.ProxySqliteDB",
"params": DEFAULT_PROXYDB_PARAMS["twistedcaldav.directory.calendaruserproxy.ProxySqliteDB"],
},
- "ProxyLoadFromFile": "", # Allows for initialization of the proxy database from an XML file
+ "ProxyLoadFromFile": "", # Allows for initialization of the proxy database from an XML file
#
# Special principals
#
- "AdminPrincipals": [], # Principals with "DAV:all" access (relative URLs)
- "ReadPrincipals": [], # Principals with "DAV:read" access (relative URLs)
- "EnableProxyPrincipals": True, # Create "proxy access" principals
+ "AdminPrincipals": [], # Principals with "DAV:all" access (relative URLs)
+ "ReadPrincipals": [], # Principals with "DAV:read" access (relative URLs)
+ "EnableProxyPrincipals": True, # Create "proxy access" principals
#
# Permissions
#
- "EnableAnonymousReadRoot": True, # Allow unauthenticated read access to /
- "EnableAnonymousReadNav": False, # Allow unauthenticated read access to hierarchy
- "EnablePrincipalListings": True, # Allow listing of principal collections
- "EnableMonolithicCalendars": True, # Render calendar collections as a monolithic iCalendar object
+ "EnableAnonymousReadRoot": True, # Allow unauthenticated read access to /
+ "EnableAnonymousReadNav": False, # Allow unauthenticated read access to hierarchy
+ "EnablePrincipalListings": True, # Allow listing of principal collections
+ "EnableMonolithicCalendars": True, # Render calendar collections as a monolithic iCalendar object
#
# Client controls
@@ -402,7 +402,7 @@
# Authentication
#
"Authentication": {
- "Basic": { "Enabled": False }, # Clear text; best avoided
+ "Basic": { "Enabled": False }, # Clear text; best avoided
"Digest": { # Digest challenge/response
"Enabled": True,
"Algorithm": "md5",
@@ -427,11 +427,11 @@
#
# Logging
#
- "AccessLogFile" : "access.log", # Apache-style access log
- "ErrorLogFile" : "error.log", # Server activity log
- "ErrorLogEnabled" : True, # True = use log file, False = stdout
- "ErrorLogRotateMB" : 10, # Rotate error log after so many megabytes
- "ErrorLogMaxRotatedFiles" : 5, # Retain this many error log files
+ "AccessLogFile" : "access.log", # Apache-style access log
+ "ErrorLogFile" : "error.log", # Server activity log
+ "ErrorLogEnabled" : True, # True = use log file, False = stdout
+ "ErrorLogRotateMB" : 10, # Rotate error log after so many megabytes
+ "ErrorLogMaxRotatedFiles" : 5, # Retain this many error log files
"PIDFile" : "caldavd.pid",
"RotateAccessLog" : False,
"EnableExtendedAccessLog": True,
@@ -452,21 +452,21 @@
"EnableTCPStatsSocket" : False,
"TCPStatsPort" : 8100,
},
-
+
"LogDatabase" : {
"LabelsInSQL" : False,
"Statistics" : False,
"StatisticsLogFile" : "sqlstats.log",
"SQLStatements" : False,
- "TransactionWaitSeconds" : 0,
+ "TransactionWaitSeconds" : 0,
},
#
# SSL/TLS
#
- "SSLCertificate" : "", # Public key
- "SSLPrivateKey" : "", # Private key
- "SSLAuthorityChain" : "", # Certificate Authority Chain
+ "SSLCertificate" : "", # Public key
+ "SSLPrivateKey" : "", # Private key
+ "SSLAuthorityChain" : "", # Certificate Authority Chain
"SSLPassPhraseDialog": "/etc/apache2/getsslpassphrase",
"SSLCertAdmin" : "/Applications/Server.app/Contents/ServerRoot/usr/sbin/certadmin",
@@ -510,11 +510,11 @@
#
# Standard (or draft) WebDAV extensions
#
- "EnableAddMember" : True, # POST ;add-member extension
- "EnableSyncReport" : True, # REPORT collection-sync
- "EnableSyncReportHome" : True, # REPORT collection-sync on home collections
- "EnableWellKnown" : True, # /.well-known resource
- "EnableCalendarQueryExtended" : True, # Extended calendar-query REPORT
+ "EnableAddMember" : True, # POST ;add-member extension
+ "EnableSyncReport" : True, # REPORT collection-sync
+ "EnableSyncReportHome" : True, # REPORT collection-sync on home collections
+ "EnableWellKnown" : True, # /.well-known resource
+ "EnableCalendarQueryExtended" : True, # Extended calendar-query REPORT
#
# Non-standard CalDAV extensions
@@ -522,50 +522,50 @@
"EnableDropBox" : False, # Calendar Drop Box
"EnablePrivateEvents" : False, # Private Events
"EnableTimezoneService" : False, # Old Timezone service
-
+
"TimezoneService" : { # New standard timezone service
- "Enabled" : False, # Overall on/off switch
+ "Enabled" : False, # Overall on/off switch
"Mode" : "primary", # Can be "primary" or "secondary"
- "BasePath" : "", # Path to zoneinfo - if None use default package path
+ "BasePath" : "", # Path to zoneinfo - if None use default package path
# secondary service MUST define its own writable path
- "XMLInfoPath" : "", # Path to db cache info - if None use default package path
+ "XMLInfoPath" : "", # Path to db cache info - if None use default package path
# secondary service MUST define its own writable path if
# not None
-
+
"SecondaryService" : {
# Only one of these should be used when a secondary service is used
- "Host" : "", # Domain/IP of secondary service to discover
- "URI" : "", # HTTP(s) URI to secondary service
+ "Host" : "", # Domain/IP of secondary service to discover
+ "URI" : "", # HTTP(s) URI to secondary service
"UpdateIntervalMinutes" : 24 * 60,
}
},
-
+
"EnableTimezonesByReference" : False, # Strip out VTIMEZONES that are known
- "EnableBatchUpload" : True, # POST batch uploads
- "MaxResourcesBatchUpload" : 100, # Maximum number of resources in a batch POST
+ "EnableBatchUpload" : True, # POST batch uploads
+ "MaxResourcesBatchUpload" : 100, # Maximum number of resources in a batch POST
"MaxBytesBatchUpload" : 10485760, # Maximum size of a batch POST (10 MB)
-
+
"Sharing": {
"Enabled" : False, # Overall on/off switch
"AllowExternalUsers" : False, # External (non-principal) sharees allowed
"Calendars" : {
- "Enabled" : True, # Calendar on/off switch
+ "Enabled" : True, # Calendar on/off switch
},
"AddressBooks" : {
- "Enabled" : True, # Address Books on/off switch
- }
+ "Enabled" : True, # Address Books on/off switch
+ }
},
-
- "RestrictCalendarsToOneComponentType" : True, # Only allow calendars to be created with a single component type
+
+ "RestrictCalendarsToOneComponentType" : True, # Only allow calendars to be created with a single component type
# If this is on, it will also trigger an upgrade behavior that will
# split existing calendars into multiples based on component type.
# If on, it will also cause new accounts to provision with separate
# calendars for events and tasks.
- "ParallelUpgrades" : False, # Perform upgrades - currently only the
+ "ParallelUpgrades" : False, # Perform upgrades - currently only the
# database -> filesystem migration - but in
# the future, hopefully all relevant
# upgrades - in parallel in subprocesses.
@@ -575,23 +575,23 @@
# the database, merge the data from the filesystem
# into the database homes.
- "EnableDefaultAlarms" : True, # Support for default alarms generated by the server
- "RemoveDuplicateAlarms": True, # Remove duplicate alarms on PUT
+ "EnableDefaultAlarms" : True, # Support for default alarms generated by the server
+ "RemoveDuplicateAlarms": True, # Remove duplicate alarms on PUT
# CardDAV Features
"DirectoryAddressBook": {
"Enabled": True,
- "type": "twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService",
- "params": directoryAddressBookBackingServiceDefaultParams["twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService"],
- "name": "directory",
+ "type": "twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService",
+ "params": directoryAddressBookBackingServiceDefaultParams["twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService"],
+ "name": "directory",
"MaxQueryResults": 1000,
},
"EnableSearchAddressBook": False, # /directory resource exists
"AnonymousDirectoryAddressBookAccess": False, # Anonymous users may access directory address book
"GlobalAddressBook": {
- "Enabled": False,
- "Name": "global-addressbook",
+ "Enabled": False,
+ "Name": "global-addressbook",
"EnableAnonymousReadAccess": False,
},
@@ -607,20 +607,35 @@
#
"Scheduling": {
-
+
"CalDAV": {
- "EmailDomain" : "", # Domain for mailto calendar user addresses on this server
- "HTTPDomain" : "", # Domain for http calendar user addresses on this server
- "AddressPatterns" : [], # Regex patterns to match local calendar user addresses
- "OldDraftCompatibility" : True, # Whether to maintain compatibility with non-implicit mode
- "ScheduleTagCompatibility" : True, # Whether to support older clients that do not use Schedule-Tag feature
- "EnablePrivateComments" : True, # Private comments from attendees to organizer
+ "EmailDomain" : "", # Domain for mailto calendar user addresses on this server
+ "HTTPDomain" : "", # Domain for http calendar user addresses on this server
+ "AddressPatterns" : [], # Regex patterns to match local calendar user addresses
+ "OldDraftCompatibility" : True, # Whether to maintain compatibility with non-implicit mode
+ "ScheduleTagCompatibility" : True, # Whether to support older clients that do not use Schedule-Tag feature
+ "EnablePrivateComments" : True, # Private comments from attendees to organizer
},
"iSchedule": {
"Enabled" : False, # iSchedule protocol
- "AddressPatterns" : [], # Reg-ex patterns to match iSchedule-able calendar user addresses
- "Servers" : "servertoserver.xml", # iSchedule server configurations
+ "AddressPatterns" : [], # Reg-ex patterns to match iSchedule-able calendar user addresses
+ "RemoteServers" : "remoteservers.xml", # iSchedule server configurations
+ "DNSDebug" : "", # File where a fake Bind zone exists for creating fake DNS results
+ "DKIM" : { # DKIM options
+ "Enabled" : True, # DKIM signing/verification enabled
+ "Domain" : "", # Domain for DKIM (defaults to ServerHostName)
+ "KeySelector" : "ischedule", # Selector for public key
+ "SignatureAlgorithm" : "rsa-sha256", # Signature algorithm (one of rsa-sha1 or rsa-sha256)
+ "UseDNSKey" : True, # This server's public key stored in DNS
+ "UseHTTPKey" : True, # This server's public key stored in HTTP /.well-known
+ "UsePrivateExchangeKey" : True, # This server's public key manually exchanged with others
+ "ExpireSeconds" : 3600, # Expiration time for signature verification
+ "PrivateKeyFile" : "", # File where private key is stored
+ "PublicKeyFile" : "", # File where public key is stored
+ "PrivateExchanges" : "", # Directory where private exchange public keys are stored
+ "ProtocolDebug" : False, # Turn on protocol level debugging to return detailed information to the requestor
+ },
},
"iMIP": {
@@ -628,28 +643,28 @@
"MailGatewayServer" : "localhost",
"MailGatewayPort" : 62310,
"Username" : "com.apple.calendarserver", # For account injecting replies
- "Password" : "", # For account injecting replies
+ "Password" : "", # For account injecting replies
"GUID" : "B86ED9D3-49BD-44F8-8F5E-C89D08753DAC", # GUID for special internal user
"Header" : "x-calendarserver-internal", # HTTP header for internal authentication
"Sending": {
- "Server" : "", # SMTP server to relay messages through
- "Port" : 587, # SMTP server port to relay messages through
- "Address" : "", # 'From' address for server
+ "Server" : "", # SMTP server to relay messages through
+ "Port" : 587, # SMTP server port to relay messages through
+ "Address" : "", # 'From' address for server
"UseSSL" : True,
- "Username" : "", # For account sending mail
- "Password" : "", # For account sending mail
- "SuppressionDays" : 7, # Messages for events older than this may days are not sent
+ "Username" : "", # For account sending mail
+ "Password" : "", # For account sending mail
+ "SuppressionDays" : 7, # Messages for events older than this may days are not sent
},
"Receiving": {
- "Server" : "", # Server to retrieve email messages from
- "Port" : 0, # Server port to retrieve email messages from
+ "Server" : "", # Server to retrieve email messages from
+ "Port" : 0, # Server port to retrieve email messages from
"UseSSL" : True,
- "Type" : "", # Type of message access server: 'pop' or 'imap'
- "PollingSeconds" : 30, # How often to fetch mail
- "Username" : "", # For account receiving mail
- "Password" : "", # For account receiving mail
+ "Type" : "", # Type of message access server: 'pop' or 'imap'
+ "PollingSeconds" : 30, # How often to fetch mail
+ "Username" : "", # For account receiving mail
+ "Password" : "", # For account receiving mail
},
- "AddressPatterns" : [], # Regex patterns to match iMIP-able calendar user addresses
+ "AddressPatterns" : [], # Regex patterns to match iMIP-able calendar user addresses
"MailTemplatesDirectory": "/Applications/Server.app/Contents/ServerRoot/usr/share/caldavd/share/email_templates", # Directory containing HTML templates for email invitations (invite.html, cancel.html)
"MailIconsDirectory": "/Applications/Server.app/Contents/ServerRoot/usr/share/caldavd/share/date_icons", # Directory containing language-specific subdirectories containing date-specific icons for email invitations
"InvitationDaysToLive" : 90, # How many days invitations are valid
@@ -659,23 +674,23 @@
"AllowGroupAsOrganizer" : False, # Allow groups to be Organizers
"AllowLocationAsOrganizer" : False, # Allow locations to be Organizers
"AllowResourceAsOrganizer" : False, # Allow resources to be Organizers
- "LimitFreeBusyAttendees" : 30, # Maximum number of attendees to request freebusy for
- "AttendeeRefreshBatch" : 5, # Number of attendees to do batched refreshes: 0 - no batching
- "AttendeeRefreshBatchDelaySeconds" : 5, # Time after an iTIP REPLY for first batched attendee refresh
- "AttendeeRefreshBatchIntervalSeconds" : 5, # Time between attendee batch refreshes
- "UIDLockTimeoutSeconds" : 60, # Time for implicit UID lock timeout
- "UIDLockExpirySeconds" : 300, # Expiration time for UID lock,
- "V1Compatibility" : False, # Allow /path-based CUAs in scheduling replies
- "PrincipalHostAliases" : [], # Hostnames matched in http(s) CUAs
-
- "DelegeteRichFreeBusy" : True, # Delegates can get extra info in a freebusy request
- "RoomResourceRichFreeBusy" : True, # Any user can get extra info for rooms/resources in a freebusy request
+ "LimitFreeBusyAttendees" : 30, # Maximum number of attendees to request freebusy for
+ "AttendeeRefreshBatch" : 5, # Number of attendees to do batched refreshes: 0 - no batching
+ "AttendeeRefreshBatchDelaySeconds" : 5, # Time after an iTIP REPLY for first batched attendee refresh
+ "AttendeeRefreshBatchIntervalSeconds" : 5, # Time between attendee batch refreshes
+ "UIDLockTimeoutSeconds" : 60, # Time for implicit UID lock timeout
+ "UIDLockExpirySeconds" : 300, # Expiration time for UID lock,
+ "V1Compatibility" : False, # Allow /path-based CUAs in scheduling replies
+ "PrincipalHostAliases" : [], # Hostnames matched in http(s) CUAs
+ "DelegeteRichFreeBusy" : True, # Delegates can get extra info in a freebusy request
+ "RoomResourceRichFreeBusy" : True, # Any user can get extra info for rooms/resources in a freebusy request
+
"AutoSchedule" : {
- "Enabled" : True, # Auto-scheduling will never occur if set to False
- "Always" : False, # Override augments setting and always auto-schedule
- "AllowUsers" : False, # Allow auto-schedule for users
- "DefaultMode" : "automatic", # Default mode for auto-schedule processing, one of:
+ "Enabled" : True, # Auto-scheduling will never occur if set to False
+ "Always" : False, # Override augments setting and always auto-schedule
+ "AllowUsers" : False, # Allow auto-schedule for users
+ "DefaultMode" : "automatic", # Default mode for auto-schedule processing, one of:
# "none" - no auto-scheduling
# "accept-always" - always accept, ignore busy time
# "decline-always" - always decline, ignore free time
@@ -688,7 +703,7 @@
"FreeBusyURL": {
"Enabled" : False, # Per-user free-busy-url protocol
- "TimePeriod" : 14, # Number of days into the future to generate f-b data if no explicit time-range is specified
+ "TimePeriod" : 14, # Number of days into the future to generate f-b data if no explicit time-range is specified
"AnonymousAccess" : False, # Allow anonymous read access to free-busy URL
},
@@ -779,11 +794,11 @@
# Support multiple hosts within a domain
#
"Servers" : {
- "Enabled": False, # Multiple servers/partitions enabled or not
- "ConfigFile": "servers.xml", # File path for server information
- "MaxClients": 5, # Pool size for connections to each partition
+ "Enabled": False, # Multiple servers/partitions enabled or not
+ "ConfigFile": "localservers.xml", # File path for server information
+ "MaxClients": 5, # Pool size for connections to each partition
},
- "ServerPartitionID": "", # Unique ID for this server's partition instance.
+ "ServerPartitionID": "", # Unique ID for this server's partition instance.
#
# Performance tuning
@@ -935,17 +950,17 @@
"EnableKeepAlive": False,
- "EnableResponseCache": True,
+ "EnableResponseCache": True,
"ResponseCacheTimeout": 30, # Minutes
- "EnableFreeBusyCache": True,
- "FreeBusyCacheDaysBack": 7,
- "FreeBusyCacheDaysForward": 12 * 7,
+ "EnableFreeBusyCache": True,
+ "FreeBusyCacheDaysBack": 7,
+ "FreeBusyCacheDaysForward": 12 * 7,
- "FreeBusyIndexLowerLimitDays": 365,
+ "FreeBusyIndexLowerLimitDays": 365,
"FreeBusyIndexExpandAheadDays": 365,
- "FreeBusyIndexExpandMaxDays": 5 * 365,
- "FreeBusyIndexDelayedExpand": True,
+ "FreeBusyIndexExpandMaxDays": 5 * 365,
+ "FreeBusyIndexDelayedExpand": True,
# Specify which opendirectory module to use:
# "opendirectory" is PyOpenDirectory (the old one which uses
@@ -965,7 +980,7 @@
# during migration
"MigratedInboxDaysCutoff": 60,
- "Includes": [], # Other plists to parse after this one
+ "Includes": [], # Other plists to parse after this one
}
@@ -988,7 +1003,7 @@
class PListConfigProvider(ConfigProvider):
-
+
def loadConfig(self):
configDict = {}
if self._configFileName:
@@ -1011,12 +1026,14 @@
try:
configDict = parser.parse(open(filename))
except (IOError, OSError):
- log.err("Configuration file does not exist or is inaccessible: %s" % (filename, ))
- raise ConfigurationError("Configuration file does not exist or is inaccessible: %s" % (filename, ))
+ log.err("Configuration file does not exist or is inaccessible: %s" % (filename,))
+ raise ConfigurationError("Configuration file does not exist or is inaccessible: %s" % (filename,))
else:
configDict = _cleanup(configDict, self._defaults)
return configDict
+
+
def _expandPath(path):
if '$' in path:
return path.replace('$', getfqdn())
@@ -1035,6 +1052,10 @@
("DataRoot", "AttachmentsRoot"),
("DataRoot", ("TimezoneService", "BasePath",)),
("ConfigRoot", "SudoersFile"),
+ ("ConfigRoot", ("Scheduling", "iSchedule", "DNSDebug",)),
+ ("ConfigRoot", ("Scheduling", "iSchedule", "DKIM", "PrivateKeyFile",)),
+ ("ConfigRoot", ("Scheduling", "iSchedule", "DKIM", "PublicKeyFile",)),
+ ("ConfigRoot", ("Scheduling", "iSchedule", "DKIM", "PrivateExchanges",)),
("LogRoot", "AccessLogFile"),
("LogRoot", "ErrorLogFile"),
("LogRoot", ("Postgres", "LogFile",)),
@@ -1062,7 +1083,7 @@
if root in configDict:
if isinstance(relativePath, str):
relativePath = (relativePath,)
-
+
inDict = configDict
for segment in relativePath[:-1]:
if segment not in inDict:
@@ -1074,7 +1095,7 @@
if inDict and lastPath in inDict:
previousAbsoluteName = ".absolute." + relativePath
previousRelativeName = ".relative." + relativePath
-
+
# If we previously made the name absolute, and the name in the
# config is still the same absolute name that we made it, let's
# change it to be the relative name again. (This is necessary
@@ -1095,6 +1116,7 @@
configDict[previousAbsoluteName] = newAbsolutePath
+
def _updateHostName(configDict, reloading=False):
if not configDict.ServerHostName:
hostname = getfqdn()
@@ -1102,6 +1124,8 @@
hostname = "localhost"
configDict.ServerHostName = hostname
+
+
def _preUpdateDirectoryService(configDict, items, reloading=False):
# Special handling for directory services configs
dsType = items.get("DirectoryService", {}).get("type", None)
@@ -1121,13 +1145,17 @@
for param in items.get("DirectoryService", {}).get("params", {}):
if dsType in DEFAULT_SERVICE_PARAMS and param not in DEFAULT_SERVICE_PARAMS[dsType]:
log.warn("Parameter %s is not supported by service %s" % (param, dsType))
-
+
+
+
def _postUpdateDirectoryService(configDict, reloading=False):
if configDict.DirectoryService.type in DEFAULT_SERVICE_PARAMS:
for param in tuple(configDict.DirectoryService.params):
if param not in DEFAULT_SERVICE_PARAMS[configDict.DirectoryService.type]:
del configDict.DirectoryService.params[param]
+
+
def _preUpdateResourceService(configDict, items, reloading=False):
# Special handling for directory services configs
dsType = items.get("ResourceService", {}).get("type", None)
@@ -1147,7 +1175,9 @@
for param in items.get("ResourceService", {}).get("params", {}):
if dsType in DEFAULT_RESOURCE_PARAMS and param not in DEFAULT_RESOURCE_PARAMS[dsType]:
log.warn("Parameter %s is not supported by service %s" % (param, dsType))
-
+
+
+
def _postUpdateResourceService(configDict, reloading=False):
if configDict.ResourceService.type in DEFAULT_RESOURCE_PARAMS:
for param in tuple(configDict.ResourceService.params):
@@ -1155,6 +1185,7 @@
del configDict.ResourceService.params[param]
+
def _preUpdateDirectoryAddressBookBackingDirectoryService(configDict, items, reloading=False):
#
# Special handling for directory address book configs
@@ -1183,6 +1214,8 @@
if param not in directoryAddressBookBackingServiceDefaultParams[configDict.DirectoryAddressBook.type]:
del configDict.DirectoryAddressBook.params[param]
+
+
def _postUpdateAugmentService(configDict, reloading=False):
if configDict.AugmentService.type in DEFAULT_AUGMENT_PARAMS:
for param in tuple(configDict.AugmentService.params):
@@ -1190,6 +1223,8 @@
log.warn("Parameter %s is not supported by service %s" % (param, configDict.AugmentService.type))
del configDict.AugmentService.params[param]
+
+
def _postUpdateProxyDBService(configDict, reloading=False):
if configDict.ProxyDBService.type in DEFAULT_PROXYDB_PARAMS:
for param in tuple(configDict.ProxyDBService.params):
@@ -1197,6 +1232,8 @@
log.warn("Parameter %s is not supported by service %s" % (param, configDict.ProxyDBService.type))
del configDict.ProxyDBService.params[param]
+
+
def _updateACLs(configDict, reloading=False):
#
# Base resource ACLs
@@ -1244,7 +1281,7 @@
readOnlyACE(configDict.EnableAnonymousReadRoot),
# Add inheritable all access for admins
- *configDict.AdminACEs
+ * configDict.AdminACEs
)
log.debug("Root ACL: %s" % (configDict.RootResourceACL.toxml(),))
@@ -1254,7 +1291,7 @@
readOnlyACE(configDict.EnableAnonymousReadNav),
# Add read and read-acl access for admins
- *[
+ * [
davxml.ACE(
davxml.Principal(davxml.HRef(principal)),
davxml.Grant(
@@ -1292,6 +1329,8 @@
except re.error, e:
raise ConfigurationError("Invalid regular expression in RejectClients: %s" % (e,))
+
+
def _updateLogLevels(configDict, reloading=False):
clearLogLevels()
@@ -1308,6 +1347,8 @@
except InvalidLogLevelError, e:
raise ConfigurationError("Invalid log level: %s" % (e.level))
+
+
def _updateNotifications(configDict, reloading=False):
# Reloading not supported -- requires process running as root
if reloading:
@@ -1358,7 +1399,6 @@
# The password doesn't exist in the keychain.
log.info("%s APN certificate passphrase not found in keychain" % (protocol,))
-
if (
service["Service"] == "twistedcaldav.notify.XMPPNotifierService" and
service["Enabled"]
@@ -1440,17 +1480,21 @@
log.info("iMIP %s password not found in keychain" %
(direction,))
+
+
def _updateServers(configDict, reloading=False):
- import servers
+ from twistedcaldav.scheduling.ischedule.localservers import Servers
if configDict.Servers.Enabled:
- servers.Servers.load()
- servers.Servers.getThisServer().installReverseProxies(
+ Servers.load()
+ Servers.getThisServer().installReverseProxies(
configDict.ServerPartitionID,
configDict.Servers.MaxClients,
)
else:
- servers.Servers.clear()
+ Servers.clear()
+
+
def _updateCompliance(configDict, reloading=False):
if configDict.EnableCalDAV:
@@ -1508,7 +1552,7 @@
_updateServers,
_updateCompliance,
)
-
+
def _cleanup(configDict, defaultDict):
cleanDict = copy.deepcopy(configDict)
@@ -1521,6 +1565,7 @@
log.err("Ignoring unknown configuration option: %r" % (key,))
del cleanDict[key]
+
def deprecated(oldKey, newKey):
log.err("Configuration option %r is deprecated in favor of %r." % (oldKey, newKey))
if oldKey in configDict and newKey in configDict:
@@ -1529,6 +1574,7 @@
% (oldKey, newKey, newKey)
)
+
def renamed(oldKey, newKey):
deprecated(oldKey, newKey)
cleanDict[newKey] = configDict[oldKey]
@@ -1570,6 +1616,8 @@
}
return preserved
+
+
def _restoreConfig(configDict, preserved):
"""
Restore certain config keys across reset( ) because these can't be
Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,5 +1,5 @@
# -*- test-case-name: twistedcaldav.test.test_wrapping -*-
-##
+# #
# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-##
+# #
import time
import hashlib
@@ -49,12 +49,12 @@
)
from twistedcaldav import customxml, carddavxml, caldavxml
-from twistedcaldav.cache import CacheStoreNotifier, ResponseCacheMixin,\
+from twistedcaldav.cache import CacheStoreNotifier, ResponseCacheMixin, \
DisabledCacheNotifier
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.carddavxml import carddav_namespace
from twistedcaldav.config import config
-from twistedcaldav.ical import Component as VCalendar, Property as VProperty,\
+from twistedcaldav.ical import Component as VCalendar, Property as VProperty, \
InvalidICalendarDataError, iCalendarProductID, allowedComponents
from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
@@ -62,9 +62,9 @@
from twistedcaldav.notifications import (
NotificationCollectionResource, NotificationResource
)
-from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource,\
+from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource, \
DefaultAlarmPropertyMixin
-from twistedcaldav.schedule import ScheduleInboxResource
+from twistedcaldav.scheduling.caldav.resource import ScheduleInboxResource
from twistedcaldav.scheduling.implicit import ImplicitScheduler
from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
@@ -91,6 +91,7 @@
"""
self._newPropertyStore = newPropertyStore
+
@classmethod
def _convertKey(cls, qname):
namespace, name = qname
@@ -115,7 +116,6 @@
FORBIDDEN,
"Property cannot be changed: %s" % (property.sname(),)
))
-
def delete(self, qname):
@@ -171,6 +171,7 @@
def exists(self):
return self._newStoreObject is not None
+
def name(self):
return self._newStoreObject.name() if self._newStoreObject is not None else self._name
@@ -232,7 +233,7 @@
def liveProperties(self):
props = super(_CommonHomeChildCollectionMixin, self).liveProperties()
-
+
if config.MaxResourcesPerCollection:
props += (customxml.MaxResources.qname(),)
@@ -241,6 +242,7 @@
return props
+
@inlineCallbacks
def readProperty(self, property, request):
if type(property) is tuple:
@@ -253,12 +255,15 @@
returnValue((yield super(_CommonHomeChildCollectionMixin, self).readProperty(property, request)))
+
def url(self):
return joinURL(self._parentResource.url(), self._name, "/")
+
def parentResource(self):
return self._parentResource
+
def index(self):
"""
Retrieve the new-style index wrapper.
@@ -288,19 +293,20 @@
if self._newStoreObject:
newStoreObject = yield self._newStoreObject.objectResourceWithName(name)
-
+
similar = self._childClass(
newStoreObject,
self._newStoreObject,
name,
principalCollections=self._principalCollections
)
-
+
self.propagateTransaction(similar)
returnValue(similar)
else:
returnValue(NoParent())
+
@inlineCallbacks
def listChildren(self):
"""
@@ -310,12 +316,14 @@
children.update((yield self._newStoreObject.listObjectResources()))
returnValue(sorted(children))
+
def countChildren(self):
"""
@return: L{Deferred} with the count of all known children of this resource.
"""
return self._newStoreObject.countObjectResources()
+
def name(self):
return self._name
@@ -343,6 +351,7 @@
def getInternalSyncToken(self):
return self._newStoreObject.syncToken() if self._newStoreObject else None
+
@inlineCallbacks
def findChildrenFaster(
self, depth, request, okcallback, badcallback, missingcallback,
@@ -351,36 +360,38 @@
"""
Override to pre-load children in certain collection types for better performance.
"""
-
+
if depth == "1":
if names:
yield self._newStoreObject.objectResourcesWithNames(names)
else:
yield self._newStoreObject.objectResources()
-
+
result = (yield super(_CommonHomeChildCollectionMixin, self).findChildrenFaster(
depth, request, okcallback, badcallback, missingcallback, names, privileges, inherited_aces
))
-
+
returnValue(result)
-
+
+
@inlineCallbacks
def createCollection(self):
"""
Override C{createCollection} to actually do the work.
"""
self._newStoreObject = (yield self._newStoreParentHome.createChildWithName(self._name))
-
+
# Re-initialize to get stuff setup again now we have a "real" object
self._initializeWithHomeChild(self._newStoreObject, self._parentResource)
returnValue(CREATED)
+
@requiresPermissions(fromParent=[davxml.Unbind()])
@inlineCallbacks
def http_DELETE(self, request):
"""
- Override http_DELETE to validate 'depth' header.
+ Override http_DELETE to validate 'depth' header.
"""
if not self.exists():
@@ -397,6 +408,7 @@
response = (yield self.storeRemove(request, True, request.uri))
returnValue(response)
+
@inlineCallbacks
def storeRemove(self, request, viaRequest, where):
"""
@@ -464,7 +476,7 @@
# Actually delete it.
yield self._newStoreObject.remove()
-
+
# Re-initialize to get stuff setup again now we have no object
self._initializeWithHomeChild(None, self._parentResource)
@@ -478,6 +490,7 @@
returnValue(response)
+
def http_COPY(self, request):
"""
Copying of calendar collections isn't allowed.
@@ -506,7 +519,7 @@
destination = yield request.locateResource(destinationURI)
if destination.exists():
returnValue(FORBIDDEN)
-
+
# Forget the destination now as after the move we will need to re-init it with its
# new store object
request._forgetResource(destination, destinationURI)
@@ -516,6 +529,7 @@
yield self._newStoreObject.rename(basename)
returnValue(NO_CONTENT)
+
@inlineCallbacks
def _readGlobalProperty(self, qname, property, request):
@@ -534,6 +548,7 @@
result = (yield super(_CommonHomeChildCollectionMixin, self)._readGlobalProperty(qname, property, request))
returnValue(result)
+
@inlineCallbacks
def checkCTagPrecondition(self, request):
if request.headers.hasHeader("If"):
@@ -547,6 +562,7 @@
if testctag != ctag:
raise HTTPError(StatusResponse(responsecode.PRECONDITION_FAILED, "CTag pre-condition failure"))
+
def checkReturnChanged(self, request):
if request.headers.hasHeader("X-MobileMe-DAV-Options"):
return_changed = request.headers.getRawHeaders("X-MobileMe-DAV-Options")[0]
@@ -554,34 +570,35 @@
else:
return False
+
@requiresPermissions(davxml.Bind())
@inlineCallbacks
def simpleBatchPOST(self, request):
-
+
# If CTag precondition
yield self.checkCTagPrecondition(request)
-
+
# Look for return changed data option
return_changed = self.checkReturnChanged(request)
# Read in all data
data = (yield allDataFromStream(request.stream))
-
+
components = self.componentsFromData(data)
if components is None:
raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body"))
-
+
# Build response
xmlresponses = []
for ctr, component in enumerate(components):
-
+
code = None
error = None
dataChanged = None
try:
# Create a new name if one was not provided
- name = md5(str(ctr) + component.resourceUID() + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
-
+ name = md5(str(ctr) + component.resourceUID() + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
+
# Get a resource for the new item
newchildURL = joinURL(request.path, name)
newchild = (yield request.locateResource(newchildURL))
@@ -595,9 +612,9 @@
error = (error.namespace, error.name,)
except Exception:
code = responsecode.BAD_REQUEST
-
+
if code is None:
-
+
etag = (yield newchild.etag())
if not return_changed or dataChanged is None:
xmlresponses.append(
@@ -625,7 +642,7 @@
)
)
)
-
+
else:
xmlresponses.append(
davxml.StatusResponse(
@@ -637,9 +654,9 @@
) if error else None,
)
)
-
+
result = MultiStatusResponse(xmlresponses)
-
+
newctag = (yield self.getInternalSyncToken())
result.headers.setRawHeaders("CTag", (newctag,))
@@ -650,16 +667,17 @@
request.extendedLogItems["rcount"] = len(xmlresponses)
returnValue(result)
-
+
+
@inlineCallbacks
def crudBatchPOST(self, request, xmlroot):
-
+
# Need to force some kind of overall authentication on the request
yield self.authorize(request, (davxml.Read(), davxml.Write(),))
# If CTag precondition
yield self.checkCTagPrecondition(request)
-
+
# Look for return changed data option
return_changed = self.checkReturnChanged(request)
@@ -671,7 +689,7 @@
updateCount = 0
deleteCount = 0
for xmlchild in xmlroot.children:
-
+
# Determine the multiput operation: create, update, delete
href = xmlchild.childOfType(davxml.HRef.qname())
set = xmlchild.childOfType(davxml.Set.qname())
@@ -679,11 +697,11 @@
xmldata_root = prop if prop else set
xmldata = xmldata_root.childOfType(self.xmlDataElementType().qname()) if xmldata_root is not None else None
if href is None:
-
+
if xmldata is None:
raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body without a DAV:Href present"))
-
- # Do privilege check on collection once
+
+ # Do privilege check on collection once
if checkedBindPrivelege is None:
try:
yield self.authorize(request, (davxml.Bind(),))
@@ -707,7 +725,7 @@
yield self.crudUpdate(request, str(href), xmldata, ifmatch, return_changed, xmlresponses)
updateCount += 1
else:
- # Do privilege check on collection once
+ # Do privilege check on collection once
if checkedUnbindPrivelege is None:
try:
yield self.authorize(request, (davxml.Unbind(),))
@@ -715,11 +733,11 @@
except HTTPError, e:
checkedUnbindPrivelege = e
- yield self.crudDelete(request, str(href), ifmatch, xmlresponses, checkedUnbindPrivelege);
+ yield self.crudDelete(request, str(href), ifmatch, xmlresponses, checkedUnbindPrivelege)
deleteCount += 1
-
+
result = MultiStatusResponse(xmlresponses)
-
+
newctag = (yield self.getInternalSyncToken())
result.headers.setRawHeaders("CTag", (newctag,))
@@ -737,9 +755,10 @@
returnValue(result)
+
@inlineCallbacks
def crudCreate(self, request, xmldata, xmlresponses, return_changed, hasPrivilege):
-
+
code = None
error = None
try:
@@ -750,8 +769,8 @@
component = xmldata.generateComponent()
# Create a new name if one was not provided
- name = md5(str(componentdata) + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
-
+ name = md5(str(componentdata) + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
+
# Get a resource for the new item
newchildURL = joinURL(request.path, name)
newchild = (yield request.locateResource(newchildURL))
@@ -768,7 +787,7 @@
except Exception:
code = responsecode.BAD_REQUEST
-
+
if code is None:
etag = (yield newchild.etag())
xmlresponses.append(
@@ -795,6 +814,7 @@
)
)
+
@inlineCallbacks
def crudUpdate(self, request, href, xmldata, ifmatch, return_changed, xmlresponses):
code = None
@@ -814,7 +834,7 @@
etag = (yield updateResource.etag())
if ifmatch and ifmatch != etag.generate():
raise HTTPError(responsecode.PRECONDITION_FAILED)
-
+
yield self.storeResourceData(request, updateResource, href, component, componentdata)
# FIXME: figure out return_changed behavior
@@ -828,7 +848,7 @@
except Exception:
code = responsecode.BAD_REQUEST
-
+
if code is None:
xmlresponses.append(
davxml.PropertyStatusResponse(
@@ -852,6 +872,7 @@
)
)
+
@inlineCallbacks
def crudDelete(self, request, href, ifmatch, xmlresponses, hasPrivilege):
code = None
@@ -870,7 +891,7 @@
raise HTTPError(responsecode.PRECONDITION_FAILED)
yield deleteResource.storeRemove(
- request,
+ request,
True,
href,
)
@@ -884,7 +905,7 @@
except Exception:
code = responsecode.BAD_REQUEST
-
+
if code is None:
xmlresponses.append(
davxml.StatusResponse(
@@ -907,14 +928,17 @@
def notifierID(self, label="default"):
self._newStoreObject.notifierID(label)
+
def notifyChanged(self):
return self._newStoreObject.notifyChanged()
+
+
class _CalendarCollectionBehaviorMixin():
"""
Functions common to calendar and inbox collections
"""
-
+
# Support component set behaviors
def setSupportedComponentSet(self, support_components_property):
"""
@@ -922,7 +946,8 @@
"""
support_components = tuple([comp.attributes["name"].upper() for comp in support_components_property.children])
return self.setSupportedComponents(support_components)
-
+
+
def getSupportedComponentSet(self):
comps = self._newStoreObject.getSupportedComponents()
if comps:
@@ -933,6 +958,7 @@
*[caldavxml.CalendarComponent(name=item) for item in comps]
)
+
def setSupportedComponents(self, components):
"""
Set the allowed component set for this calendar.
@@ -940,14 +966,15 @@
@param components: list of names of components to support
@type components: C{list}
"""
-
+
# Validate them first - raise on failure
if not self.validSupportedComponents(components):
raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Invalid CALDAV:supported-calendar-component-set"))
support_components = ",".join(sorted([comp.upper() for comp in components]))
return maybeDeferred(self._newStoreObject.setSupportedComponents, support_components)
-
+
+
def getSupportedComponents(self):
comps = self._newStoreObject.getSupportedComponents()
if comps:
@@ -956,9 +983,11 @@
comps = allowedComponents
return comps
+
def isSupportedComponent(self, componentType):
return self._newStoreObject.isSupportedComponent(componentType)
+
def validSupportedComponents(self, components):
"""
Test whether the supplied set of components is valid for the current server's component set
@@ -968,13 +997,13 @@
return components in (("VEVENT",), ("VTODO",),)
return True
-
+
+
class CalendarCollectionResource(DefaultAlarmPropertyMixin, _CalendarCollectionBehaviorMixin, _CommonHomeChildCollectionMixin, CalDAVResource):
"""
Wrapper around a L{txdav.caldav.icalendar.ICalendar}.
"""
-
def __init__(self, calendar, home, name=None, *args, **kw):
"""
Create a CalendarCollectionResource from a L{txdav.caldav.icalendar.ICalendar}
@@ -990,6 +1019,7 @@
self._postHandlers[("text", "calendar")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
self.xmlDocHandlers[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST
+
def __repr__(self):
return "<Calendar Collection Resource %r:%r %s>" % (
self._newStoreParentHome.uid(),
@@ -1008,6 +1038,7 @@
"""
return True
+
@inlineCallbacks
def iCalendarRolledup(self, request):
# FIXME: uncached: implement cache in the storage layer
@@ -1031,7 +1062,7 @@
isowner = (yield self.isOwner(request))
accessPrincipal = (yield self.resourceOwnerPrincipal(request))
- for name, uid, type in (yield maybeDeferred(self.index().bruteForceSearch)): #@UnusedVariable
+ for name, uid, type in (yield maybeDeferred(self.index().bruteForceSearch)): # @UnusedVariable
try:
child = yield request.locateChildResource(self, name)
except TypeError:
@@ -1052,7 +1083,7 @@
assert subcalendar.name() == "VCALENDAR"
for component in subcalendar.subcomponents():
-
+
# Only insert VTIMEZONEs once
if component.name() == "VTIMEZONE":
tzid = component.propertyValue("TZID")
@@ -1068,21 +1099,21 @@
returnValue(calendar)
-
createCalendarCollection = _CommonHomeChildCollectionMixin.createCollection
+
@classmethod
def componentsFromData(cls, data):
"""
Need to split a single VCALENDAR into separate ones based on UID with the
appropriate VTIEMZONES included.
"""
-
+
results = []
# Split into components by UID and TZID
try:
- vcal = VCalendar.fromString(data)
+ vcal = VCalendar.fromString(data)
except InvalidICalendarDataError:
return None
@@ -1093,14 +1124,14 @@
by_tzid[subcomponent.propertyValue("TZID")] = subcomponent
else:
by_uid.setdefault(subcomponent.propertyValue("UID"), []).append(subcomponent)
-
+
# Re-constitute as separate VCALENDAR objects
for components in by_uid.values():
-
+
newvcal = VCalendar("VCALENDAR")
newvcal.addProperty(VProperty("VERSION", "2.0"))
newvcal.addProperty(VProperty("PRODID", vcal.propertyValue("PRODID")))
-
+
# Get the set of TZIDs and include them
tzids = set()
for component in components:
@@ -1113,39 +1144,42 @@
# We ignore the error and generate invalid ics which someone will
# complain about at some point
pass
-
+
# Now add each component
for component in components:
newvcal.addComponent(component.duplicate())
-
+
results.append(newvcal)
-
+
return results
+
@classmethod
def resourceSuffix(cls):
return ".ics"
+
@classmethod
def xmlDataElementType(cls):
return caldavxml.CalendarData
+
@inlineCallbacks
def storeResourceData(self, request, newchild, newchildURL, component, returnData=False):
storer = StoreCalendarObjectResource(
- request = request,
- destination = newchild,
- destination_uri = newchildURL,
- destinationcal = True,
- destinationparent = self,
- calendar = component,
- returnData = returnData,
+ request=request,
+ destination=newchild,
+ destination_uri=newchildURL,
+ destinationcal=True,
+ destinationparent=self,
+ calendar=component,
+ returnData=returnData,
)
yield storer.run()
-
+
returnValue(storer.storeddata if hasattr(storer, "storeddata") else None)
-
+
@inlineCallbacks
def storeRemove(self, request, implicitly, where):
"""
@@ -1206,7 +1240,7 @@
that calendar's name.
"""
defaultCalendarType = (yield self.isDefaultCalendar(request))
-
+
result = (yield super(CalendarCollectionResource, self).http_MOVE(request))
if result == NO_CONTENT:
destinationURI = urlsplit(request.headers.getHeader("destination"))[2]
@@ -1216,6 +1250,7 @@
returnValue(result)
+
class StoreScheduleInboxResource(_CalendarCollectionBehaviorMixin, _CommonHomeChildCollectionMixin, ScheduleInboxResource):
def __init__(self, *a, **kw):
@@ -1249,19 +1284,24 @@
def provisionFile(self):
pass
+
def provision(self):
pass
+
def http_DELETE(self, request):
return FORBIDDEN
+
def http_COPY(self, request):
return FORBIDDEN
+
def http_MOVE(self, request):
return FORBIDDEN
+
class _GetChildHelper(CalDAVResource):
def locateChild(self, request, segments):
@@ -1330,7 +1370,7 @@
def resourceType(self,):
- return davxml.ResourceType.dropboxhome #@UndefinedVariable
+ return davxml.ResourceType.dropboxhome # @UndefinedVariable
def listChildren(self):
@@ -1378,7 +1418,7 @@
def resourceType(self):
- return davxml.ResourceType.dropbox #@UndefinedVariable
+ return davxml.ResourceType.dropbox # @UndefinedVariable
@inlineCallbacks
@@ -1522,6 +1562,7 @@
returnValue(davxml.ACL(*tuple(originalACEs + newACEs)))
+
@inlineCallbacks
def sharedDropboxACEs(self):
@@ -1577,6 +1618,7 @@
returnValue(aces)
+
class CalendarAttachment(_NewStoreFileMetaDataHelper, _GetChildHelper):
def __init__(self, calendarObject, attachment, attachmentName, **kw):
@@ -1643,7 +1685,7 @@
except IOError, e:
log.error("Unable to read attachment: %s, due to: %s" % (self, e,))
raise HTTPError(responsecode.NOT_FOUND)
- return Response(OK, {"content-type":self.contentType()}, stream)
+ return Response(OK, {"content-type": self.contentType()}, stream)
@requiresPermissions(fromParent=[davxml.Unbind()])
@@ -1659,30 +1701,37 @@
self._newStoreAttachment = self._newStoreCalendarObject = None
returnValue(NO_CONTENT)
-
http_MKCOL = None
http_MKCALENDAR = None
+
def http_PROPPATCH(self, request):
"""
- No dead properties allowed on attachments.
+ No dead properties allowed on attachments.
"""
return FORBIDDEN
+
def isCollection(self):
return False
+
class NoParent(CalDAVResource):
+
def http_MKCALENDAR(self, request):
return CONFLICT
+
def http_PUT(self, request):
return CONFLICT
+
def isCollection(self):
return False
+
+
class _CommonObjectResource(_NewStoreFileMetaDataHelper, CalDAVResource, FancyEqMixin):
_componentFromStream = None
@@ -1719,6 +1768,7 @@
def component(self):
return self._newStoreObject.component()
+
@inlineCallbacks
def render(self, request):
if not self.exists():
@@ -1735,7 +1785,7 @@
@requiresPermissions(fromParent=[davxml.Unbind()])
def http_DELETE(self, request):
"""
- Override http_DELETE to validate 'depth' header.
+ Override http_DELETE to validate 'depth' header.
"""
if not self.exists():
log.debug("Resource not found: %s" % (self,))
@@ -1743,15 +1793,17 @@
return self.storeRemove(request, True, request.uri)
+
def http_PROPPATCH(self, request):
"""
- No dead properties allowed on object resources.
+ No dead properties allowed on object resources.
"""
if self._newStoreParent.objectResourcesHaveProperties():
return super(_CommonObjectResource, self).http_PROPPATCH(request)
else:
return FORBIDDEN
+
@inlineCallbacks
def storeStream(self, stream):
@@ -1772,6 +1824,7 @@
returnValue(CREATED)
+
@inlineCallbacks
def storeComponent(self, component):
@@ -1789,7 +1842,6 @@
returnValue(CREATED)
-
@inlineCallbacks
def storeRemove(self, request, implicitly, where):
"""
@@ -1849,13 +1901,13 @@
class _CalendarObjectMetaDataMixin(object):
"""
- Dynamically create the required meta-data for an object resource
+ Dynamically create the required meta-data for an object resource
"""
- accessMode = _MetadataProperty("accessMode")
- isScheduleObject = _MetadataProperty("isScheduleObject")
- scheduleTag = _MetadataProperty("scheduleTag")
- scheduleEtags = _MetadataProperty("scheduleEtags")
+ accessMode = _MetadataProperty("accessMode")
+ isScheduleObject = _MetadataProperty("isScheduleObject")
+ scheduleTag = _MetadataProperty("scheduleTag")
+ scheduleEtags = _MetadataProperty("scheduleEtags")
hasPrivateComment = _MetadataProperty("hasPrivateComment")
@@ -2010,6 +2062,7 @@
returnValue(NO_CONTENT)
+
class AddressBookCollectionResource(_CommonHomeChildCollectionMixin, CalDAVResource):
"""
Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.
@@ -2030,6 +2083,7 @@
self._postHandlers[("text", "vcard")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
self.xmlDocHandlers[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST
+
def __repr__(self):
return "<AddressBook Collection Resource %r:%r %s>" % (
self._newStoreParentHome.uid(),
@@ -2048,9 +2102,9 @@
"""
return True
-
createAddressBookCollection = _CommonHomeChildCollectionMixin.createCollection
+
@classmethod
def componentsFromData(cls, data):
try:
@@ -2058,30 +2112,34 @@
except InvalidVCardDataError:
return None
+
@classmethod
def resourceSuffix(cls):
return ".vcf"
+
@classmethod
def xmlDataElementType(cls):
return carddavxml.AddressData
+
@inlineCallbacks
def storeResourceData(self, request, newchild, newchildURL, component, returnData=False):
storer = StoreAddressObjectResource(
- request = request,
- sourceadbk = False,
- destination = newchild,
- destination_uri = newchildURL,
- destinationadbk = True,
- destinationparent = self,
- vcard = component,
- returnData = returnData,
+ request=request,
+ sourceadbk=False,
+ destination=newchild,
+ destination_uri=newchildURL,
+ destinationadbk=True,
+ destinationparent=self,
+ vcard=component,
+ returnData=returnData,
)
yield storer.run()
-
+
returnValue(storer.returndata if hasattr(storer, "returndata") else None)
+
@inlineCallbacks
def storeRemove(self, request, viaRequest, where):
"""
@@ -2129,6 +2187,7 @@
returnValue(response)
+
# FIXME: access control
@inlineCallbacks
def http_MOVE(self, request):
@@ -2137,7 +2196,7 @@
that address book's name.
"""
defaultAddressBook = (yield self.isDefaultAddressBook(request))
-
+
result = (yield super(AddressBookCollectionResource, self).http_MOVE(request))
if result == NO_CONTENT:
destinationURI = urlsplit(request.headers.getHeader("destination"))[2]
@@ -2146,12 +2205,16 @@
destination, destinationURI)
returnValue(result)
+
+
class GlobalAddressBookCollectionResource(GlobalAddressBookResource, AddressBookCollectionResource):
"""
Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.
"""
pass
+
+
class AddressBookObjectResource(_CommonObjectResource):
"""
A resource wrapping a addressbook object.
@@ -2171,6 +2234,7 @@
vCard = _CommonObjectResource.component
+
class _NotificationChildHelper(object):
"""
Methods for things which are like notification objects.
@@ -2260,9 +2324,11 @@
def name(self):
return "notification"
+
def url(self):
return joinURL(self._parentResource.url(), self.name(), "/")
+
@inlineCallbacks
def listChildren(self):
l = []
@@ -2270,6 +2336,7 @@
l.append(notification.name())
returnValue(l)
+
def isCollection(self):
return True
@@ -2301,6 +2368,7 @@
)
+
class StoreNotificationObjectFile(_NewStoreFileMetaDataHelper, NotificationResource):
"""
A resource wrapping a calendar object.
@@ -2328,6 +2396,7 @@
props += (customxml.NotificationType.qname(),)
return props
+
@inlineCallbacks
def readProperty(self, property, request):
if type(property) is tuple:
@@ -2340,6 +2409,7 @@
returnValue((yield super(StoreNotificationObjectFile, self).readProperty(property, request)))
+
def isCollection(self):
return False
@@ -2361,7 +2431,7 @@
raise HTTPError(responsecode.NOT_FOUND)
returnValue(
- Response(OK, {"content-type":self.contentType()},
+ Response(OK, {"content-type": self.contentType()},
MemoryStream((yield self.text())))
)
@@ -2369,7 +2439,7 @@
@requiresPermissions(fromParent=[davxml.Unbind()])
def http_DELETE(self, request):
"""
- Override http_DELETE to validate 'depth' header.
+ Override http_DELETE to validate 'depth' header.
"""
if not self.exists():
log.debug("Resource not found: %s" % (self,))
@@ -2377,12 +2447,14 @@
return self.storeRemove(request, request.uri)
+
def http_PROPPATCH(self, request):
"""
- No dead properties allowed on notification objects.
+ No dead properties allowed on notification objects.
"""
return FORBIDDEN
+
@inlineCallbacks
def storeRemove(self, request, where):
"""
Deleted: CalendarServer/trunk/twistedcaldav/test/test_mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_mail.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/test/test_mail.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,867 +0,0 @@
-##
-# Copyright (c) 2008-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-
-from cStringIO import StringIO
-from twisted.internet.defer import inlineCallbacks
-from twisted.python.modules import getModule
-from twisted.web.template import Element, renderer, flattenString
-from twistedcaldav.config import config, ConfigDict
-from twistedcaldav.directory import augment
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.ical import Component
-from twistedcaldav.mail import injectionSettingsFromURL
-from twistedcaldav.mail import MailGatewayTokensDatabase
-from twistedcaldav.mail import MailHandler
-from twistedcaldav.mail import StringFormatTemplateLoader
-from twistedcaldav.mail import serverForOrganizer
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.servers import Servers
-from twistedcaldav.test.util import TestCase
-from twistedcaldav.test.util import xmlFile, augmentsFile
-import datetime
-import email
-import os
-from pycalendar.datetime import PyCalendarDateTime
-
-
-def echo(*args):
- return args
-
-initialInviteText = u"""BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REQUEST
-BEGIN:VEVENT
-UID:CFDD5E46-4F74-478A-9311-B3FF905449C3
-DTSTART:20100325T154500Z
-DTEND:20100325T164500Z
-ATTENDEE;CN=Th\xe9 Attendee;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TRU
- E:mailto:attendee at example.com
-ATTENDEE;CN=Th\xe9 Organizer;CUTYPE=INDIVIDUAL;EMAIL=organizer at example.com;P
- ARTSTAT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
-ATTENDEE;CN=An Attendee without CUTYPE;EMAIL=nocutype at example.com;PARTSTAT=A
- CCEPTED:urn:uuid:4DB528DC-3E60-44FA-9546-2A00FCDCFFAB
-ATTENDEE;EMAIL=nocn at example.com;PARTSTAT=ACCEPTED:urn:uuid:A592CF8B-4FC8-4E4
- F-B543-B2F29A7EEB0B
-ORGANIZER;CN=Th\xe9 Organizer;EMAIL=organizer at example.com:urn:uuid:C3B38B00-
- 4166-11DD-B22C-A07C87E02F6A
-SUMMARY:t\xe9sting outbound( )
-DESCRIPTION:awesome description with "<" and "&"
-END:VEVENT
-END:VCALENDAR
-"""
-
-class MailHandlerTests(TestCase):
-
- def setUp(self):
- super(MailHandlerTests, self).setUp()
-
- self._setupServers(serverData)
- self.directory = XMLDirectoryService(
- {
- 'xmlFile' : xmlFile,
- 'augmentService' :
- augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
- }
- )
- self.handler = MailHandler(dataRoot=":memory:", directory=self.directory)
- module = getModule(__name__)
- self.dataPath = module.filePath.sibling("data").child("mail")
-
-
- def _setupServers(self, data):
- self.patch(config, "ServerHostName", "caldav1.example.com")
- self.patch(config, "HTTPPort", 8008)
- self.patch(config.Servers, "Enabled", True)
-
- xmlFile = StringIO(data)
- servers = Servers
- servers.load(xmlFile, ignoreIPLookupFailures=True)
-
-
- def dataFile(self, name):
- """
- Get the contents of a given data file from the 'data/mail' test
- fixtures directory.
- """
- return self.dataPath.child(name).getContent()
-
- def test_serverDetection(self):
- wsanchez = self.directory.recordWithShortName("users",
- "wsanchez")
- cdaboo = self.directory.recordWithShortName("users",
- "cdaboo")
- server = wsanchez.server()
- self.assertEquals(server.uri, "http://caldav1.example.com:8008")
- server = cdaboo.server()
- self.assertEquals(server.uri, "https://caldav2.example.com:8843")
-
- url = serverForOrganizer(self.directory,
- "mailto:wsanchez at example.com")
- self.assertEquals(url, "http://caldav1.example.com:8008")
- url = serverForOrganizer(self.directory,
- "mailto:cdaboo at example.com")
- self.assertEquals(url, "https://caldav2.example.com:8843")
-
- def test_purge_and_lowercase(self):
- """
- Ensure that purge( ) cleans out old tokens, and that lowercase( )
- converts all mailto: to lowercase, since earlier server versions
- didn't do that before inserting into the database.
- """
-
- # Insert an "old" token
- token = "test_token_1"
- organizer = "urn:uuid:19BFE23D-0269-46CA-877C-D4B521A7A9A5"
- attendee = "mailto:you at example.com"
- icaluid = "123"
- pastDate = datetime.date(2009,1,1)
- self.handler.db._db_execute(
- """
- insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
- values (:1, :2, :3, :4, :5)
- """, token, organizer, attendee, icaluid, pastDate
- )
- self.handler.db._db_commit()
-
- # purge, and make sure we don't see that token anymore
- self.handler.purge()
- retrieved = self.handler.db.getToken(organizer, attendee, icaluid)
- self.assertEquals(retrieved, None)
-
-
- # Insert a token with (old-format) mailto:
- token = "test_token_2"
- organizer = "MailTo:Organizer at Example.com"
- attendee = "MAILTO:YouTwo at Example.com"
- icaluid = "456"
- futureDate = datetime.date(2100,1,1)
- self.handler.db._db_execute(
- """
- insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
- values (:1, :2, :3, :4, :5)
- """, token, organizer, attendee, icaluid, futureDate
- )
- self.handler.db._db_commit()
-
- self.handler.lowercase()
- retrieved = self.handler.db.getToken(organizer.lower(),
- attendee.lower(), icaluid)
- self.assertIsInstance(retrieved, str)
- self.assertEquals(retrieved, token)
-
- # Insert a token with (new-format) urn:uuid:
- token = "test_token_3"
- organizer = "urn:uuid:E0CF4031-676B-4668-A9D3-8F33A0212F70"
- attendee = "MAILTO:YouTwo at Example.com"
- icaluid = "789"
- futureDate = datetime.date(2100,1,1)
- self.handler.db._db_execute(
- """
- insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
- values (:1, :2, :3, :4, :5)
- """, token, organizer, attendee, icaluid, futureDate
- )
- self.handler.db._db_commit()
-
- self.handler.lowercase()
- retrieved = self.handler.db.getToken(organizer,
- attendee.lower(), icaluid)
- self.assertEquals(retrieved, token)
-
-
- def test_checkDSNFailure(self):
-
- data = {
- 'good_reply' : (False, None, None),
- 'dsn_failure_no_original' : (True, 'failed', None),
- 'dsn_failure_no_ics' : (True, 'failed', None),
- 'dsn_failure_with_ics' : (True, 'failed', '''BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-PRODID:-//example Inc.//iCal 3.0//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
-DTSTART;TZID=US/Pacific:20080812T094500
-DTEND;TZID=US/Pacific:20080812T104500
-ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:user01 at exam
- ple.com
-ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
- CTION;CN=nonexistant at example.com:mailto:nonexistant at example.com
-CREATED:20080812T191857Z
-DTSTAMP:20080812T191932Z
-ORGANIZER;CN=User 01:mailto:xyzzy+8e16b897-d544-4217-88e9-a363d08
- 46f6c at example.com
-SEQUENCE:2
-SUMMARY:New Event
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-'''),
- }
-
- for filename, expected in data.iteritems():
- msg = email.message_from_string(self.dataFile(filename))
- self.assertEquals(self.handler.checkDSN(msg), expected)
-
-
- def test_processDSN(self):
-
- template = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-PRODID:-//example Inc.//iCal 3.0//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
-DTSTART;TZID=US/Pacific:20080812T094500
-DTEND;TZID=US/Pacific:20080812T104500
-ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:user01 at exam
- ple.com
-ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
- CTION;CN=nonexistant at example.com:mailto:nonexistant at example.com
-CREATED:20080812T191857Z
-DTSTAMP:20080812T191932Z
-ORGANIZER;CN=User 01:mailto:xyzzy+%s at example.com
-SEQUENCE:2
-SUMMARY:New Event
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-"""
-
- # Make sure an unknown token is not processed
- calBody = template % "bogus_token"
- self.assertEquals(self.handler.processDSN(calBody, "xyzzy", echo),
- None)
-
- # Make sure a known token *is* processed
- token = self.handler.db.createToken(
- "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
- "mailto:user02 at example.com", "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C")
- calBody = template % token
- url, organizer, attendee, calendar, msgId = self.handler.processDSN(calBody,
- "xyzzy", echo)
- self.assertEquals(organizer, 'urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500')
- self.assertEquals(attendee, 'mailto:user02 at example.com')
- self.assertEquals(str(calendar), """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-PRODID:-//example Inc.//iCal 3.0//EN
-BEGIN:VTIMEZONE
-TZID:US/Pacific
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:PDT
-TZOFFSETFROM:-0800
-TZOFFSETTO:-0700
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:PST
-TZOFFSETFROM:-0700
-TZOFFSETTO:-0800
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
-DTSTART;TZID=US/Pacific:20080812T094500
-DTEND;TZID=US/Pacific:20080812T104500
-CREATED:20080812T191857Z
-DTSTAMP:20080812T191932Z
-ORGANIZER;CN=User 01:urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500
-REQUEST-STATUS:5.1;Service unavailable
-SEQUENCE:2
-SUMMARY:New Event
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"))
- self.assertEquals(msgId, 'xyzzy')
-
-
- def test_processReply(self):
- msg = email.message_from_string(self.dataFile('good_reply'))
-
- # Make sure an unknown token is not processed
- result = self.handler.processReply(msg, echo)
- self.assertEquals(result, None)
-
- # Make sure a known token *is* processed
- self.handler.db.createToken(
- "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
- "mailto:xyzzy at example.com",
- icaluid="1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
- token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
- )
- url, organizer, attendee, calendar, msgId = self.handler.processReply(msg, echo)
- self.assertEquals(url, "https://caldav2.example.com:8843")
- self.assertEquals(organizer,
- 'urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500')
- self.assertEquals(attendee, 'mailto:xyzzy at example.com')
- self.assertEquals(msgId,
- '<1983F777-BE86-4B98-881E-06D938E60920 at example.com>')
-
- def test_injectionSettingsFromURL(self):
- testData = (
- (
- None,
- {
- "Scheduling": {
- "iMIP" : {
- "MailGatewayServer" : "localhost",
- },
- },
- "EnableSSL" : True,
- "ServerHostName" : "calendar.example.com",
- "HTTPPort" : 1111,
- "SSLPort" : 2222,
- },
- "https://localhost:2222/inbox/",
- ),
- (
- None,
- {
- "Scheduling": {
- "iMIP" : {
- "MailGatewayServer" : "mailgateway.example.com",
- },
- },
- "EnableSSL" : False,
- "ServerHostName" : "calendar.example.com",
- "HTTPPort" : 1111,
- "SSLPort" : 2222,
- },
- "http://calendar.example.com:1111/inbox/",
- ),
- (
- "https://calendar.example.com:1234/",
- { },
- "https://calendar.example.com:1234/inbox/",
- ),
- (
- "https://calendar.example.com:1234",
- { },
- "https://calendar.example.com:1234/inbox/",
- ),
- )
-
- for url, configData, expected in testData:
- self.assertEquals(
- expected,
- injectionSettingsFromURL(url, ConfigDict(mapping=configData))
- )
-
- def test_processReplyMissingOrganizer(self):
- msg = email.message_from_string(self.dataFile('reply_missing_organizer'))
- # stick the token in the database first
- self.handler.db.createToken(
- "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
- "mailto:xyzzy at example.com",
- icaluid="1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
- token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
- )
-
- url, organizer, attendee, calendar, msgId = self.handler.processReply(
- msg, echo)
- organizerProp = calendar.mainComponent().getOrganizerProperty()
- self.assertTrue(organizerProp is not None)
- self.assertEquals(organizer,
- "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500")
-
-
- def test_processReplyMissingAttendee(self):
- msg = email.message_from_string(self.dataFile('reply_missing_attendee'))
-
- # stick the token in the database first
- self.handler.db.createToken(
- "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
- "mailto:xyzzy at example.com",
- icaluid="1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
- token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
- )
-
- url, organizer, attendee, calendar, msgId = self.handler.processReply(
- msg, echo)
-
- # Since the expected attendee was missing, the reply processor should
- # have added an attendee back in with a "5.1;Service unavailable"
- # schedule-status
- attendeeProp = calendar.mainComponent().getAttendeeProperty([attendee])
- self.assertEquals(attendeeProp.parameterValue("SCHEDULE-STATUS"),
- iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
- def test_processReplyMissingAttachment(self):
-
- msg = email.message_from_string(
- self.dataFile('reply_missing_attachment')
- )
- # stick the token in the database first
- self.handler.db.createToken(
- "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
- "mailto:xyzzy at example.com",
- icaluid="1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
- token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
- )
-
- self.assertEquals(
- self.handler.processReply(msg, echo, testMode=True),
- ("cdaboo at example.com", "xyzzy at example.com")
- )
-
-
- @inlineCallbacks
- def test_outbound(self):
- """
- Make sure outbound( ) stores tokens properly so they can be looked up
- """
-
- config.Scheduling.iMIP.Sending.Address = "server at example.com"
- self.patch(config.Localization, "LocalesDirectory", os.path.join(os.path.dirname(__file__), "locales"))
-
- data = (
- # Initial invite
- (
- initialInviteText,
- "CFDD5E46-4F74-478A-9311-B3FF905449C3",
- "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
- "mailto:attendee at example.com",
- "new",
- "organizer at example.com",
- u"Th\xe9 Organizer",
- [
- (u'Th\xe9 Attendee', u'attendee at example.com'),
- (u'Th\xe9 Organizer', u'organizer at example.com'),
- (u'An Attendee without CUTYPE', u'nocutype at example.com'),
- (None, u'nocn at example.com'),
- ],
- u"Th\xe9 Organizer <organizer at example.com>",
- "attendee at example.com",
- ),
-
- # Update
- (
- """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REQUEST
-BEGIN:VEVENT
-UID:CFDD5E46-4F74-478A-9311-B3FF905449C3
-DTSTART:20100325T154500Z
-DTEND:20100325T164500Z
-ATTENDEE;CN=The Attendee;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:
- mailto:attendee at example.com
-ATTENDEE;CN=The Organizer;CUTYPE=INDIVIDUAL;EMAIL=organizer at example.com;PAR
- TSTAT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
-ORGANIZER;CN=The Organizer;EMAIL=organizer at example.com:urn:uuid:C3B38B00-41
- 66-11DD-B22C-A07C87E02F6A
-SUMMARY:testing outbound( ) *update*
-END:VEVENT
-END:VCALENDAR
-""",
- "CFDD5E46-4F74-478A-9311-B3FF905449C3",
- "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
- "mailto:attendee at example.com",
- "update",
- "organizer at example.com",
- "The Organizer",
- [
- (u'The Attendee', u'attendee at example.com'),
- (u'The Organizer', u'organizer at example.com')
- ],
- "The Organizer <organizer at example.com>",
- "attendee at example.com",
- ),
-
- # Reply
- (
- """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REPLY
-BEGIN:VEVENT
-UID:DFDD5E46-4F74-478A-9311-B3FF905449C4
-DTSTART:20100325T154500Z
-DTEND:20100325T164500Z
-ATTENDEE;CN=The Attendee;CUTYPE=INDIVIDUAL;EMAIL=attendee at example.com;PARTST
- AT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
-ORGANIZER;CN=The Organizer;EMAIL=organizer at example.com:mailto:organizer at exam
- ple.com
-SUMMARY:testing outbound( ) *reply*
-END:VEVENT
-END:VCALENDAR
-""",
- None,
- "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
- "mailto:organizer at example.com",
- "reply",
- "organizer at example.com",
- "The Organizer",
- [
- (u'The Attendee', u'attendee at example.com'),
- ],
- "attendee at example.com",
- "organizer at example.com",
- ),
-
- )
- for (inputCalendar, UID, inputOriginator, inputRecipient, inviteState,
- outputOrganizerEmail, outputOrganizerName, outputAttendeeList,
- outputFrom, outputRecipient) in data:
-
- (actualInviteState, actualCalendar, actualOrganizerEmail,
- actualOrganizerName, actualAttendeeList, actualFrom,
- actualRecipient, actualReplyTo) = (yield self.handler.outbound(
- inputOriginator,
- inputRecipient,
- Component.fromString(inputCalendar.replace("\n", "\r\n")),
- language="ja",
- send=False,
- onlyAfter=PyCalendarDateTime(2010, 1, 1, 0, 0, 0))
- )
-
- self.assertEquals(actualInviteState, inviteState)
- self.assertEquals(actualOrganizerEmail, outputOrganizerEmail)
- self.assertEquals(actualOrganizerName, outputOrganizerName)
- self.assertEquals(actualAttendeeList, outputAttendeeList)
- self.assertEquals(actualFrom, outputFrom)
- self.assertEquals(actualRecipient, outputRecipient)
-
- if UID: # The organizer is local, and server is sending to remote
- # attendee
-
- token = self.handler.db.getToken(inputOriginator,
- inputRecipient, UID)
- self.assertNotEquals(token, None)
- self.assertEquals(actualReplyTo,
- "server+%s at example.com" % (token,))
-
- # Make sure attendee property for organizer exists and matches
- # the CUA of the organizer property
- orgValue = actualCalendar.getOrganizerProperty().value()
- self.assertEquals(
- orgValue,
- actualCalendar.getAttendeeProperty([orgValue]).value()
- )
-
-
- else: # Reply only -- the attendee is local, and server is sending
- # reply to remote organizer
-
- self.assertEquals(actualReplyTo, actualFrom)
-
-
- # Check that we don't send any messages for events completely in
- # the past.
- result = (yield self.handler.outbound(
- inputOriginator,
- inputRecipient,
- Component.fromString(inputCalendar.replace("\n", "\r\n")),
- send=False,
- onlyAfter=PyCalendarDateTime(2012, 1, 1, 0, 0, 0))
- )
- self.assertEquals(result, True)
-
-
- @inlineCallbacks
- def test_mailtoTokens(self):
- """
- Make sure old mailto tokens are still honored
- """
-
- organizerEmail = "mailto:organizer at example.com"
-
- config.Scheduling.iMIP.Sending.Address = "server at example.com"
-
- # Explictly store a token with mailto: CUA for organizer
- # (something that doesn't happen any more, but did in the past)
- origToken = self.handler.db.createToken(organizerEmail,
- "mailto:attendee at example.com",
- "CFDD5E46-4F74-478A-9311-B3FF905449C3")
-
- inputCalendar = initialInviteText
- UID = "CFDD5E46-4F74-478A-9311-B3FF905449C3"
- inputOriginator = "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A"
- inputRecipient = "mailto:attendee at example.com"
-
- (actualInviteState, actualCalendar, actualOrganizerEmail,
- actualOrganizerName, actualAttendeeList, actualFrom,
- actualRecipient, actualReplyTo) = (yield self.handler.outbound(
- inputOriginator,
- inputRecipient,
- Component.fromString(inputCalendar.replace("\n", "\r\n")),
- send=False,
- onlyAfter=PyCalendarDateTime(2010, 1, 1, 0, 0, 0))
- )
-
- # Verify we didn't create a new token...
- token = self.handler.db.getToken(inputOriginator,
- inputRecipient, UID)
- self.assertEquals(token, None)
-
- # But instead kept the old one...
- token = self.handler.db.getToken(organizerEmail,
- inputRecipient, UID)
- self.assertEquals(token, origToken)
-
-
- def generateSampleEmail(self):
- """
- Invoke L{MailHandler.generateEmail} and parse the result.
- """
- calendar = Component.fromString(initialInviteText)
- msgID, msgTxt = self.handler.generateEmail(
- inviteState='new',
- calendar=calendar,
- orgEmail=u"user01 at localhost",
- orgCN=u"User Z\xe9ro One",
- attendees=[(u"Us\xe9r One", "user01 at localhost"),
- (u"User 2", "user02 at localhost")],
- fromAddress="user01 at localhost",
- replyToAddress="imip-system at localhost",
- toAddress="user03 at localhost",
- )
- message = email.message_from_string(msgTxt)
- return msgID, message
-
-
- def test_generateEmail(self):
- """
- L{MailHandler.generateEmail} generates a MIME-formatted email with a
- text/plain part, a text/html part, and a text/calendar part.
- """
- msgID, message = self.generateSampleEmail()
- self.assertEquals(message['Message-ID'], msgID)
- expectedTypes = set(["text/plain", "text/html", "text/calendar"])
- actualTypes = set([
- part.get_content_type() for part in message.walk()
- if part.get_content_type().startswith("text/")
- ])
- self.assertEquals(actualTypes, expectedTypes)
-
-
- def test_emailEncoding(self):
- """
- L{MailHandler.generateEmail} will preserve any non-ASCII characters
- present in the fields that it formats in the message body.
- """
- msgID, message = self.generateSampleEmail()
- textPart = partByType(message, "text/plain")
- htmlPart = partByType(message, "text/html")
-
- plainText = textPart.get_payload(decode=True).decode(
- textPart.get_content_charset()
- )
- htmlText = htmlPart.get_payload(decode=True).decode(
- htmlPart.get_content_charset()
- )
-
- self.assertIn(u"Us\u00e9r One", plainText)
- self.assertIn(u'<a href="mailto:user01 at localhost">Us\u00e9r One</a>',
- htmlText)
-
- # The same assertion, but with the organizer's form.
- self.assertIn(
- u'<a href="mailto:user01 at localhost">User Z\u00e9ro One</a>',
- htmlText)
-
-
- def test_emailQuoting(self):
- """
- L{MailHandler.generateEmail} will HTML-quote all relevant fields in the
- HTML part, but not the text/plain part.
- """
- msgID, message = self.generateSampleEmail()
- htmlPart = partByType(message, "text/html").get_payload(decode=True)
- plainPart = partByType(message, "text/plain").get_payload(decode=True)
- expectedPlain = 'awesome description with "<" and "&"'
- expectedHTML = expectedPlain.replace("&", "&").replace("<", "<")
-
- self.assertIn(expectedPlain, plainPart)
- self.assertIn(expectedHTML, htmlPart)
-
-
- def test_stringFormatTemplateLoader(self):
- """
- L{StringFormatTemplateLoader.load} will convert a template with
- C{%(x)s}-format slots by converting it to a template with C{<t:slot
- name="x" />} slots, and a renderer on the document element named
- according to the constructor argument.
- """
- class StubElement(Element):
- loader = StringFormatTemplateLoader(
- lambda : StringIO(
- "<test><alpha>%(slot1)s</alpha>%(other)s</test>"
- ),
- "testRenderHere"
- )
-
- @renderer
- def testRenderHere(self, request, tag):
- return tag.fillSlots(slot1="hello",
- other="world")
- result = []
- flattenString(None, StubElement()).addCallback(result.append)
- self.assertEquals(result,
- ["<test><alpha>hello</alpha>world</test>"])
-
-
- def test_templateLoaderWithAttributes(self):
- """
- L{StringFormatTemplateLoader.load} will convert a template with
- C{%(x)s}-format slots inside attributes into t:attr elements containing
- t:slot slots.
- """
- class StubElement(Element):
- loader = StringFormatTemplateLoader(
- lambda : StringIO(
- '<test><alpha beta="before %(slot1)s after">inner</alpha>'
- '%(other)s</test>'
- ),
- "testRenderHere"
- )
-
- @renderer
- def testRenderHere(self, request, tag):
- return tag.fillSlots(slot1="hello",
- other="world")
- result = []
- flattenString(None, StubElement()).addCallback(result.append)
- self.assertEquals(result,
- ['<test><alpha beta="before hello after">'
- 'inner</alpha>world</test>'])
-
-
- def test_templateLoaderTagSoup(self):
- """
- L{StringFormatTemplateLoader.load} will convert a template with
- C{%(x)s}-format slots into t:slot slots, and render a well-formed output
- document, even if the input is malformed (i.e. missing necessary closing
- tags).
- """
- class StubElement(Element):
- loader = StringFormatTemplateLoader(
- lambda : StringIO(
- '<test><alpha beta="before %(slot1)s after">inner</alpha>'
- '%(other)s'
- ),
- "testRenderHere"
- )
-
- @renderer
- def testRenderHere(self, request, tag):
- return tag.fillSlots(slot1="hello",
- other="world")
- result = []
- flattenString(None, StubElement()).addCallback(result.append)
- self.assertEquals(result,
- ['<test><alpha beta="before hello after">'
- 'inner</alpha>world</test>'])
-
-
-def partByType(message, contentType):
- """
- Retrieve a MIME part from an L{email.message.Message} based on a content
- type.
- """
- for part in message.walk():
- if part.get_content_type() == contentType:
- return part
- raise KeyError(contentType)
-
-
-
-class MailGatewayTokensDatabaseTests(TestCase):
-
- def setUp(self):
- TestCase.setUp(self)
- self.db = MailGatewayTokensDatabase(":memory:")
-
-
- def test_tokens(self):
- self.assertEquals(self.db.lookupByToken("xyzzy"), None)
-
- token = self.db.createToken("organizer", "attendee", "icaluid")
- self.assertEquals(self.db.getToken("organizer", "attendee", "icaluid"),
- token)
- self.assertEquals(self.db.lookupByToken(token),
- ("organizer", "attendee", "icaluid"))
- self.db.deleteToken(token)
- self.assertEquals(self.db.lookupByToken(token), None)
-
-
-serverData = """<?xml version="1.0" encoding="utf-8"?>
-<servers>
- <server>
- <id>00001</id>
- <uri>http://caldav1.example.com:8008</uri>
- <allowed-from>127.0.0.1</allowed-from>
- <shared-secret>foobar</shared-secret>
- </server>
- <server>
- <id>00002</id>
- <uri>https://caldav2.example.com:8843</uri>
- <partitions>
- <partition>
- <id>A</id>
- <uri>https://machine1.example.com:8443</uri>
- </partition>
- <partition>
- <id>B</id>
- <uri>https://machine2.example.com:8443</uri>
- </partition>
- </partitions>
- </server>
-</servers>
-"""
-
-
Deleted: CalendarServer/trunk/twistedcaldav/test/test_schedule.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_schedule.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/test/test_schedule.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,502 +0,0 @@
-##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.web2 import responsecode, http_headers
-from txdav.xml import element as davxml
-from twext.web2.dav.util import davXMLFromStream
-from twext.web2.http import HTTPError
-from twext.web2.iweb import IResponse
-from twext.web2.stream import MemoryStream
-from twext.web2.test.test_server import SimpleRequest
-
-from twisted.internet.defer import inlineCallbacks
-
-from twistedcaldav import caldavxml, customxml
-from twistedcaldav.config import config
-from twistedcaldav.memcachelock import MemcacheLock
-from twistedcaldav.schedule import IScheduleInboxResource
-from twistedcaldav.test.util import HomeTestCase, TestCase
-
-class Properties (HomeTestCase):
- """
- CalDAV properties
- """
- def test_free_busy_set_prop(self):
- """
- Test for PROPFIND on Inbox with missing calendar-free-busy-set property.
- """
-
- inbox_uri = "/inbox/"
-
- def propfind_cb(response):
- response = IResponse(response)
-
- if response.code != responsecode.MULTI_STATUS:
- self.fail("Incorrect response to PROPFIND: %s" % (response.code,))
-
- def got_xml(doc):
- if not isinstance(doc.root_element, davxml.MultiStatus):
- self.fail("PROPFIND response XML root element is not multistatus: %r" % (doc.root_element,))
-
- response = doc.root_element.childOfType(davxml.Response)
- href = response.childOfType(davxml.HRef)
- self.failUnless(str(href) == inbox_uri)
-
- for propstat in response.childrenOfType(davxml.PropertyStatus):
- status = propstat.childOfType(davxml.Status)
- if status.code != responsecode.OK:
- self.fail("Unable to read requested properties (%s): %r"
- % (status, propstat.childOfType(davxml.PropertyContainer).toxml()))
-
- container = propstat.childOfType(davxml.PropertyContainer)
-
- #
- # Check CalDAV:calendar-free-busy-set
- #
-
- free_busy_set = container.childOfType(caldavxml.CalendarFreeBusySet)
- if not free_busy_set:
- self.fail("Expected CalDAV:calendar-free-busy-set element; but got none.")
-
- if not free_busy_set.children:
- self.fail("Expected non-empty CalDAV:calendar-free-busy-set element.")
-
- return davXMLFromStream(response.stream).addCallback(got_xml)
-
- query = davxml.PropertyFind(
- davxml.PropertyContainer(
- caldavxml.CalendarFreeBusySet(),
- ),
- )
-
- request = SimpleRequest(
- self.site,
- "PROPFIND",
- inbox_uri,
- headers=http_headers.Headers({"Depth":"0"}),
- )
- request.stream = MemoryStream(query.toxml())
- return self.send(request, propfind_cb)
-
- @inlineCallbacks
- def test_free_busy_set_remove_broken(self):
- """
- ???
- """
-
- request = SimpleRequest(self.site, "GET", "/inbox/")
- inbox = yield request.locateResource("/inbox/")
- self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
- oldfbset = set(("/calendar",))
- oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
-
- newfbset = set()
- newfbset.update(oldfbset)
- newfbset.add("/calendar-broken")
- newset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in newfbset])
-
- inbox.writeDeadProperty(newset)
- changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
- self.assertEqual(tuple(changedset.children), tuple(newset.children))
-
- yield inbox.writeProperty(newset, request)
-
- changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
- self.assertEqual(tuple(changedset.children), tuple(oldset.children))
-
- @inlineCallbacks
- def test_free_busy_set_strip_slash(self):
- """
- ???
- """
-
- request = SimpleRequest(self.site, "GET", "/inbox/")
- inbox = yield request.locateResource("/inbox/")
- self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
-
- oldfbset = set(("/calendar/",))
- oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
- inbox.writeDeadProperty(oldset)
-
- writefbset = set(("/calendar/",))
- writeset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in writefbset])
- yield inbox.writeProperty(writeset, request)
-
- correctfbset = set(("/calendar",))
- correctset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in correctfbset])
- changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
- self.assertEqual(tuple(changedset.children), tuple(correctset.children))
-
- @inlineCallbacks
- def test_free_busy_set_strip_slash_remove(self):
- """
- ???
- """
-
- request = SimpleRequest(self.site, "GET", "/inbox/")
- inbox = yield request.locateResource("/inbox/")
- self.assertTrue(inbox.hasDeadProperty(caldavxml.CalendarFreeBusySet))
-
- oldfbset = set(("/calendar/", "/broken/"))
- oldset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in oldfbset])
- inbox.writeDeadProperty(oldset)
-
- writefbset = set(("/calendar/", "/broken/"))
- writeset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in writefbset])
- yield inbox.writeProperty(writeset, request)
-
- correctfbset = set(("/calendar",))
- correctset = caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in correctfbset])
- changedset = inbox.readDeadProperty(caldavxml.CalendarFreeBusySet)
- self.assertEqual(tuple(changedset.children), tuple(correctset.children))
-
-class DefaultCalendar (TestCase):
-
- def setUp(self):
- super(DefaultCalendar, self).setUp()
- self.createStockDirectoryService()
- self.setupCalendars()
-
- @inlineCallbacks
- def test_pick_default_vevent_calendar(self):
- """
- Test that pickNewDefaultCalendar will choose the correct calendar.
- """
-
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
- # default property initially not present
- try:
- inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
- except HTTPError:
- pass
- else:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
- yield inbox.pickNewDefaultCalendar(request)
-
- try:
- default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
- except HTTPError:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
- else:
- self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
-
- request._newStoreTransaction.abort()
-
- @inlineCallbacks
- def test_pick_default_vtodo_calendar(self):
- """
- Test that pickNewDefaultCalendar will choose the correct tasks calendar.
- """
-
-
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
- # default property initially not present
- try:
- inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
- except HTTPError:
- pass
- else:
- self.fail("customxml.ScheduleDefaultTasksURL is not empty")
-
- yield inbox.pickNewDefaultCalendar(request, tasks=True)
-
- try:
- default = inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
- except HTTPError:
- self.fail("customxml.ScheduleDefaultTasksURL is not present")
- else:
- self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/tasks")
-
- request._newStoreTransaction.abort()
-
- @inlineCallbacks
- def test_missing_default_vevent_calendar(self):
- """
- Test that pickNewDefaultCalendar will create a missing default calendar.
- """
-
-
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- home = yield request.locateResource("/calendars/users/wsanchez/")
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
- # default property initially not present
- try:
- inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
- except HTTPError:
- pass
- else:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
- # Forcibly remove the one we need
- yield home._newStoreHome.removeChildWithName("calendar")
- names = [calendarName for calendarName in (yield home._newStoreHome.listCalendars())]
- self.assertTrue("calendar" not in names)
-
- yield inbox.pickNewDefaultCalendar(request)
-
- try:
- default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
- except HTTPError:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
- else:
- self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
-
- request._newStoreTransaction.abort()
-
- @inlineCallbacks
- def test_missing_default_vtodo_calendar(self):
- """
- Test that pickNewDefaultCalendar will create a missing default tasks calendar.
- """
-
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- home = yield request.locateResource("/calendars/users/wsanchez/")
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
- # default property initially not present
- try:
- inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
- except HTTPError:
- pass
- else:
- self.fail("caldavxml.ScheduleDefaultTasksURL is not empty")
-
- # Forcibly remove the one we need
- yield home._newStoreHome.removeChildWithName("tasks")
- names = [calendarName for calendarName in (yield home._newStoreHome.listCalendars())]
- self.assertTrue("tasks" not in names)
-
- yield inbox.pickNewDefaultCalendar(request, tasks=True)
-
- try:
- default = inbox.readDeadProperty(customxml.ScheduleDefaultTasksURL)
- except HTTPError:
- self.fail("caldavxml.ScheduleDefaultTasksURL is not present")
- else:
- self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/tasks")
-
- request._newStoreTransaction.abort()
-
- @inlineCallbacks
- def test_pick_default_other(self):
- """
- Make calendar
- """
-
-
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
- # default property not present
- try:
- inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
- except HTTPError:
- pass
- else:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
- # Create a new default calendar
- newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
- yield newcalendar.createCalendarCollection()
- inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(
- davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
- ))
-
- # Delete the normal calendar
- calendar = yield request.locateResource("/calendars/users/wsanchez/calendar")
- yield calendar.storeRemove(request, False, "/calendars/users/wsanchez/calendar")
-
- inbox.removeDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
-
- # default property not present
- try:
- inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
- except HTTPError:
- pass
- else:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
- request._newStoreTransaction.commit()
-
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
- yield inbox.pickNewDefaultCalendar(request)
-
- try:
- default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
- except HTTPError:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
- else:
- self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-
- request._newStoreTransaction.abort()
-
- @inlineCallbacks
- def test_fix_shared_default(self):
- """
- Make calendar
- """
-
-
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
- # Create a new default calendar
- newcalendar = yield request.locateResource("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
- yield newcalendar.createCalendarCollection()
- inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(
- davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
- ))
- try:
- default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)
- except HTTPError:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
- else:
- self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-
- # Force the new calendar to think it is a sharee collection
- newcalendar._isShareeCollection = True
-
- try:
- default = yield inbox.readProperty(caldavxml.ScheduleDefaultCalendarURL, request)
- except HTTPError:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
- else:
- self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar")
-
- request._newStoreTransaction.abort()
-
- @inlineCallbacks
- def test_set_default_vevent_other(self):
- """
- Test that the default URL can be set to another VEVENT calendar
- """
-
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
-
- # default property not present
- try:
- inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
- except HTTPError:
- pass
- else:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not empty")
-
- # Create a new default calendar
- newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
- yield newcalendar.createCalendarCollection()
- yield newcalendar.setSupportedComponents(("VEVENT",))
- request._newStoreTransaction.commit()
-
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
- yield inbox.writeProperty(caldavxml.ScheduleDefaultCalendarURL(davxml.HRef("/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")), request)
-
- try:
- default = inbox.readDeadProperty(caldavxml.ScheduleDefaultCalendarURL)
- except HTTPError:
- self.fail("caldavxml.ScheduleDefaultCalendarURL is not present")
- else:
- self.assertEqual(str(default.children[0]), "/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newcalendar")
-
- request._newStoreTransaction.commit()
-
- @inlineCallbacks
- def test_is_default_calendar(self):
- """
- Test .isDefaultCalendar() returns the proper class or None.
- """
-
- # Create a new non-default calendar
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
- yield newcalendar.createCalendarCollection()
- yield newcalendar.setSupportedComponents(("VEVENT",))
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
- yield inbox.pickNewDefaultCalendar(request)
- request._newStoreTransaction.commit()
-
- request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
- inbox = yield request.locateResource("/calendars/users/wsanchez/inbox")
- calendar = yield request.locateResource("/calendars/users/wsanchez/calendar")
- newcalendar = yield request.locateResource("/calendars/users/wsanchez/newcalendar")
- tasks = yield request.locateResource("/calendars/users/wsanchez/tasks")
-
- result = yield inbox.isDefaultCalendar(request, calendar)
- self.assertEqual(result, caldavxml.ScheduleDefaultCalendarURL)
-
- result = yield inbox.isDefaultCalendar(request, newcalendar)
- self.assertEqual(result, None)
-
- result = yield inbox.isDefaultCalendar(request, tasks)
- self.assertEqual(result, customxml.ScheduleDefaultTasksURL)
-
- request._newStoreTransaction.commit()
-
-class iSchedulePOST (TestCase):
-
- def setUp(self):
- super(iSchedulePOST, self).setUp()
- self.createStockDirectoryService()
- self.setupCalendars()
- self.site.resource.putChild("ischedule", IScheduleInboxResource(self.site.resource, self._newStore))
-
- @inlineCallbacks
- def test_deadlock(self):
- """
- Make calendar
- """
-
- request = SimpleRequest(
- self.site,
- "POST",
- "/ischedule",
- headers=http_headers.Headers(rawHeaders={
- "Originator": ("mailto:wsanchez at example.com",),
- "Recipient": ("mailto:cdaboo at example.com",),
- }),
- content="""BEGIN:VCALENDAR
-CALSCALE:GREGORIAN
-PRODID:-//Example Inc.//Example Calendar//EN
-VERSION:2.0
-BEGIN:VEVENT
-DTSTAMP:20051222T205953Z
-CREATED:20060101T150000Z
-DTSTART:20060101T100000Z
-DURATION:PT1H
-SUMMARY:event 1
-UID:deadlocked
-ORGANIZER:mailto:wsanchez at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:wsanchez at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:cdaboo at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
- )
-
- # Lock the UID here to force a deadlock - but adjust the timeout so the test does not wait too long
- self.patch(config.Scheduling.Options, "UIDLockTimeoutSeconds", 1)
- lock = MemcacheLock("ImplicitUIDLock", "deadlocked", timeout=60, expire_time=60)
- yield lock.acquire()
-
- response = (yield self.send(request))
- self.assertEqual(response.code, responsecode.CONFLICT)
Deleted: CalendarServer/trunk/twistedcaldav/test/test_servers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_servers.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/test/test_servers.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,179 +0,0 @@
-##
-# Copyright (c) 2009-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twext.web2.test.test_server import SimpleRequest
-from twistedcaldav.config import config
-from twistedcaldav.servers import Servers, SERVER_SECRET_HEADER
-from twistedcaldav.test.util import TestCase
-import StringIO as StringIO
-
-class ServerTests(TestCase):
-
- data1 = """<?xml version="1.0" encoding="utf-8"?>
-<servers>
- <server>
- <id>00001</id>
- <uri>http://caldav1.example.com:8008</uri>
- <allowed-from>127.0.0.1</allowed-from>
- <shared-secret>foobar</shared-secret>
- </server>
- <server>
- <id>00002</id>
- <uri>https://caldav2.example.com:8843</uri>
- <partitions>
- <partition>
- <id>A</id>
- <uri>https://machine1.example.com:8443</uri>
- </partition>
- <partition>
- <id>B</id>
- <uri>https://machine2.example.com:8443</uri>
- </partition>
- </partitions>
- </server>
-</servers>
-"""
-
- data2 = """<?xml version="1.0" encoding="utf-8"?>
-<servers>
- <server>
- <id>00001</id>
- <uri>http://caldav1.example.com:8008</uri>
- <allowed-from>localhost</allowed-from>
- <shared-secret>foobar</shared-secret>
- </server>
- <server>
- <id>00002</id>
- <uri>https://caldav2.example.com:8843</uri>
- <partitions>
- <partition>
- <id>A</id>
- <uri>https://machine1.example.com:8443</uri>
- </partition>
- <partition>
- <id>B</id>
- <uri>https://machine2.example.com:8443</uri>
- </partition>
- </partitions>
- </server>
-</servers>
-"""
-
- def _setupServers(self, data=data1):
- self.patch(config, "ServerHostName", "caldav1.example.com")
- self.patch(config, "HTTPPort", 8008)
-
- xmlFile = StringIO.StringIO(data)
- servers = Servers
- servers.load(xmlFile, ignoreIPLookupFailures=True)
-
- return servers
-
- def test_read_ok(self):
-
- servers = self._setupServers()
-
- self.assertTrue(servers.getServerById("00001") is not None)
- self.assertTrue(servers.getServerById("00002") is not None)
-
- self.assertEqual(servers.getServerById("00001").uri, "http://caldav1.example.com:8008")
- self.assertEqual(servers.getServerById("00002").uri, "https://caldav2.example.com:8843")
-
- self.assertEqual(servers.getServerById("00001").allowed_from_ips, set(("127.0.0.1",)))
- self.assertEqual(servers.getServerById("00002").allowed_from_ips, set())
-
- self.assertEqual(servers.getServerById("00001").shared_secret, "foobar")
- self.assertEqual(servers.getServerById("00002").shared_secret, None)
-
- self.assertEqual(len(servers.getServerById("00001").partitions), 0)
- self.assertEqual(len(servers.getServerById("00002").partitions), 2)
-
- self.assertEqual(servers.getServerById("00002").getPartitionURIForId("A"), "https://machine1.example.com:8443")
- self.assertEqual(servers.getServerById("00002").getPartitionURIForId("B"), "https://machine2.example.com:8443")
-
- def test_this_server(self):
-
- servers = self._setupServers()
-
- self.assertTrue(servers.getServerById("00001").thisServer)
- self.assertFalse(servers.getServerById("00002").thisServer)
-
- self.patch(config, "ServerHostName", "caldav2.example.com")
- self.patch(config, "SSLPort", 8443)
- self.patch(config, "BindSSLPorts", [8843])
-
- xmlFile = StringIO.StringIO(ServerTests.data1)
- servers = Servers
- servers.load(xmlFile, ignoreIPLookupFailures=True)
-
- self.assertFalse(servers.getServerById("00001").thisServer)
- self.assertTrue(servers.getServerById("00002").thisServer)
-
- def test_check_is_partitioned(self):
-
- servers = self._setupServers()
-
- self.assertFalse(servers.getServerById("00001").isPartitioned())
- self.assertTrue(servers.getServerById("00002").isPartitioned())
-
- def test_check_this_ip(self):
-
- servers = self._setupServers()
- servers.getServerById("00001").ips = set(("127.0.0.2",))
- servers.getServerById("00002").ips = set(("127.0.0.3",))
-
- self.assertTrue(servers.getServerById("00001").checkThisIP("127.0.0.2"))
- self.assertFalse(servers.getServerById("00001").checkThisIP("127.0.0.3"))
-
- def test_check_allowed_from(self):
-
- for servers in (self._setupServers(), self._setupServers(data=self.data2),):
- self.assertTrue(servers.getServerById("00001").hasAllowedFromIP())
- self.assertFalse(servers.getServerById("00002").hasAllowedFromIP())
-
- self.assertTrue(servers.getServerById("00001").checkAllowedFromIP("127.0.0.1"))
- self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.2"))
- self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.3"))
- self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.1"))
- self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.2"))
- self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.3"))
-
- def test_check_shared_secret(self):
-
- servers = self._setupServers()
-
- request = SimpleRequest(None, "POST", "/ischedule")
- request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
- self.assertTrue(servers.getServerById("00001").checkSharedSecret(request))
-
- request = SimpleRequest(None, "POST", "/ischedule")
- request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
- self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
-
- request = SimpleRequest(None, "POST", "/ischedule")
- self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
-
- request = SimpleRequest(None, "POST", "/ischedule")
- request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
- self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
-
- request = SimpleRequest(None, "POST", "/ischedule")
- request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
- self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
-
- request = SimpleRequest(None, "POST", "/ischedule")
- self.assertTrue(servers.getServerById("00002").checkSharedSecret(request))
-
Modified: CalendarServer/trunk/twistedcaldav/test/test_upgrade.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_upgrade.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/test/test_upgrade.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -15,7 +15,9 @@
##
import hashlib
-import os, zlib, cPickle
+import os
+import zlib
+import cPickle
from twisted.python.reflect import namedClass
from twisted.internet.defer import inlineCallbacks
@@ -26,7 +28,7 @@
from twistedcaldav.config import config
from twistedcaldav.directory.xmlfile import XMLDirectoryService
from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
-from twistedcaldav.mail import MailGatewayTokensDatabase
+from twistedcaldav.scheduling.imip.mailgateway import MailGatewayTokensDatabase
from twistedcaldav.upgrade import (
xattrname, upgradeData, updateFreeBusySet,
removeIllegalCharacters, normalizeCUAddrs
@@ -51,7 +53,7 @@
class UpgradeTests(TestCase):
-
+
def setUpXMLDirectory(self):
xmlFile = os.path.join(os.path.dirname(os.path.dirname(__file__)),
"directory", "test", "accounts.xml")
@@ -80,13 +82,13 @@
self.setUpOldDocRoot()
self.setUpOldDocRootWithoutDB()
self.setUpNewDocRoot()
-
+
self.setUpNewDataRoot()
self.setUpDataRootWithProxyDB()
def setUpOldDocRoot(self):
-
+
# Set up doc root
self.olddocroot = os.path.abspath(self.mktemp())
os.mkdir(self.olddocroot)
@@ -104,7 +106,7 @@
def setUpOldDocRootWithoutDB(self):
-
+
# Set up doc root
self.olddocrootnodb = os.path.abspath(self.mktemp())
os.mkdir(self.olddocrootnodb)
@@ -119,22 +121,25 @@
os.mkdir(os.path.join(principals, "sudoers"))
os.mkdir(os.path.join(self.olddocrootnodb, "calendars"))
+
def setUpNewDocRoot(self):
-
+
# Set up doc root
self.newdocroot = os.path.abspath(self.mktemp())
os.mkdir(self.newdocroot)
os.mkdir(os.path.join(self.newdocroot, "calendars"))
+
def setUpNewDataRoot(self):
-
+
# Set up data root
self.newdataroot = os.path.abspath(self.mktemp())
os.mkdir(self.newdataroot)
+
def setUpDataRootWithProxyDB(self):
-
+
# Set up data root
self.existingdataroot = os.path.abspath(self.mktemp())
os.mkdir(self.existingdataroot)
@@ -156,7 +161,6 @@
config.DocumentRoot = self.olddocroot
config.DataRoot = self.newdataroot
-
# Check pre-conditions
self.assertTrue(os.path.exists(os.path.join(config.DocumentRoot, "principals")))
self.assertTrue(os.path.isdir(os.path.join(config.DocumentRoot, "principals")))
@@ -164,7 +168,7 @@
self.assertFalse(os.path.exists(os.path.join(config.DataRoot, NEWPROXYFILE)))
(yield self.doUpgrade(config))
-
+
# Check post-conditions
self.assertFalse(os.path.exists(os.path.join(config.DocumentRoot, "principals",)))
self.assertTrue(os.path.exists(os.path.join(config.DataRoot, NEWPROXYFILE)))
@@ -180,13 +184,13 @@
config.DocumentRoot = self.newdocroot
config.DataRoot = self.existingdataroot
-
+
# Check pre-conditions
self.assertFalse(os.path.exists(os.path.join(config.DocumentRoot, "principals")))
self.assertTrue(os.path.exists(os.path.join(config.DataRoot, NEWPROXYFILE)))
(yield self.doUpgrade(config))
-
+
# Check post-conditions
self.assertFalse(os.path.exists(os.path.join(config.DocumentRoot, "principals",)))
self.assertTrue(os.path.exists(os.path.join(config.DataRoot, NEWPROXYFILE)))
@@ -219,11 +223,9 @@
value = cPickle.dumps(doc.root_element)
self.assertEquals(updateFreeBusySet(value, directory), None)
-
#
# Verify these values do require updating:
#
-
expected = "<?xml version='1.0' encoding='UTF-8'?>\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar/</href>\r\n</calendar-free-busy-set>"
# Uncompressed XML
@@ -247,11 +249,9 @@
newValue = zlib.decompress(newValue)
self.assertEquals(newValue, expected)
-
#
# Shortname not in directory, return empty string
#
-
expected = "<?xml version='1.0' encoding='UTF-8'?>\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>"
value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/users/nonexistent/calendar</href>\r\n</calendar-free-busy-set>\r\n"
newValue = updateFreeBusySet(value, directory)
@@ -309,7 +309,7 @@
},
"notifications": {
"sample-notification.xml": {
- "@contents": "<?xml version='1.0'>\n<should-be-ignored />"
+ "@contents": "<?xml version='1.0'>\n<should-be-ignored />"
}
}
}
@@ -699,7 +699,7 @@
{
"@contents" : "",
},
- "__uids__" :ignoredUIDContents,
+ "__uids__" : ignoredUIDContents,
},
"principals" :
{
@@ -810,7 +810,7 @@
{
"@contents" : "",
},
- "__uids__" :beforeUIDContents,
+ "__uids__" : beforeUIDContents,
},
"principals" :
{
@@ -1098,6 +1098,7 @@
(yield self.verifyDirectoryComparison(before, after, reverify=True))
+
@inlineCallbacks
def test_calendarsUpgradeWithNoChange(self):
"""
@@ -1226,7 +1227,6 @@
(yield self.verifyDirectoryComparison(before, after))
-
@inlineCallbacks
def test_calendarsUpgradeWithInboxItems(self):
"""
@@ -1375,7 +1375,6 @@
}
}
-
after = {
".calendarserver_version" :
{
@@ -1425,7 +1424,6 @@
},
}
-
root = self.createHierarchy(before)
config.DocumentRoot = root
@@ -1435,6 +1433,7 @@
self.assertTrue(self.verifyHierarchy(root, after))
+
@inlineCallbacks
def test_migrateResourceInfo(self):
# Fake getResourceInfo( )
@@ -1449,7 +1448,7 @@
def _getResourceInfo(ignored):
results = []
for guid, info in assignments.iteritems():
- results.append( (guid, info[0], info[1], info[2]) )
+ results.append((guid, info[0], info[1], info[2]))
return results
self.setUpInitialStates()
@@ -1522,7 +1521,6 @@
self.assertEquals(info[0], autoSchedule)
-
def test_removeIllegalCharacters(self):
"""
Control characters aside from NL and CR are removed.
@@ -1712,6 +1710,7 @@
return False
+
class ParallelUpgradeTests(UpgradeTests):
"""
Tests for upgradeData in parallel.
@@ -1721,4 +1720,3 @@
from txdav.common.datastore.upgrade.test.test_migrate import StubSpawner
spawner = StubSpawner(config)
return upgradeData(config, spawner, 2)
-
Modified: CalendarServer/trunk/twistedcaldav/timezonexml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/timezonexml.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/timezonexml.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -18,8 +18,8 @@
This module provides XML definitions for use with Timezone Standard Service.
"""
-from txdav.xml import element as davxml
from txdav.xml.element import registerElement
+from txdav.xml.element import WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
##
@@ -30,7 +30,7 @@
@registerElement
-class Capabilities (davxml.WebDAVElement):
+class Capabilities (WebDAVElement):
namespace = timezone_namespace
name = "capabilities"
allowed_children = {
@@ -38,8 +38,9 @@
}
+
@registerElement
-class Operation (davxml.WebDAVElement):
+class Operation (WebDAVElement):
namespace = timezone_namespace
name = "operation"
allowed_children = {
@@ -49,20 +50,23 @@
}
+
@registerElement
-class Action (davxml.WebDAVTextElement):
+class Action (WebDAVTextElement):
namespace = timezone_namespace
name = "action"
+
@registerElement
-class Description (davxml.WebDAVTextElement):
+class Description (WebDAVTextElement):
namespace = timezone_namespace
name = "description"
+
@registerElement
-class AcceptParameter (davxml.WebDAVElement):
+class AcceptParameter (WebDAVElement):
namespace = timezone_namespace
name = "accept-parameter"
allowed_children = {
@@ -74,32 +78,37 @@
}
+
@registerElement
-class Name (davxml.WebDAVTextElement):
+class Name (WebDAVTextElement):
namespace = timezone_namespace
name = "name"
+
@registerElement
-class Required (davxml.WebDAVTextElement):
+class Required (WebDAVTextElement):
namespace = timezone_namespace
name = "required"
+
@registerElement
-class Multi (davxml.WebDAVTextElement):
+class Multi (WebDAVTextElement):
namespace = timezone_namespace
name = "multi"
+
@registerElement
-class Value (davxml.WebDAVTextElement):
+class Value (WebDAVTextElement):
namespace = timezone_namespace
name = "value"
+
@registerElement
-class TimezoneList (davxml.WebDAVElement):
+class TimezoneList (WebDAVElement):
namespace = timezone_namespace
name = "timezone-list"
allowed_children = {
@@ -108,14 +117,16 @@
}
+
@registerElement
-class Dtstamp (davxml.WebDAVTextElement):
+class Dtstamp (WebDAVTextElement):
namespace = timezone_namespace
name = "dtstamp"
+
@registerElement
-class Summary (davxml.WebDAVElement):
+class Summary (WebDAVElement):
namespace = timezone_namespace
name = "summary"
allowed_children = {
@@ -127,38 +138,44 @@
}
+
@registerElement
-class Tzid (davxml.WebDAVTextElement):
+class Tzid (WebDAVTextElement):
namespace = timezone_namespace
name = "tzid"
+
@registerElement
-class LastModified (davxml.WebDAVTextElement):
+class LastModified (WebDAVTextElement):
namespace = timezone_namespace
name = "last-modified"
+
@registerElement
-class LocalName (davxml.WebDAVTextElement):
+class LocalName (WebDAVTextElement):
namespace = timezone_namespace
name = "local-name"
+
@registerElement
-class Alias (davxml.WebDAVTextElement):
+class Alias (WebDAVTextElement):
namespace = timezone_namespace
name = "alias"
+
@registerElement
-class Inactive (davxml.WebDAVEmptyElement):
+class Inactive (WebDAVEmptyElement):
namespace = timezone_namespace
name = "inactive"
+
@registerElement
-class Timezones (davxml.WebDAVElement):
+class Timezones (WebDAVElement):
namespace = timezone_namespace
name = "timezones"
allowed_children = {
@@ -167,8 +184,9 @@
}
+
@registerElement
-class Tzdata (davxml.WebDAVElement):
+class Tzdata (WebDAVElement):
namespace = timezone_namespace
name = "tzdata"
allowed_children = {
@@ -178,14 +196,16 @@
}
+
@registerElement
-class Calscale (davxml.WebDAVTextElement):
+class Calscale (WebDAVTextElement):
namespace = timezone_namespace
name = "calscale"
+
@registerElement
-class Observance (davxml.WebDAVElement):
+class Observance (WebDAVElement):
namespace = timezone_namespace
name = "observance"
allowed_children = {
@@ -197,19 +217,22 @@
}
+
@registerElement
-class Onset (davxml.WebDAVTextElement):
+class Onset (WebDAVTextElement):
namespace = timezone_namespace
name = "onset"
+
@registerElement
-class UTCOffsetFrom (davxml.WebDAVTextElement):
+class UTCOffsetFrom (WebDAVTextElement):
namespace = timezone_namespace
name = "utc-offset-from"
+
@registerElement
-class UTCOffsetTo (davxml.WebDAVTextElement):
+class UTCOffsetTo (WebDAVTextElement):
namespace = timezone_namespace
name = "utc-offset-to"
Modified: CalendarServer/trunk/twistedcaldav/upgrade.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/upgrade.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/twistedcaldav/upgrade.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -17,7 +17,17 @@
from __future__ import with_statement
-import xattr, os, zlib, hashlib, datetime, pwd, grp, shutil, errno, operator, time
+import xattr
+import os
+import zlib
+import hashlib
+import datetime
+import pwd
+import grp
+import shutil
+import errno
+import operator
+import time
from zlib import compress
from cPickle import loads as unpickle, UnpicklingError
@@ -34,8 +44,8 @@
from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
from twistedcaldav.directory.xmlfile import XMLDirectoryService
from twistedcaldav.ical import Component
-from twistedcaldav.mail import MailGatewayTokensDatabase
from twistedcaldav.scheduling.cuaddress import LocalCalendarUser
+from twistedcaldav.scheduling.imip.mailgateway import MailGatewayTokensDatabase
from twistedcaldav.scheduling.scheduler import DirectScheduler
from twistedcaldav.util import normalizationLookup
@@ -69,6 +79,8 @@
def xattrname(n):
return deadPropertyXattrPrefix + n
+
+
def getCalendarServerIDs(config):
# Determine uid/gid for ownership of directories we create here
@@ -89,6 +101,7 @@
return uid, gid
+
def fixBadQuotes(data):
if (
data.find('\\"') != -1 or
@@ -182,7 +195,6 @@
collectionUpdated = True
-
if collectionUpdated:
ctagValue = "<?xml version='1.0' encoding='UTF-8'?>\r\n<getctag xmlns='http://calendarserver.org/ns/'>%s</getctag>\r\n" % (str(datetime.datetime.now()),)
ctagValue = zlib.compress(ctagValue)
@@ -295,6 +307,7 @@
log.err(f)
errorOccurred.append(True)
+
def doProxyDatabaseMoveUpgrade(config, uid=-1, gid=-1):
# See if the new one is already present
oldFilename = ".db.calendaruserproxy"
@@ -403,8 +416,6 @@
if os.path.exists(journalPath):
os.chown(journalPath, uid, gid)
-
-
cuaCache = {}
docRoot = config.DocumentRoot
@@ -480,7 +491,6 @@
os.rmdir(dirPath)
-
# Count how many calendar homes we'll be processing, and build
# list of pending inbox items
total = 0
@@ -522,7 +532,7 @@
spawner.startService()
parallelizer = Parallelizer((yield gatherResults(
[spawner.spawnWithConfig(config, To1Driver(), To1Home)
- for x in xrange(parallel)]
+ for _ignore_x in xrange(parallel)]
)))
log.warn("Processing %d calendar homes in %s" % (total, uidHomes))
@@ -576,6 +586,7 @@
log.warn("Data upgrade encountered errors but will proceed; see error.log for details")
+
def normalizeCUAddrs(data, directory, cuaCache):
"""
Normalize calendar user addresses to urn:uuid: form.
@@ -598,7 +609,7 @@
def lookupFunction(cuaddr, principalFunction, config):
# Return cached results, if any.
- if cuaCache.has_key(cuaddr):
+ if cuaddr in cuaCache:
return cuaCache[cuaddr]
result = normalizationLookup(cuaddr, principalFunction, config)
@@ -617,8 +628,6 @@
@inlineCallbacks
def upgrade_to_2(config, spawner, parallel, directory):
-
- errorOccurred = False
def renameProxyDB():
#
@@ -626,12 +635,13 @@
#
oldFilename = "calendaruserproxy.sqlite"
newFilename = "proxies.sqlite"
-
+
oldDbPath = os.path.join(config.DataRoot, oldFilename)
newDbPath = os.path.join(config.DataRoot, newFilename)
if os.path.exists(oldDbPath) and not os.path.exists(newDbPath):
os.rename(oldDbPath, newDbPath)
+
def flattenHome(calHome):
log.debug("Flattening calendar home: %s" % (calHome,))
@@ -643,19 +653,19 @@
# Skip non-directories; these might have been uploaded by a
# random DAV client, they can't be calendar collections.
continue
-
+
if cal in ("dropbox",):
continue
if os.path.exists(os.path.join(calPath, db_basename)):
continue
-
+
# Commented this out because it is only needed if there are calendars nested inside of regular collections.
# Whilst this is technically possible in early versions of the servers the main clients did not support it.
# Also, the v1 upgrade does not look at nested calendars for cu-address normalization.
# However, we do still need to "ignore" regular collections in the calendar home so what we do is rename them
# with a ".collection." prefix.
# def scanCollection(collection):
-#
+#
# for child in os.listdir(collection):
# childCollection = os.path.join(collection, child)
# if os.path.isdir(childCollection):
@@ -677,15 +687,16 @@
except Exception, e:
log.error("Failed to upgrade calendar home %s: %s" % (calHome, e))
return succeed(False)
-
+
return succeed(True)
+
def flattenHomes():
"""
Make sure calendars inside regular collections are all moved to the top level.
"""
errorOccurred = False
-
+
log.debug("Flattening calendar homes")
docRoot = config.DocumentRoot
@@ -693,7 +704,7 @@
calRoot = os.path.join(docRoot, "calendars")
if os.path.exists(calRoot) and os.path.isdir(calRoot):
uidHomes = os.path.join(calRoot, "__uids__")
- if os.path.isdir(uidHomes):
+ if os.path.isdir(uidHomes):
for path1 in os.listdir(uidHomes):
uidLevel1 = os.path.join(uidHomes, path1)
if not os.path.isdir(uidLevel1):
@@ -716,9 +727,7 @@
# Move auto-schedule from resourceinfo sqlite to augments:
yield migrateAutoSchedule(config, directory)
- errorOccurred = flattenHomes()
-
- if errorOccurred:
+ if flattenHomes():
raise UpgradeError("Data upgrade failed, see error.log for details")
@@ -776,6 +785,8 @@
if os.path.exists(triggerPath):
os.remove(triggerPath)
+
+
class UpgradeError(RuntimeError):
"""
Generic upgrade error.
@@ -783,6 +794,7 @@
pass
+
#
# Utility functions
#
@@ -805,6 +817,7 @@
return newHref
+
def updateFreeBusySet(value, directory):
try:
@@ -845,6 +858,7 @@
return None # no update required
+
def makeDirsUserGroup(path, uid=-1, gid=-1):
parts = path.split("/")
if parts[0] == "": # absolute path
@@ -860,6 +874,7 @@
os.chown(path, uid, gid)
+
def archive(config, srcPath, uid, gid):
"""
Move srcPath into dataroot/archived, giving the destination a unique
@@ -889,6 +904,7 @@
os.remove(srcPath)
+
DELETECHARS = ''.join(chr(i) for i in xrange(32) if i not in (9, 10, 13))
def removeIllegalCharacters(data):
"""
@@ -898,7 +914,7 @@
the data changed.
"""
beforeLen = len(data)
- data = data.translate(None, DELETECHARS)
+ data = data.translate(None, DELETECHARS)
afterLen = len(data)
if afterLen != beforeLen:
return data, True
@@ -906,6 +922,7 @@
return data, False
+
# Deferred
def migrateFromOD(config, directory):
#
@@ -927,6 +944,7 @@
return migrateResources(userService, resourceService)
+
@inlineCallbacks
def migrateAutoSchedule(config, directory):
# Fetch the autoSchedule assignments from resourceinfo.sqlite and store
@@ -953,6 +971,7 @@
log.warn("Migrated %d auto-schedule settings" % (len(augmentRecords),))
+
class UpgradeFileSystemFormatService(Service, object):
"""
Upgrade filesystem from previous versions.
@@ -1018,6 +1037,7 @@
self.store = store
self.config = config
+
@inlineCallbacks
def startService(self):
"""
@@ -1087,7 +1107,7 @@
totalItems = len(itemsToProcess)
ignoreUUIDs = set()
for ctr, inboxItem in enumerate(itemsToProcess):
- log.info("Processing %d/%d inbox item: %s" % (ctr+1, totalItems, inboxItem,))
+ log.info("Processing %d/%d inbox item: %s" % (ctr + 1, totalItems, inboxItem,))
ignore, uuid, ignore, fileName = inboxItem.rsplit("/", 3)
if uuid in ignoreUUIDs:
@@ -1156,7 +1176,6 @@
else:
log.debug("Ignored inbox item - no inbox: %s" % (inboxItem,))
-
inboxItems.remove(inboxItem)
finally:
Modified: CalendarServer/trunk/txdav/base/datastore/subpostgres.py
===================================================================
--- CalendarServer/trunk/txdav/base/datastore/subpostgres.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/txdav/base/datastore/subpostgres.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -1,5 +1,5 @@
# -*- test-case-name: txdav.base.datastore.test.test_subpostgres -*-
-##
+# #
# Copyright (c) 2010-2012 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-##
+# #
"""
Run and manage PostgreSQL as a subprocess.
@@ -69,8 +69,9 @@
if _MAGIC_READY_COOKIE in line:
self.svc.ready()
+ disconnecting = False
- disconnecting = False
+
def connectionMade(self):
self.lineReceiver.makeConnection(self)
@@ -190,25 +191,32 @@
# completed, our stopService( ) examines the delayedShutdown flag.
# If True, we wait on the shutdownDeferred to fire before proceeding.
# The deferred gets fired once database init is complete.
- self.delayedShutdown = False # set to True when in critical code
- self.shutdownDeferred = None # the actual deferred
+ self.delayedShutdown = False # set to True when in critical code
+ self.shutdownDeferred = None # the actual deferred
# Options from config
self.databaseName = databaseName
self.logFile = logFile
- if socketDir:
- # Unix socket length path limit
- self.socketDir = CachingFilePath("%s/ccs_postgres_%s/" %
- (socketDir, md5(dataStoreDirectory.path).hexdigest()))
- if len(self.socketDir.path) > 64:
- socketDir = "/tmp"
- self.socketDir = CachingFilePath("/tmp/ccs_postgres_%s/" %
- (md5(dataStoreDirectory.path).hexdigest()))
- self.host = self.socketDir.path
+ if listenAddresses:
+ self.socketDir = None
+ self.host, self.port = listenAddresses[0].split(":") if ":" in listenAddresses[0] else (listenAddresses[0], None,)
+ self.listenAddresses = [addr.split(":")[0] for addr in listenAddresses]
else:
- self.socketDir = None
- self.host = "localhost"
- self.listenAddresses = listenAddresses
+ if socketDir:
+ # Unix socket length path limit
+ self.socketDir = CachingFilePath("%s/ccs_postgres_%s/" %
+ (socketDir, md5(dataStoreDirectory.path).hexdigest()))
+ if len(self.socketDir.path) > 64:
+ socketDir = "/tmp"
+ self.socketDir = CachingFilePath("/tmp/ccs_postgres_%s/" %
+ (md5(dataStoreDirectory.path).hexdigest()))
+ self.host = self.socketDir.path
+ self.port = None
+ else:
+ self.socketDir = None
+ self.host = "localhost"
+ self.port = None
+ self.listenAddresses = []
self.sharedBuffers = sharedBuffers if not testMode else 16
self.maxConnections = maxConnections if not testMode else 4
self.options = options
@@ -251,9 +259,14 @@
pwd.getpwuid(self.uid).pw_name)
else:
dsn = "%s:dbname=%s" % (self.host, databaseName)
- return DBAPIConnector(pgdb, postgresPreflight, dsn)
+ kwargs = {}
+ if self.port:
+ kwargs["host"] = "%s:%s" % (self.host, self.port,)
+ return DBAPIConnector(pgdb, postgresPreflight, dsn, **kwargs)
+
+
def produceConnection(self, label="<unlabeled>", databaseName=None):
"""
Produce a DB-API 2.0 connection pointed at this database.
@@ -314,17 +327,19 @@
# for pipe in self.monitor.transport.pipes.values():
# pipe.stopReading()
# pipe.stopWriting()
+ pass
def unpauseMonitor(self):
"""
Unpause monitoring.
- @see: L{pauseMonitor}
+ @see: L{pauseMonitor}
"""
# for pipe in self.monitor.transport.pipes.values():
# pipe.startReading()
# pipe.startWriting()
+ pass
def startDatabase(self):
@@ -341,6 +356,8 @@
)
if self.socketDir:
options.append("-k '%s'" % (self.socketDir.path,))
+ if self.port:
+ options.append("-c port=%s" % (self.port,))
options.append("-c shared_buffers=%d" % (self.sharedBuffers,))
options.append("-c max_connections=%d" % (self.maxConnections,))
options.append("-c standard_conforming_strings=on")
Modified: CalendarServer/trunk/txdav/caldav/datastore/util.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/util.py 2012-10-25 15:15:51 UTC (rev 9983)
+++ CalendarServer/trunk/txdav/caldav/datastore/util.py 2012-10-25 15:20:28 UTC (rev 9984)
@@ -224,7 +224,7 @@
if exists is None:
newattachment = yield outObject.createAttachmentWithName(name)
transport = newattachment.store(ctype)
- proto =_AttachmentMigrationProto(transport)
+ proto = _AttachmentMigrationProto(transport)
attachment.retrieve(proto)
yield proto.done
@@ -267,18 +267,18 @@
# usual suspects.
contentTypes.update(
{
- '.conf': 'text/plain',
- '.diff': 'text/plain',
- '.exe': 'application/x-executable',
- '.flac': 'audio/x-flac',
- '.java': 'text/plain',
- '.ogg': 'application/ogg',
- '.oz': 'text/x-oz',
- '.swf': 'application/x-shockwave-flash',
- '.tgz': 'application/x-gtar',
- '.wml': 'text/vnd.wap.wml',
- '.xul': 'application/vnd.mozilla.xul+xml',
- '.py': 'text/plain',
+ '.conf': 'text/plain',
+ '.diff': 'text/plain',
+ '.exe': 'application/x-executable',
+ '.flac': 'audio/x-flac',
+ '.java': 'text/plain',
+ '.ogg': 'application/ogg',
+ '.oz': 'text/x-oz',
+ '.swf': 'application/x-shockwave-flash',
+ '.tgz': 'application/x-gtar',
+ '.wml': 'text/vnd.wap.wml',
+ '.xul': 'application/vnd.mozilla.xul+xml',
+ '.py': 'text/plain',
'.patch': 'text/plain',
}
)
@@ -304,12 +304,15 @@
self.storeTransport = storeTransport
self.done = Deferred()
+
def connectionMade(self):
self.storeTransport.registerProducer(self.transport, False)
+
def dataReceived(self, data):
self.storeTransport.write(data)
+
@inlineCallbacks
def connectionLost(self, reason):
try:
@@ -566,14 +569,12 @@
def getHost(self):
return self
-
_everResumedProducing = False
def resumeProducing(self):
self._everResumedProducing = True
super(AttachmentRetrievalTransport, self).resumeProducing()
-
_deliveryLoop = None
def _maybeLoopDelivery(self):
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121025/4d597db6/attachment-0001.html>
More information about the calendarserver-changes
mailing list