[CalendarServer-changes] [9751] CalendarServer/branches/users/cdaboo/ischedule-dkim

source_changes at macosforge.org source_changes at macosforge.org
Wed Aug 29 13:24:22 PDT 2012


Revision: 9751
          http://trac.macosforge.org/projects/calendarserver/changeset/9751
Author:   cdaboo at apple.com
Date:     2012-08-29 13:24:20 -0700 (Wed, 29 Aug 2012)
Log Message:
-----------
Initial iSchedule-DKIM support.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/ischedule-dkim/calendarserver/tap/util.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/__init__.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/schedule.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/addressmapping.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/scheduler.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/timezonexml.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/__init__.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/delivery.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/dkim.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/servers.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_dkim.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py

Removed Paths:
-------------
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule.py
    CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischeduleservers.py

Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/calendarserver/tap/util.py	2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/calendarserver/tap/util.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -58,7 +58,7 @@
 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.resource import IScheduleInboxResource
 from twistedcaldav.simpleresource import SimpleResource, SimpleRedirectResource
 from twistedcaldav.timezones import TimezoneCache
 from twistedcaldav.timezoneservice import TimezoneServiceResource
@@ -533,6 +533,7 @@
             (config.EnableCalDAV, "caldav", "/",),
             (config.EnableCardDAV, "carddav", "/",),
             (config.TimezoneService.Enabled, "timezone", "/stdtimezones",),
+            (config.Scheduling.iSchedule.Enabled, "ischedule", "/ischedule"),
         ):
             if enabled:
                 if config.EnableSSL:

Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/__init__.py	2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/__init__.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -41,6 +41,8 @@
 import twistedcaldav.carddavxml
 import twistedcaldav.mkcolxml
 import twistedcaldav.customxml
+import twistedcaldav.timezonexml
+import twistedcaldav.scheduling.ischedule.xml
 
 twistedcaldav # Shhh.. pyflakes
 

Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/schedule.py	2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/schedule.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -30,12 +30,9 @@
 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
 
@@ -45,10 +42,9 @@
 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 CalDAVResource
 from twistedcaldav.resource import isCalendarCollectionResource
-from twistedcaldav.scheduling.scheduler import CalDAVScheduler, IScheduleScheduler
+from twistedcaldav.scheduling.scheduler import CalDAVScheduler
 
 from txdav.base.propertystore.base import PropertyName
 
@@ -488,117 +484,3 @@
         responses = [davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)) for href in multiget.resources]
         return succeed(MultiStatusResponse((responses)))
 
-class IScheduleInboxResource (ReadOnlyNoCopyResourceMixIn, DAVResource):
-    """
-    iSchedule Inbox resource.
-
-    Extends L{DAVResource} to provide iSchedule inbox functionality.
-    """
-
-    def __init__(self, parent, store):
-        """
-        @param parent: the parent resource of this one.
-        """
-        assert parent is not None
-
-        DAVResource.__init__(self, principalCollections=parent.principalCollections())
-
-        self.parent = parent
-        self._newStore = store
-
-    def deadProperties(self):
-        if not hasattr(self, "_dead_properties"):
-            self._dead_properties = NonePropertyStore(self)
-        return self._dead_properties
-
-    def etag(self):
-        return succeed(None)
-
-    def checkPreconditions(self, request):
-        return None
-
-    def resourceType(self):
-        return davxml.ResourceType.ischeduleinbox
-
-    def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8");
-
-    def isCollection(self):
-        return False
-
-    def isCalendarCollection(self):
-        return False
-
-    def isPseudoCalendarCollection(self):
-        return False
-
-    def principalForCalendarUserAddress(self, address):
-        for principalCollection in self.principalCollections():
-            principal = principalCollection.principalForCalendarUserAddress(address)
-            if principal is not None:
-                return principal
-        return None
-
-    def render(self, request):
-        output = """<html>
-<head>
-<title>Server To Server Inbox Resource</title>
-</head>
-<body>
-<h1>Server To Server Inbox Resource.</h1>
-</body
-</html>"""
-
-        response = Response(200, {}, output)
-        response.headers.setHeader("content-type", MimeType("text", "html"))
-        return response
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The server-to-server POST method.
-        """
-
-        # Check authentication and access controls
-        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
-
-        # This is a server-to-server scheduling operation.
-        scheduler = IScheduleScheduler(request, self)
-
-        # Need a transaction to work with
-        txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
-        request._newStoreTransaction = txn
-         
-        # Do the POST processing treating this as a non-local schedule
-        try:
-            result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
-        except Exception, e:
-            yield txn.abort()
-            raise e
-        else:
-            yield txn.commit()
-        returnValue(result.response())
-
-    ##
-    # ACL
-    ##
-
-    def supportedPrivileges(self, request):
-        return succeed(deliverSchedulePrivilegeSet)
-
-    def defaultAccessControlList(self):
-        privs = (
-            davxml.Privilege(davxml.Read()),
-            davxml.Privilege(caldavxml.ScheduleDeliver()),
-        )
-        if config.Scheduling.CalDAV.OldDraftCompatibility:
-            privs += (davxml.Privilege(caldavxml.Schedule()),)
-
-        return davxml.ACL(
-            # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(*privs),
-                davxml.Protected(),
-            ),
-        )

Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/addressmapping.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/addressmapping.py	2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/addressmapping.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -23,7 +23,7 @@
 from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
 from twistedcaldav.scheduling.delivery import DeliveryService
 from twistedcaldav.scheduling.imip import ScheduleViaIMip
-from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
+from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
 from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser, EmailCalendarUser, InvalidCalendarUser,\
     calendarUserFromPrincipal
 

Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/__init__.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,19 @@
+##
+# 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.
+"""

Copied: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/delivery.py (from rev 9747, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/delivery.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/delivery.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,344 @@
+##
+# 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.ischedule.servers import IScheduleServers
+from twistedcaldav.scheduling.ischedule.servers 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)
+

Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/dkim.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/dkim.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/dkim.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,545 @@
+##
+# 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
+from twext.web2.stream import MemoryStream
+from twisted.internet.defer import inlineCallbacks, returnValue
+import base64
+import collections
+import hashlib
+import rsa
+import textwrap
+import time
+
+"""
+DKIM HTTP message generation and validation,
+"""
+
+log = Logger()
+
+
+class DKIMUtils(object):
+    """
+    Some useful functions.
+    """
+    
+    @staticmethod
+    def hashlib_method(algorithm):
+        """
+        Return hashlib function for DKIM algorithm.
+        """
+        return {
+            "rsa-sha1"  : hashlib.sha1,
+            "rsa-sha256": hashlib.sha256,
+        }[algorithm]
+    
+    @staticmethod
+    def hash_name(algorithm):
+        """
+        Return RSA hash name for DKIM algorithm.
+        """
+        return {
+            "rsa-sha1"  : "SHA-1",
+            "rsa-sha256": "SHA-256"
+        }[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:
+            name, value = item.split("=", 1)
+            dkim_tags[name.strip()] = value.strip()
+        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":
+            pos = value.find(remove_b)
+            value = value[:pos] + value[pos + len(remove_b):]
+            value = " ".join(value.split())
+
+        return "%s:%s\r\n" % (name, value,)
+
+    
+
+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,
+        expire,
+    ):
+        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 ("rsa-sha1", "rsa-sha256",)
+        assert useDNSKey or useHTTPKey
+
+        self.hash_method = DKIMUtils.hashlib_method(self.algorithm)
+        self.hash_name = DKIMUtils.hash_name(self.algorithm)
+        
+        self.keyMethods = []
+        if useDNSKey: self.keyMethods.append("dns/txt")
+        if useHTTPKey: self.keyMethods.append("http-well-known")
+
+
+    @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(data).digest()))
+
+
+    @inlineCallbacks
+    def signatureHeaders(self):
+        """
+        Generate the headers that are going to be signed as well as the DKIM-Signature tags.
+        """
+        
+        # Figure out all the existing headers to sign
+        headers = []
+        sign_headers = []
+        raw = dict([(name, values) for name, values in self.headers.getAllRawHeaders()])
+        for name in self.sign_headers:
+            for value in raw[name]:
+                headers.append(DKIMUtils.canonicalizeHeader(name, value))
+                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(("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.PrivateKey.load_pkcs1(open(self.key_file).read())
+        return base64.b64encode(rsa.sign(headers, self.keys[self.key_file], self.hash_name))
+
+
+
+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):
+        """
+        @param request: The HTTP request to process
+        @type request: L{twext.server.Request}
+        """
+        self.request = request
+        self.dkim_tags = {}
+
+        # Prefer HTTP over DNS when both are present
+        self.key_lookup_methods = (
+            PublicKeyLookup_HTTP_WellKnown,
+            PublicKeyLookup_DNSTXT,
+        ) if key_lookup is None else key_lookup
+            
+            
+
+    @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:
+            rsa.verify(headers, base64.b64decode(self.dkim_tags["b"]), pubkey)
+        except rsa.VerificationError:
+            log.debug("DKIM: Could not verify signature: DKIM-Signature:%s" % (self.request.headers.getRawHeaders("DKIM-Signature"),))
+            raise DKIMVerificationError("Could not verify the DKIM signature")
+
+        # 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
+        bh = base64.b64encode(self.hash_method(data).digest())
+        if bh != self.dkim_tags["bh"]:
+            log.debug("DKIM: Could not verify body hash: DKIM-Signature:%s" % (self.request.headers.getRawHeaders("DKIM-Signature"),))
+            raise DKIMVerificationError("Could not verify the DKIM body hash")
+
+        
+    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:
+            log.debug("DKIM: No DKIM-Signature header present in the request")
+            raise DKIMVerificationError("No DKIM-Signature header present in the request")
+        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
+            log.debug("DKIM: Only one DKIM-Signature allowed in the request")
+            raise DKIMVerificationError("Only one DKIM-Signature allowed in the request")
+        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",)
+        for tag in required_tags:
+            if tag not in self.dkim_tags:
+                log.debug("DKIM: Missing DKIM-Signature tag: %s" % (tag,))
+                raise DKIMVerificationError("Missing DKIM-Signature tag: %s" % (tag,))
+
+        check_values = {
+            "v": ("1",),
+            "a": ("rsa-sha1", "rsa-sha256",),
+            "c": ("relaxed", "relaxed/simple",),
+            "q": ("dns/txt", "http-well-known",),
+        }
+        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:
+                    log.debug("DKIM: Tag: %s has incorrect value: %s" % (tag, self.dkim_tags[tag],))
+                    raise DKIMVerificationError("DKIM-Signature tag: %s has incorrect value: %s" % (tag, self.dkim_tags[tag],))
+
+        # Check expiration
+        if "x" in self.dkim_tags:
+            diff_time = int(time.time()) - int(self.dkim_tags["x"])
+            if diff_time > 0:
+                log.debug("DKIM: Signature expired: %d seconds" % (diff_time,))
+                raise DKIMVerificationError("DKIM-Signature expired: %d seconds" % (diff_time,))
+
+        # Some useful bits
+        self.hash_method = DKIMUtils.hashlib_method(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)
+            try:
+                headers.append((header, actual_headers[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:
+                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
+    """
+    
+    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", "ischedule") not in ("*", "ischedule",):
+                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-----
+""" % (textwrap.wrap(pkey["p"], 64),)
+
+        try:
+            return rsa.PublicKey.load_pkcs1(key_data)
+        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 = "dns/txt"
+
+
+    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"],)
+        
+    
+    def lookupKeys(self):
+        """
+        Do the key lookup using the actual lookup method.
+        """
+        raise NotImplementedError
+        
+
+
+class PublicKeyLookup_HTTP_WellKnown(PublicKeyLookup):
+    
+    
+    method = "http-well-known"
+
+
+    def getSelectorKey(self):
+        """
+        Get a token used to uniquely identify the key being looked up. Token format will
+        depend on the lookup method.
+        """
+        return "https://%s/.well-known/domainkey/%s" % (self.dkim_tags["d"], self.dkim_tags["s"],)
+        
+    
+    def lookupKeys(self):
+        """
+        Do the key lookup using the actual lookup method.
+        """
+        raise NotImplementedError
+

Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,228 @@
+##
+# 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
+from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
+from twistedcaldav.schedule import deliverSchedulePrivilegeSet
+from twistedcaldav.scheduling.scheduler import IScheduleScheduler
+from txdav.xml import element as davxml
+import twistedcaldav.scheduling.ischedule.xml  as ischedulexml
+
+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
+
+    def http_GET(self, request):
+        """
+        The iSchedule GET method.
+        """
+
+        if not request.args:
+            # Do normal GET behavior
+            return self.render(request)
+
+        query = request.args.get("query", ("",))
+        if len(query) != 1:
+            raise HTTPError(StatusResponse(
+                responsecode.BAD_REQUEST,
+                "Invalid query parameter",
+            ))
+        query = query[0]
+            
+        query = {
+            "capabilities"  : self.doCapabilities,
+        }.get(query, None)
+        
+        if query is None:
+            raise HTTPError(StatusResponse(
+                responsecode.BAD_REQUEST,
+                "Unknown query query parameter",
+            ))
+
+        return query(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.CapabilitySet(
+                ischedulexml.SupportedVersionSet(
+                    ischedulexml.Version.fromString("1.0"),
+                ),
+                ischedulexml.SupportedSchedulingMessageSet(
+                    ischedulexml.Comp(
+                        ischedulexml.Method(name="REQUEST"),
+                        ischedulexml.Method(name="CANCEL"),
+                        ischedulexml.Method(name="REPLY"),
+                        name="VEVENT"
+                    ),
+                    ischedulexml.Comp(
+                        ischedulexml.Method(name="REQUEST"),
+                        ischedulexml.Method(name="CANCEL"),
+                        ischedulexml.Method(name="REPLY"),
+                        name="VTODO"
+                    ),
+                    ischedulexml.Comp(
+                        ischedulexml.Method(name="REQUEST"),
+                        name="VFREEBUSY"
+                    ),
+                ),
+                ischedulexml.SupportedCalendarDataType(
+                    ischedulexml.CalendarDataType(**{
+                            "content-type":"text/calendar",
+                            "version":"2.0",
+                    }),
+                ),
+                ischedulexml.SupportedAttachmentValues(
+                    ischedulexml.ExternalAttachment(),
+                ),
+                ischedulexml.SupportedRecipientURISchemeSet(
+                    ischedulexml.Scheme.fromString("mailto")
+                ),
+                ischedulexml.MaxContentLength.fromString(config.MaxResourceSize),
+                ischedulexml.MinDateTime.fromString(minDateTime.getText()),
+                ischedulexml.MaxDateTime.fromString(maxDateTime.getText()),
+                ischedulexml.MaxInstances.fromString(config.MaxAllowedInstances),
+                ischedulexml.MaxRecipients.fromString(config.MaxAttendeesPerInstance),
+                ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")),
+            ),
+        )
+        return XMLResponse(responsecode.OK, result)
+
+    @inlineCallbacks
+    def http_POST(self, request):
+        """
+        The server-to-server POST method.
+        """
+
+        # This is a server-to-server scheduling operation.
+        scheduler = IScheduleScheduler(request, self)
+
+        # Need a transaction to work with
+        txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
+        request._newStoreTransaction = txn
+         
+        # Do the POST processing treating this as a non-local schedule
+        try:
+            result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
+        except Exception, e:
+            yield txn.abort()
+            raise e
+        else:
+            yield txn.commit()
+        returnValue(result.response())
+
+    ##
+    # ACL
+    ##
+
+    def supportedPrivileges(self, request):
+        return succeed(deliverSchedulePrivilegeSet)
+
+    def defaultAccessControlList(self):
+        privs = (
+            davxml.Privilege(davxml.Read()),
+            davxml.Privilege(caldavxml.ScheduleDeliver()),
+        )
+
+        return davxml.ACL(
+            # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
+            davxml.ACE(
+                davxml.Principal(davxml.All()),
+                davxml.Grant(*privs),
+                davxml.Protected(),
+            ),
+        )

Copied: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/servers.py (from rev 9747, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischeduleservers.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/servers.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/servers.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,191 @@
+##
+# Copyright (c) 2006-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.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 import xmlutil
+
+"""
+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
+        _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 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]

Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -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.
+##

Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_dkim.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_dkim.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_dkim.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,741 @@
+##
+# 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.web2.http_headers import Headers, MimeType
+from twext.web2.stream import MemoryStream
+from twisted.internet.defer import inlineCallbacks, succeed
+from twistedcaldav.scheduling.ischedule.dkim import DKIMRequest, DKIMVerifier,\
+    DKIMVerificationError, DKIMUtils, PublicKeyLookup_DNSTXT,\
+    PublicKeyLookup_HTTP_WellKnown
+import base64
+import hashlib
+import rsa
+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.
+    """
+
+    def test_valid_request(self):
+        
+        stream = MemoryStream("")
+        request = DKIMRequest("POST", "/", {}, stream, "example.com", "dkim", "/tmp/key", "rsa-sha1", ("Originator", "Recipient",), True, True, 3600)
+
+        
+    @inlineCallbacks
+    def test_body_hash(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", "/tmp/key", algorithm, ("Originator", "Recipient", "Content-Type",), True, True, 3600)
+            hash = base64.b64encode(hash_method(data).digest())
+            result = (yield request.bodyHash())
+            self.assertEqual(result, hash)
+
+        
+    def test_generateSignature(self):
+
+        data = "Hello World!"
+
+        for algorithm, hash_method, hash_name in (
+            ("rsa-sha1", hashlib.sha1, "SHA-1",),
+            ("rsa-sha256", hashlib.sha256, "SHA-256"),
+        ):
+            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, 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
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http-well-known; 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.PrivateKey.load_pkcs1(open(self.private_keyfile).read())
+            signature = base64.b64encode(rsa.sign(sign_this, key, hash_name))
+    
+            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, 3600)
+            result, _ignore_tags = (yield request.signatureHeaders())
+            
+            # 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
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient:Content-Type; bh=%s; b=
+""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.time, request.expire, algorithm, bodyhash)
+    
+            self.assertEqual(result, sign_this)
+
+        
+    @inlineCallbacks
+    def test_sign(self):
+
+        data = "Hello World!"
+        for algorithm, hash_method, hash_name in (
+            ("rsa-sha1", hashlib.sha1, "SHA-1",),
+            ("rsa-sha256", hashlib.sha256, "SHA-256"),
+        ):
+            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, 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(data).digest())
+            sign_this = """originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com
+content-type:%s
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient:Content-Type; bh=%s; b=
+""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.time, request.expire, algorithm, bodyhash)
+            key = rsa.PrivateKey.load_pkcs1(open(self.private_keyfile).read())
+            signature = base64.b64encode(rsa.sign(sign_this, key, hash_name))
+    
+            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=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient:Content-Type; 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.PublicKey.load_pkcs1(open(self.public_keyfile).read())
+            self.assertEqual(rsa.verify(sign_this, base64.b64decode(result), pubkey), None)
+
+
+class TestDKIMVerifier (TestDKIMBase):
+    """
+    L{DKIMVerifier} support tests.
+    """
+
+    class StubRequest(object):
+        
+        def __init__(self, headers, body):
+            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; 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; 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; 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; 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; 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; 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; 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; 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; 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; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
+        )
+        
+        for headers, result in data:
+            request = self.StubRequest(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"),
+            (
+                "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; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def",
+                "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=\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; b= def ; c=relaxed/simple; h=Originator:Recipient; bh=abc",
+                "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; b= ; c=relaxed/simple; h=Originator:Recipient; bh=abc\r\n",
+            ),
+        )
+        
+        for name, value, result in data:
+            request = self.StubRequest(((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  
+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; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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  
+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; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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
+recipient:mailto:user04 at example.com
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=
+"""
+            ),
+            # Under 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  
+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; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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
+recipient:mailto:user04 at example.com
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=
+"""
+            ),
+            # Re-ordered Content-Type
+            ("""Host:example.com
+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; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=
+"""
+            ),
+        )
+        
+        for hdrs, result in data:
+            headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
+            request = self.StubRequest(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; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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(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, 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.addRawHeader("Recipient", ("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 test_selector_key(self):
+        
+        for lookup, result in (
+            (PublicKeyLookup_DNSTXT, "dkim._domainkey.example.com"),
+            (PublicKeyLookup_HTTP_WellKnown, "https://example.com/.well-known/domainkey/dkim")
+        ):
+            dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+            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 ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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.assertNotEqual(pubkey, None)
+        
+        # Valid with more tags
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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.assertNotEqual(pubkey, None)
+        
+        # Invalid - key type
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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.assertEqual(pubkey, None)
+        
+        # Invalid - hash
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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.assertEqual(pubkey, None)
+        
+        # Invalid - service
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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.assertEqual(pubkey, None)
+        
+        # Invalid - revoked
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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.assertEqual(pubkey, None)
+        
+        # Multiple valid
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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.assertNotEqual(pubkey, 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 ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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.assertNotEqual(pubkey, None)
+        
+        # Multiple - invalid
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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.assertEqual(pubkey, 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 ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; 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.assertNotEqual(pubkey, None)
+        
+        # Cache valid
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.keys = []
+        pubkey = (yield lookup.getPublicKey())
+        self.assertNotEqual(pubkey, None)
+        
+        # Cache invalid
+        dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+        lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+        lookup.flushCache()
+        lookup.keys = []
+        pubkey = (yield lookup.getPublicKey())
+        self.assertEqual(pubkey, None)

Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,191 @@
+##
+# 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 txdav.xml.element import registerElement
+from txdav.xml.element import WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
+
+
+##
+# 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 CapabilitySet (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "capability-set"
+    allowed_children = {
+        (ischedule_namespace, "supported-version-set"): (1, 1),
+        (ischedule_namespace, "supported-scheduling-message-set"): (1, 1),
+        (ischedule_namespace, "supported-calendar-data-type"): (1, 1),
+        (ischedule_namespace, "supported-attachment-values"): (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 SupportedVersionSet (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "supported-version-set"
+    allowed_children = {
+        (ischedule_namespace, "version"): (1, None),
+    }
+
+
+ at registerElement
+class Version (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "version"
+
+
+ at registerElement
+class SupportedSchedulingMessageSet (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "supported-scheduling-message-set"
+    allowed_children = {
+        (ischedule_namespace, "comp"): (1, None),
+    }
+
+
+ at registerElement
+class Comp (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "comp"
+    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 SupportedCalendarDataType (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "supported-calendar-data-type"
+    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 SupportedAttachmentValues (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "supported-attachment-values"
+    allowed_children = {
+        (ischedule_namespace, "inline-attachment"): (0, 1),
+        (ischedule_namespace, "external-attachment"): (0, 1),
+    }
+
+
+ at registerElement
+class InlineAttachment (WebDAVEmptyElement):
+    namespace = ischedule_namespace
+    name = "inline-attachment"
+
+
+ at registerElement
+class ExternalAttachment (WebDAVEmptyElement):
+    namespace = ischedule_namespace
+    name = "external-attachment"
+
+
+ at registerElement
+class SupportedRecipientURISchemeSet (WebDAVElement):
+    namespace = ischedule_namespace
+    name = "supported-recipient-uri-scheme-set"
+    allowed_children = {
+        (ischedule_namespace, "scheme"): (1, None),
+    }
+
+
+ at registerElement
+class Scheme (WebDAVTextElement):
+    namespace = ischedule_namespace
+    name = "scheme"
+
+
+ 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"
+

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

Deleted: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischeduleservers.py	2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischeduleservers.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -1,214 +0,0 @@
-##
-# Copyright (c) 2006-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.python.filepath import CachingFilePath as FilePath
-
-from twext.python.log import Logger
-
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.scheduling.delivery import DeliveryService
-
-import xml.dom.minidom
-
-"""
-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
-        fd = open(xmlFile.path, "r")
-        doc = xml.dom.minidom.parse(fd)
-        fd.close()
-
-        # Verify that top-level element is correct
-        servers_node = doc._get_documentElement()
-        if servers_node._get_localName() != ELEMENT_SERVERS:
-            log.error("Ignoring file %r because it is not a server-to-server config file" % (self.xmlFile,))
-            return
-        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._get_childNodes():
-            child_name = child._get_localName()
-            if child_name is None:
-                continue
-            elif child_name == 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._get_childNodes():
-            child_name = child._get_localName()
-            if child_name is None:
-                continue
-            elif child_name == ELEMENT_URI:
-                if child.firstChild is not None:
-                    self.uri = child.firstChild.data.encode("utf-8")
-            elif child_name == ELEMENT_AUTHENTICATION:
-                self._parseAuthentication(child)
-            elif child_name == ELEMENT_ALLOW_REQUESTS_FROM:
-                self.allow_from = True
-            elif child_name == ELEMENT_ALLOW_REQUESTS_TO:
-                self.allow_to = True
-            elif child_name == ELEMENT_DOMAINS:
-                self._parseList(child, ELEMENT_DOMAIN, self.domains)
-            elif child_name == ELEMENT_CLIENT_HOSTS:
-                self._parseList(child, ELEMENT_HOST, self.client_hosts)
-            else:
-                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child_name,))
-        
-        self._parseDetails()
-
-    def _parseList(self, node, element_name, appendto):
-        for child in node._get_childNodes():
-            if child._get_localName() == element_name:
-                if child.firstChild is not None:
-                    appendto.append(child.firstChild.data.encode("utf-8"))
-
-    def _parseAuthentication(self, node):
-        if node.hasAttribute(ATTRIBUTE_TYPE):
-            atype = node.getAttribute(ATTRIBUTE_TYPE).encode("utf-8")
-            if atype != ATTRIBUTE_BASICAUTH:
-                return
-        else:
-            return
-
-        for child in node._get_childNodes():
-            if child._get_localName() == ELEMENT_USER:
-                if child.firstChild is not None:
-                    user = child.firstChild.data.encode("utf-8")
-            elif child._get_localName() == ELEMENT_PASSWORD:
-                if child.firstChild is not None:
-                    password = child.firstChild.data.encode("utf-8")
-        
-        self.authentication = ("basic", user, password,)
-
-    def _parseDetails(self):
-        # Extract scheme, host, port and path
-        if self.uri.startswith("http://"):
-            self.ssl = False
-            rest = self.uri[7:]
-        elif self.uri.startswith("https://"):
-            self.ssl = True
-            rest = self.uri[8:]
-        
-        splits = rest.split("/", 1)
-        hostport = splits[0].split(":")
-        self.host = hostport[0]
-        if len(hostport) > 1:
-            self.port = int(hostport[1])
-        else:
-            self.port = {False:80, True:443}[self.ssl]
-        self.path = "/"
-        if len(splits) > 1:
-            self.path += splits[1]

Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/scheduler.py	2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/scheduler.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -47,8 +47,8 @@
 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.ischedule.delivery import ScheduleViaISchedule
+from twistedcaldav.scheduling.ischedule.servers import IScheduleServers
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
 from twistedcaldav.servers import Servers
 from twistedcaldav.util import normalizationLookup

Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/stdconfig.py	2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/stdconfig.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -606,6 +606,17 @@
             "Enabled"          : False, # iSchedule protocol
             "AddressPatterns"  : [],    # Reg-ex patterns to match iSchedule-able calendar user addresses
             "Servers"          : "servertoserver.xml",    # iSchedule server configurations
+            "DKIM"             : {      # DKIM options
+                "Enabled"               : True,             # DKIM signing/verification enabled
+                "Domain"                : "",               # Domain for DKIM (defaults to ServerHostName)
+                "SignatureAlgorithm"    : "rsa-sha256",     # Signature algorithm (one of rsa-sha1 or rsa-sha256)
+                "UseDNSKey"             : True,             # Public key stored in DNS
+                "UseHTTPKey"            : True,             # Public key stored in HTTP /.well-known
+                "KeySelector"           : "ischedule",      # Selector for public key
+                "ExpireSeconds"         : 3600,             # Expiration time for signature verification
+                "PrivateKeyFile"        : "",               # File where private key is stored
+                "PublicKeyFile"         : "",               # File where public key is stored
+            },
         },
 
         "iMIP": {

Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/timezonexml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/timezonexml.py	2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/timezonexml.py	2012-08-29 20:24:20 UTC (rev 9751)
@@ -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 = {
@@ -39,7 +39,7 @@
 
 
 @registerElement
-class Operation (davxml.WebDAVElement):
+class Operation (WebDAVElement):
     namespace = timezone_namespace
     name = "operation"
     allowed_children = {
@@ -50,19 +50,19 @@
 
 
 @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 = {
@@ -75,31 +75,31 @@
 
 
 @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 = {
@@ -109,13 +109,13 @@
 
 
 @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 = {
@@ -128,37 +128,37 @@
 
 
 @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 = {
@@ -168,7 +168,7 @@
 
 
 @registerElement
-class Tzdata (davxml.WebDAVElement):
+class Tzdata (WebDAVElement):
     namespace = timezone_namespace
     name = "tzdata"
     allowed_children = {
@@ -179,13 +179,13 @@
 
 
 @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 = {
@@ -198,18 +198,18 @@
 
 
 @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"
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120829/a159ed2d/attachment-0001.html>


More information about the calendarserver-changes mailing list