[CalendarServer-changes] [1844]
CalendarServer/branches/users/cdaboo/server2server-1842
source_changes at macosforge.org
source_changes at macosforge.org
Thu Sep 6 09:07:15 PDT 2007
Revision: 1844
http://trac.macosforge.org/projects/calendarserver/changeset/1844
Author: cdaboo at apple.com
Date: 2007-09-06 09:07:14 -0700 (Thu, 06 Sep 2007)
Log Message:
-----------
Created a new top-level resource /inbox that will act as the server-to-server inbox. Refactored scheduling code to start pulling
out the core scheduling features into more manageable chunks so that the slightly different behaviors of CalDAV and Server-to-server
can be easily handled without code duplication. The server-to-server behavior is controlled via a config option.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/server2server-1842/conf/caldavd-test.plist
CalendarServer/branches/users/cdaboo/server2server-1842/conf/caldavd.plist
CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/config.py
CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/customxml.py
CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule.py
CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/static.py
CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/tap.py
Added Paths:
-----------
CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule_common.py
Modified: CalendarServer/branches/users/cdaboo/server2server-1842/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/conf/caldavd-test.plist 2007-09-06 01:30:57 UTC (rev 1843)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/conf/caldavd-test.plist 2007-09-06 16:07:14 UTC (rev 1844)
@@ -330,7 +330,11 @@
<key>EnableNotifications</key>
<true/>
+ <!-- Server to server protocol -->
+ <key>EnableServerToServer</key>
+ <true/>
+
<!--
Twisted
-->
Modified: CalendarServer/branches/users/cdaboo/server2server-1842/conf/caldavd.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/conf/caldavd.plist 2007-09-06 01:30:57 UTC (rev 1843)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/conf/caldavd.plist 2007-09-06 16:07:14 UTC (rev 1844)
@@ -264,6 +264,9 @@
<key>EnableNotifications</key>
<true/>
+ <!-- Server to server protocol -->
+ <key>EnableServerToServer</key>
+ <true/>
</dict>
</plist>
Modified: CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/config.py 2007-09-06 01:30:57 UTC (rev 1843)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/config.py 2007-09-06 16:07:14 UTC (rev 1844)
@@ -145,8 +145,9 @@
#
# Non-standard CalDAV extensions
#
- "EnableDropBox" : False, # Calendar Drop Box
- "EnableNotifications": False, # Drop Box Notifications
+ "EnableDropBox" : False, # Calendar Drop Box
+ "EnableNotifications" : False, # Drop Box Notifications
+ "EnableServerToServer": False, # Server-to-server protocol
#
# Implementation details
Modified: CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/customxml.py 2007-09-06 01:30:57 UTC (rev 1843)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/customxml.py 2007-09-06 16:07:14 UTC (rev 1844)
@@ -254,6 +254,14 @@
return found
+class ServerToServerInbox (davxml.WebDAVEmptyElement):
+ """
+ Denotes the resourcetype of a server-to_server Inbox.
+ (CalDAV-s2s-xx, section x.x.x)
+ """
+ namespace = calendarserver_namespace
+ name = "server-to-server-inbox"
+
##
# Extensions to davxml.ResourceType
##
@@ -263,3 +271,4 @@
davxml.ResourceType.notifications = davxml.ResourceType(davxml.Collection(), Notifications())
davxml.ResourceType.calendarproxyread = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyRead())
davxml.ResourceType.calendarproxywrite = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyWrite())
+davxml.ResourceType.servertoserverinbox = davxml.ResourceType(ServerToServerInbox())
Modified: CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule.py 2007-09-06 01:30:57 UTC (rev 1843)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule.py 2007-09-06 16:07:14 UTC (rev 1844)
@@ -25,32 +25,23 @@
"ScheduleOutboxResource",
]
-from twisted.internet import reactor
-from twisted.internet.defer import deferredGenerator, maybeDeferred, succeed, waitForDeferred
-from twisted.python import log
-from twisted.python.failure import Failure
+from twisted.internet.defer import deferredGenerator, succeed, waitForDeferred
from twisted.web2 import responsecode
-from twisted.web2.http import HTTPError, Response
-from twisted.web2.http_headers import MimeType
from twisted.web2.dav import davxml
-from twisted.web2.dav.http import ErrorResponse, errorForFailure, messageForFailure, statusForFailure
-from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.http import ErrorResponse
from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError
+from twisted.web2.http import Response
+from twisted.web2.http_headers import MimeType
from twistedcaldav import caldavxml
-from twistedcaldav import itip
-from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.config import config
from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.ical import Component
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.resource import CalDAVResource
from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.schedule_common import doSchedulingViaPOST
-import md5
-import time
-
class CalendarSchedulingCollectionResource (CalDAVResource):
"""
CalDAV principal resource.
@@ -201,398 +192,80 @@
yield x
x.getResult()
- # Must be content-type text/calendar
- content_type = request.headers.getHeader("content-type")
- if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
- log.err("MIME type %s not allowed in calendar collection" % (content_type,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-
- # Must have Originator header
- originator = request.headers.getRawHeaders("originator")
- if originator is None or (len(originator) != 1):
- log.err("POST request must have Originator header")
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
- else:
- originator = originator[0]
-
- # Verify that Originator is a valid calendar user (has an INBOX)
- oprincipal = self.principalForCalendarUserAddress(originator)
- if oprincipal is None:
- log.err("Could not find principal for originator: %s" % (originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ # Do the POST processing treating this as a local schedule
+ x = waitForDeferred(doSchedulingViaPOST(self, request, True))
+ yield x
+ yield x.getResult()
- inboxURL = oprincipal.scheduleInboxURL()
- if inboxURL is None:
- log.err("Could not find inbox for originator: %s" % (originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
- # Verify that Originator matches the authenticated user
- if davxml.Principal(davxml.HRef(oprincipal.principalURL())) != self.currentPrincipal(request):
- log.err("Originator: %s does not match authorized user: %s" % (originator, self.currentPrincipal(request).children[0],))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
- # Get list of Recipient headers
- rawrecipients = request.headers.getRawHeaders("recipient")
- if rawrecipients is None or (len(rawrecipients) == 0):
- log.err("POST request must have at least one Recipient header")
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+class ScheduleServerToServerResource (CalDAVResource):
+ """
+ Server-to-server schedule Inbox resource.
- # Recipient header may be comma separated list
- recipients = []
- for rawrecipient in rawrecipients:
- for r in rawrecipient.split(","):
- r = r.strip()
- if len(r):
- recipients.append(r)
+ Extends L{DAVResource} to provide Server-to-server functionality.
+ """
- timerange = TimeRange(start="20000101", end="20000102")
- recipients_state = {"OK":0, "BAD":0}
+ def __init__(self, parent):
+ """
+ @param parent: the parent resource of this one.
+ """
+ assert parent is not None
- # Parse the calendar object from the HTTP request stream
- try:
- d = waitForDeferred(Component.fromIStream(request.stream))
- yield d
- calendar = d.getResult()
- except:
- log.err("Error while handling POST: %s" % (Failure(),))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # Must be a valid calendar
- try:
- calendar.validCalendarForCalDAV()
- except ValueError:
- log.err("POST request calendar component is not valid: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+ CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
- # Must have a METHOD
- if not calendar.isValidMethod():
- log.err("POST request must have valid METHOD property in calendar component: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # Verify iTIP behaviour
- if not calendar.isValidITIP():
- log.err("POST request must have a calendar component that satisfies iTIP requirements: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # Verify that the ORGANIZER's cu address maps to the request.uri
- outboxURL = None
- organizer = calendar.getOrganizer()
- if organizer is not None:
- oprincipal = self.principalForCalendarUserAddress(organizer)
- if oprincipal is not None:
- outboxURL = oprincipal.scheduleOutboxURL()
- if outboxURL is None:
- log.err("ORGANIZER in calendar data is not valid: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ self.parent = parent
- # Prevent spoofing of ORGANIZER with specific METHODs
- if (calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER")) and (outboxURL != request.uri):
- log.err("ORGANIZER in calendar data does not match owner of Outbox: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ def defaultAccessControlList(self):
+ return davxml.ACL(
+ # DAV:Read, CalDAV:schedule for all principals (includes anonymous)
+ davxml.ACE(
+ davxml.Principal(davxml.All()),
+ davxml.Grant(
+ davxml.Privilege(davxml.Read()),
+ davxml.Privilege(caldavxml.Schedule()),
+ ),
+ davxml.Protected(),
+ ),
+ )
- # Prevent spoofing when doing reply-like METHODs
- if calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
- # Verify that there is a single ATTENDEE property and that the Originator has permission
- # to send on behalf of that ATTENDEE
- attendees = calendar.getAttendees()
-
- # Must have only one
- if len(attendees) != 1:
- log.err("ATTENDEE list in calendar data is wrong: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-
- # Attendee's Outbox MUST be the request URI
- aoutboxURL = None
- aprincipal = self.principalForCalendarUserAddress(attendees[0])
- if aprincipal is not None:
- aoutboxURL = aprincipal.scheduleOutboxURL()
- if aoutboxURL is None or aoutboxURL != request.uri:
- log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+ def resourceType(self):
+ return davxml.ResourceType.servertoserverinbox
- # For free-busy do immediate determination of iTIP result rather than fan-out
- if (calendar.propertyValue("METHOD") == "REQUEST") and (calendar.mainType() == "VFREEBUSY"):
- # Extract time range from VFREEBUSY object
- vfreebusies = [v for v in calendar.subcomponents() if v.name() == "VFREEBUSY"]
- if len(vfreebusies) != 1:
- log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- dtstart = vfreebusies[0].getStartDateUTC()
- dtend = vfreebusies[0].getEndDateUTC()
- if dtstart is None or dtend is None:
- log.err("VFREEBUSY start/end not valid: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- timerange.start = dtstart
- timerange.end = dtend
+ def isCollection(self):
+ return False
- # Look for maksed UID
- excludeuid = calendar.getMaskUID()
+ def isCalendarCollection(self):
+ return False
- # Do free busy operation
- freebusy = True
- else:
- # Do regular invite (fan-out)
- freebusy = False
+ def isPseudoCalendarCollection(self):
+ return False
- # Prepare for multiple responses
- responses = ScheduleResponseQueue("POST", responsecode.OK)
-
- # Extract the ORGANIZER property and UID value from the calendar data for use later
- organizerProp = calendar.getOrganizerProperty()
- uid = calendar.resourceUID()
+ 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>"""
- # Loop over each recipient and do appropriate action.
- autoresponses = []
- for recipient in recipients:
- # Get the principal resource for this recipient
- principal = self.principalForCalendarUserAddress(recipient)
+ response = Response(200, {}, output)
+ response.headers.setHeader("content-type", MimeType("text", "html"))
+ return response
- # Map recipient to their inbox
- inbox = None
- if principal is None:
- log.err("No principal for calendar user address: %s" % (recipient,))
- else:
- inboxURL = principal.scheduleInboxURL()
- if inboxURL:
- inbox = waitForDeferred(request.locateResource(inboxURL))
- yield inbox
- inbox = inbox.getResult()
- else:
- log.err("No schedule inbox for principal: %s" % (principal,))
-
- if inbox is None:
- err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
- recipients_state["BAD"] += 1
-
- # Process next recipient
- continue
- else:
- #
- # Check access controls
- #
- try:
- d = waitForDeferred(inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(oprincipal.principalURL()))))
- yield d
- d.getResult()
- except AccessDeniedError:
- log.err("Could not access Inbox for recipient: %s" % (recipient,))
- err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permisions")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
- recipients_state["BAD"] += 1
-
- # Process next recipient
- continue
-
- # Different behaviour for free-busy vs regular invite
- if freebusy:
- # Extract the ATTENDEE property matching current recipient from the calendar data
- cuas = principal.calendarUserAddresses()
- attendeeProp = calendar.getAttendeeProperty(cuas)
-
- # Find the current recipients calendar-free-busy-set
- fbset = waitForDeferred(principal.calendarFreeBusyURIs(request))
- yield fbset
- fbset = fbset.getResult()
-
- # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
- fbinfo = ([], [], [])
-
- try:
- # Process the availability property from the Inbox.
- has_prop = waitForDeferred(inbox.hasProperty((calendarserver_namespace, "calendar-availability"), request))
- yield has_prop
- has_prop = has_prop.getResult()
- if has_prop:
- availability = waitForDeferred(inbox.readProperty((calendarserver_namespace, "calendar-availability"), request))
- yield availability
- availability = availability.getResult()
- availability = availability.calendar()
- report_common.processAvailabilityFreeBusy(availability, fbinfo, timerange)
-
- # Check to see if the recipient is the same calendar user as the organizer.
- # Needed for masked UID stuff.
- same_calendar_user = oprincipal.principalURL() == principal.principalURL()
-
- # Now process free-busy set calendars
- matchtotal = 0
- for calURL in fbset:
- cal = waitForDeferred(request.locateResource(calURL))
- yield cal
- cal = cal.getResult()
- if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
- # We will ignore missing calendars. If the recipient has failed to
- # properly manage the free busy set that should not prevent us from working.
- continue
-
- matchtotal = waitForDeferred(report_common.generateFreeBusyInfo(
- request,
- cal,
- fbinfo,
- timerange,
- matchtotal,
- excludeuid=excludeuid,
- organizer=organizer,
- same_calendar_user=same_calendar_user))
- yield matchtotal
- matchtotal = matchtotal.getResult()
-
- # Build VFREEBUSY iTIP reply for this recipient
- fbresult = report_common.buildFreeBusyResult(fbinfo, timerange, organizer=organizerProp, attendee=attendeeProp, uid=uid)
-
- responses.add(recipient, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
- recipients_state["OK"] += 1
-
- except:
- log.err("Could not determine free busy information: %s" % (recipient,))
- err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
- recipients_state["BAD"] += 1
-
- else:
- # Hash the iCalendar data for use as the last path element of the URI path
- name = md5.new(str(calendar) + str(time.time()) + inbox.fp.path).hexdigest() + ".ics"
-
- # Get a resource for the new item
- childURL = joinURL(inboxURL, name)
- child = waitForDeferred(request.locateResource(childURL))
- yield child
- child = child.getResult()
-
- # Copy calendar to inbox (doing fan-out)
- d = waitForDeferred(
- maybeDeferred(
- storeCalendarObjectResource,
- request=request,
- sourcecal = False,
- destination = child,
- destination_uri = childURL,
- calendardata = str(calendar),
- destinationparent = inbox,
- destinationcal = True,
- isiTIP = True
- )
- )
- yield d
- try:
- d.getResult()
- responses.add(recipient, responsecode.OK, reqstatus="2.0;Success")
- recipients_state["OK"] += 1
-
- # Store CALDAV:originator property
- child.writeDeadProperty(caldavxml.Originator(davxml.HRef(originator)))
-
- # Store CALDAV:recipient property
- child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient)))
-
- # Look for auto-schedule option
- if principal.autoSchedule():
- autoresponses.append((principal, inbox, child))
- except:
- log.err("Could not store data in Inbox : %s" % (inbox,))
- err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
- recipients_state["BAD"] += 1
-
- # Now we have to do auto-respond
- if len(autoresponses) != 0:
- # First check that we have a method that we can auto-respond to
- if not itip.canAutoRespond(calendar):
- autoresponses = []
-
- # Now do the actual auto response
- for principal, inbox, child in autoresponses:
- # Add delayed reactor task to handle iTIP responses
- reactor.callLater(0.0, itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
- #reactor.callInThread(itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
-
- # Return with final response if we are done
- yield responses.response()
-
-class ScheduleResponseResponse (Response):
- """
- ScheduleResponse L{Response} object.
- Renders itself as a CalDAV:schedule-response XML document.
- """
- def __init__(self, xml_responses, location=None):
+ @deferredGenerator
+ def http_POST(self, request):
"""
- @param xml_responses: an interable of davxml.Response objects.
- @param location: the value of the location header to return in the response,
- or None.
+ The server-to-server POST method.
"""
- Response.__init__(self, code=responsecode.OK,
- stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+ # Check authentication and access controls
+ x = waitForDeferred(self.authorize(request, (caldavxml.Schedule(),)))
+ yield x
+ x.getResult()
- self.headers.setHeader("content-type", MimeType("text", "xml"))
-
- if location is not None:
- self.headers.setHeader("location", location)
-
-class ScheduleResponseQueue (object):
- """
- Stores a list of (typically error) responses for use in a
- L{ScheduleResponse}.
- """
- def __init__(self, method, success_response):
- """
- @param method: the name of the method generating the queue.
- @param success_response: the response to return in lieu of a
- L{ScheduleResponse} if no responses are added to this queue.
- """
- self.responses = []
- self.method = method
- self.success_response = success_response
- self.location = None
-
- def setLocation(self, location):
- """
- @param location: the value of the location header to return in the response,
- or None.
- """
- self.location = location
-
- def add(self, recipient, what, reqstatus=None, calendar=None):
- """
- Add a response.
- @param recipient: the recipient for this response.
- @param what: a status code or a L{Failure} for the given recipient.
- @param status: the iTIP request-status for the given recipient.
- @param calendar: the calendar data for the given recipient response.
- """
- if type(what) is int:
- code = what
- error = None
- message = responsecode.RESPONSES[code]
- elif isinstance(what, Failure):
- code = statusForFailure(what)
- error = errorForFailure(what)
- message = messageForFailure(what)
- else:
- raise AssertionError("Unknown data type: %r" % (what,))
-
- if code > 400: # Error codes only
- log.err("Error during %s for %s: %s" % (self.method, recipient, message))
-
- children = []
- children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
- children.append(caldavxml.RequestStatus(reqstatus))
- if calendar is not None:
- children.append(caldavxml.CalendarData.fromCalendar(calendar))
- if error is not None:
- children.append(error)
- if message is not None:
- children.append(davxml.ResponseDescription(message))
- self.responses.append(caldavxml.Response(*children))
-
- def response(self):
- """
- Generate a L{ScheduleResponseResponse} with the responses contained in the
- queue or, if no such responses, return the C{success_response} provided
- to L{__init__}.
- @return: the response.
- """
- if self.responses:
- return ScheduleResponseResponse(self.responses, self.location)
- else:
- return self.success_response
+ # Do the POST processing treating this as a non-local schedule
+ x = waitForDeferred(doSchedulingViaPOST(self, request, False))
+ yield x
+ yield x.getResult()
Added: CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule_common.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule_common.py 2007-09-06 16:07:14 UTC (rev 1844)
@@ -0,0 +1,456 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# DRI: Cyrus Daboo, cdaboo at apple.com
+##
+
+"""
+CalDAV/Server-to-Server scheduling behavior.
+"""
+
+__all__ = [
+ "doSchedulingViaPOST",
+]
+
+from twisted.internet import reactor
+from twisted.internet.defer import deferredGenerator, maybeDeferred, waitForDeferred
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.web2 import responsecode
+from twisted.web2.http import HTTPError, Response
+from twisted.web2.http_headers import MimeType
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse, errorForFailure, messageForFailure, statusForFailure
+from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.util import joinURL
+
+from twistedcaldav import caldavxml
+from twistedcaldav import itip
+from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.ical import Component
+from twistedcaldav.method import report_common
+from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.resource import isCalendarCollectionResource
+
+import md5
+import time
+
+ at deferredGenerator
+def doSchedulingViaPOST(self, request, local):
+ """
+ The CalDAV POST method.
+
+ This uses a generator function yielding either L{waitForDeferred} objects or L{Response} objects.
+ This allows for code that follows a 'linear' execution pattern rather than having to use nested
+ L{Deferred} callbacks. The logic is easier to follow this way plus we don't run into deep nesting
+ issues which the other approach would have with large numbers of recipients.
+ """
+
+ # Must be content-type text/calendar
+ content_type = request.headers.getHeader("content-type")
+ if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
+ log.err("MIME type %s not allowed in calendar collection" % (content_type,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
+
+ # Must have Originator header
+ originator = request.headers.getRawHeaders("originator")
+ if originator is None or (len(originator) != 1):
+ log.err("POST request must have Originator header")
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
+ else:
+ originator = originator[0]
+
+ # Verify that Originator is a valid calendar user (has an INBOX)
+ oprincipal = self.principalForCalendarUserAddress(originator)
+ if oprincipal is None:
+ log.err("Could not find principal for originator: %s" % (originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ inboxURL = oprincipal.scheduleInboxURL()
+ if inboxURL is None:
+ log.err("Could not find inbox for originator: %s" % (originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ # Verify that Originator matches the authenticated user
+ if davxml.Principal(davxml.HRef(oprincipal.principalURL())) != self.currentPrincipal(request):
+ log.err("Originator: %s does not match authorized user: %s" % (originator, self.currentPrincipal(request).children[0],))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ # Get list of Recipient headers
+ rawrecipients = request.headers.getRawHeaders("recipient")
+ if rawrecipients is None or (len(rawrecipients) == 0):
+ log.err("POST request must have at least one Recipient header")
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+
+ # Recipient header may be comma separated list
+ recipients = []
+ for rawrecipient in rawrecipients:
+ for r in rawrecipient.split(","):
+ r = r.strip()
+ if len(r):
+ recipients.append(r)
+
+ timerange = TimeRange(start="20000101", end="20000102")
+ recipients_state = {"OK":0, "BAD":0}
+
+ # Parse the calendar object from the HTTP request stream
+ try:
+ d = waitForDeferred(Component.fromIStream(request.stream))
+ yield d
+ calendar = d.getResult()
+ except:
+ log.err("Error while handling POST: %s" % (Failure(),))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Must be a valid calendar
+ try:
+ calendar.validCalendarForCalDAV()
+ except ValueError:
+ log.err("POST request calendar component is not valid: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Must have a METHOD
+ if not calendar.isValidMethod():
+ log.err("POST request must have valid METHOD property in calendar component: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Verify iTIP behaviour
+ if not calendar.isValidITIP():
+ log.err("POST request must have a calendar component that satisfies iTIP requirements: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Verify that the ORGANIZER's cu address maps to the request.uri
+ outboxURL = None
+ organizer = calendar.getOrganizer()
+ if organizer is not None:
+ oprincipal = self.principalForCalendarUserAddress(organizer)
+ if oprincipal is not None:
+ outboxURL = oprincipal.scheduleOutboxURL()
+ if outboxURL is None:
+ log.err("ORGANIZER in calendar data is not valid: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ # Prevent spoofing of ORGANIZER with specific METHODs
+ if (calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER")) and (outboxURL != request.uri):
+ log.err("ORGANIZER in calendar data does not match owner of Outbox: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ # Prevent spoofing when doing reply-like METHODs
+ if calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+ # Verify that there is a single ATTENDEE property and that the Originator has permission
+ # to send on behalf of that ATTENDEE
+ attendees = calendar.getAttendees()
+
+ # Must have only one
+ if len(attendees) != 1:
+ log.err("ATTENDEE list in calendar data is wrong: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+
+ # Attendee's Outbox MUST be the request URI
+ aoutboxURL = None
+ aprincipal = self.principalForCalendarUserAddress(attendees[0])
+ if aprincipal is not None:
+ aoutboxURL = aprincipal.scheduleOutboxURL()
+ if aoutboxURL is None or aoutboxURL != request.uri:
+ log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+
+ # For free-busy do immediate determination of iTIP result rather than fan-out
+ if (calendar.propertyValue("METHOD") == "REQUEST") and (calendar.mainType() == "VFREEBUSY"):
+ # Extract time range from VFREEBUSY object
+ vfreebusies = [v for v in calendar.subcomponents() if v.name() == "VFREEBUSY"]
+ if len(vfreebusies) != 1:
+ log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+ dtstart = vfreebusies[0].getStartDateUTC()
+ dtend = vfreebusies[0].getEndDateUTC()
+ if dtstart is None or dtend is None:
+ log.err("VFREEBUSY start/end not valid: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+ timerange.start = dtstart
+ timerange.end = dtend
+
+ # Look for maksed UID
+ excludeuid = calendar.getMaskUID()
+
+ # Do free busy operation
+ freebusy = True
+ else:
+ # Do regular invite (fan-out)
+ freebusy = False
+
+ # Prepare for multiple responses
+ responses = ScheduleResponseQueue("POST", responsecode.OK)
+
+ # Extract the ORGANIZER property and UID value from the calendar data for use later
+ organizerProp = calendar.getOrganizerProperty()
+ uid = calendar.resourceUID()
+
+ # Loop over each recipient and do appropriate action.
+ autoresponses = []
+ for recipient in recipients:
+ # Get the principal resource for this recipient
+ principal = self.principalForCalendarUserAddress(recipient)
+
+ # Map recipient to their inbox
+ inbox = None
+ if principal is None:
+ log.err("No principal for calendar user address: %s" % (recipient,))
+ else:
+ inboxURL = principal.scheduleInboxURL()
+ if inboxURL:
+ inbox = waitForDeferred(request.locateResource(inboxURL))
+ yield inbox
+ inbox = inbox.getResult()
+ else:
+ log.err("No schedule inbox for principal: %s" % (principal,))
+
+ if inbox is None:
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
+ responses.add(recipient, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
+ recipients_state["BAD"] += 1
+
+ # Process next recipient
+ continue
+ else:
+ #
+ # Check access controls
+ #
+ try:
+ d = waitForDeferred(inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(oprincipal.principalURL()))))
+ yield d
+ d.getResult()
+ except AccessDeniedError:
+ log.err("Could not access Inbox for recipient: %s" % (recipient,))
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permisions")))
+ responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
+ recipients_state["BAD"] += 1
+
+ # Process next recipient
+ continue
+
+ # Different behaviour for free-busy vs regular invite
+ if freebusy:
+ # Extract the ATTENDEE property matching current recipient from the calendar data
+ cuas = principal.calendarUserAddresses()
+ attendeeProp = calendar.getAttendeeProperty(cuas)
+
+ # Find the current recipients calendar-free-busy-set
+ fbset = waitForDeferred(principal.calendarFreeBusyURIs(request))
+ yield fbset
+ fbset = fbset.getResult()
+
+ # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+ fbinfo = ([], [], [])
+
+ try:
+ # Process the availability property from the Inbox.
+ has_prop = waitForDeferred(inbox.hasProperty((calendarserver_namespace, "calendar-availability"), request))
+ yield has_prop
+ has_prop = has_prop.getResult()
+ if has_prop:
+ availability = waitForDeferred(inbox.readProperty((calendarserver_namespace, "calendar-availability"), request))
+ yield availability
+ availability = availability.getResult()
+ availability = availability.calendar()
+ report_common.processAvailabilityFreeBusy(availability, fbinfo, timerange)
+
+ # Check to see if the recipient is the same calendar user as the organizer.
+ # Needed for masked UID stuff.
+ same_calendar_user = oprincipal.principalURL() == principal.principalURL()
+
+ # Now process free-busy set calendars
+ matchtotal = 0
+ for calURL in fbset:
+ cal = waitForDeferred(request.locateResource(calURL))
+ yield cal
+ cal = cal.getResult()
+ if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
+ # We will ignore missing calendars. If the recipient has failed to
+ # properly manage the free busy set that should not prevent us from working.
+ continue
+
+ matchtotal = waitForDeferred(report_common.generateFreeBusyInfo(
+ request,
+ cal,
+ fbinfo,
+ timerange,
+ matchtotal,
+ excludeuid=excludeuid,
+ organizer=organizer,
+ same_calendar_user=same_calendar_user))
+ yield matchtotal
+ matchtotal = matchtotal.getResult()
+
+ # Build VFREEBUSY iTIP reply for this recipient
+ fbresult = report_common.buildFreeBusyResult(fbinfo, timerange, organizer=organizerProp, attendee=attendeeProp, uid=uid)
+
+ responses.add(recipient, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
+ recipients_state["OK"] += 1
+
+ except:
+ log.err("Could not determine free busy information: %s" % (recipient,))
+ err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+ responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
+ recipients_state["BAD"] += 1
+
+ else:
+ # Hash the iCalendar data for use as the last path element of the URI path
+ name = md5.new(str(calendar) + str(time.time()) + inbox.fp.path).hexdigest() + ".ics"
+
+ # Get a resource for the new item
+ childURL = joinURL(inboxURL, name)
+ child = waitForDeferred(request.locateResource(childURL))
+ yield child
+ child = child.getResult()
+
+ # Copy calendar to inbox (doing fan-out)
+ d = waitForDeferred(
+ maybeDeferred(
+ storeCalendarObjectResource,
+ request=request,
+ sourcecal = False,
+ destination = child,
+ destination_uri = childURL,
+ calendardata = str(calendar),
+ destinationparent = inbox,
+ destinationcal = True,
+ isiTIP = True
+ )
+ )
+ yield d
+ try:
+ d.getResult()
+ responses.add(recipient, responsecode.OK, reqstatus="2.0;Success")
+ recipients_state["OK"] += 1
+
+ # Store CALDAV:originator property
+ child.writeDeadProperty(caldavxml.Originator(davxml.HRef(originator)))
+
+ # Store CALDAV:recipient property
+ child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient)))
+
+ # Look for auto-schedule option
+ if principal.autoSchedule():
+ autoresponses.append((principal, inbox, child))
+ except:
+ log.err("Could not store data in Inbox : %s" % (inbox,))
+ err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+ responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
+ recipients_state["BAD"] += 1
+
+ # Now we have to do auto-respond
+ if len(autoresponses) != 0:
+ # First check that we have a method that we can auto-respond to
+ if not itip.canAutoRespond(calendar):
+ autoresponses = []
+
+ # Now do the actual auto response
+ for principal, inbox, child in autoresponses:
+ # Add delayed reactor task to handle iTIP responses
+ reactor.callLater(0.0, itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
+ #reactor.callInThread(itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
+
+ # Return with final response if we are done
+ yield responses.response()
+
+class ScheduleResponseResponse (Response):
+ """
+ ScheduleResponse L{Response} object.
+ Renders itself as a CalDAV:schedule-response XML document.
+ """
+ def __init__(self, xml_responses, location=None):
+ """
+ @param xml_responses: an interable of davxml.Response objects.
+ @param location: the value of the location header to return in the response,
+ or None.
+ """
+
+ Response.__init__(self, code=responsecode.OK,
+ stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+
+ self.headers.setHeader("content-type", MimeType("text", "xml"))
+
+ if location is not None:
+ self.headers.setHeader("location", location)
+
+class ScheduleResponseQueue (object):
+ """
+ Stores a list of (typically error) responses for use in a
+ L{ScheduleResponse}.
+ """
+ def __init__(self, method, success_response):
+ """
+ @param method: the name of the method generating the queue.
+ @param success_response: the response to return in lieu of a
+ L{ScheduleResponse} if no responses are added to this queue.
+ """
+ self.responses = []
+ self.method = method
+ self.success_response = success_response
+ self.location = None
+
+ def setLocation(self, location):
+ """
+ @param location: the value of the location header to return in the response,
+ or None.
+ """
+ self.location = location
+
+ def add(self, recipient, what, reqstatus=None, calendar=None):
+ """
+ Add a response.
+ @param recipient: the recipient for this response.
+ @param what: a status code or a L{Failure} for the given recipient.
+ @param status: the iTIP request-status for the given recipient.
+ @param calendar: the calendar data for the given recipient response.
+ """
+ if type(what) is int:
+ code = what
+ error = None
+ message = responsecode.RESPONSES[code]
+ elif isinstance(what, Failure):
+ code = statusForFailure(what)
+ error = errorForFailure(what)
+ message = messageForFailure(what)
+ else:
+ raise AssertionError("Unknown data type: %r" % (what,))
+
+ if code > 400: # Error codes only
+ log.err("Error during %s for %s: %s" % (self.method, recipient, message))
+
+ children = []
+ children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
+ children.append(caldavxml.RequestStatus(reqstatus))
+ if calendar is not None:
+ children.append(caldavxml.CalendarData.fromCalendar(calendar))
+ if error is not None:
+ children.append(error)
+ if message is not None:
+ children.append(davxml.ResponseDescription(message))
+ self.responses.append(caldavxml.Response(*children))
+
+ def response(self):
+ """
+ Generate a L{ScheduleResponseResponse} with the responses contained in the
+ queue or, if no such responses, return the C{success_response} provided
+ to L{__init__}.
+ @return: the response.
+ """
+ if self.responses:
+ return ScheduleResponseResponse(self.responses, self.location)
+ else:
+ return self.success_response
Modified: CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/static.py 2007-09-06 01:30:57 UTC (rev 1843)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/static.py 2007-09-06 16:07:14 UTC (rev 1844)
@@ -63,7 +63,7 @@
from twistedcaldav.index import Index, IndexSchedule
from twistedcaldav.notifications import NotificationsCollectionResource, NotificationResource
from twistedcaldav.resource import CalDAVResource, isCalendarCollectionResource, isPseudoCalendarCollectionResource
-from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
+from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource, ScheduleServerToServerResource
from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource, DropBoxChildResource
from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
from twistedcaldav.directory.calendar import DirectoryCalendarHomeTypeProvisioningResource
@@ -602,6 +602,48 @@
def __repr__(self):
return "<%s (calendar outbox collection): %s>" % (self.__class__.__name__, self.fp.path)
+class ServerToServerInboxFile (ScheduleServerToServerResource, CalDAVFile):
+ """
+ Server-to-server scheduling inbox resource.
+ """
+ def __init__(self, path, parent):
+ CalDAVFile.__init__(self, path, parent)
+ ScheduleServerToServerResource.__init__(self, parent)
+
+ self.fp.open("w").close()
+ self.fp.restat(False)
+
+ def __repr__(self):
+ return "<%s (server-to-server inbox resource): %s>" % (self.__class__.__name__, self.fp.path)
+
+ def isCollection(self):
+ return False
+
+ def createSimilarFile(self, path):
+ if path == self.fp.path:
+ return self
+ else:
+ return CalDAVFile(path, principalCollections=self.principalCollections())
+
+ def http_PUT (self, request): return responsecode.FORBIDDEN
+ def http_COPY (self, request): return responsecode.FORBIDDEN
+ def http_MOVE (self, request): return responsecode.FORBIDDEN
+ def http_DELETE (self, request): return responsecode.FORBIDDEN
+ def http_MKCOL (self, request): return responsecode.FORBIDDEN
+
+ def http_MKCALENDAR(self, request):
+ return ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "calendar-collection-location-ok")
+ )
+
+ ##
+ # ACL
+ ##
+
+ def supportedPrivileges(self, request):
+ return succeed(schedulePrivilegeSet)
+
class DropBoxHomeFile (AutoProvisioningFileMixIn, DropBoxHomeResource, CalDAVFile):
def __init__(self, path, parent):
DropBoxHomeResource.__init__(self)
Modified: CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/tap.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/tap.py 2007-09-06 01:30:57 UTC (rev 1843)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/tap.py 2007-09-06 16:07:14 UTC (rev 1844)
@@ -52,6 +52,7 @@
from twistedcaldav import pdmonster
from twistedcaldav.static import CalendarHomeProvisioningFile
+from twistedcaldav.static import ServerToServerInboxFile
try:
from twistedcaldav.authkerb import NegotiateCredentialFactory
@@ -239,7 +240,7 @@
def checkDirectory(self, dirpath, description, access=None, fail=False, permissions=None, uname=None, gname=None, create=None):
if not os.path.exists(dirpath):
if create is not None:
- # create is a tuple of (mode, username, groupname)
+ # create is a tuple of (mode, username, groupname)
try:
os.mkdir(dirpath)
os.chmod(dirpath, create[0])
@@ -341,9 +342,10 @@
# default resource classes
#
- rootResourceClass = RootResource
- principalResourceClass = DirectoryPrincipalProvisioningResource
- calendarResourceClass = CalendarHomeProvisioningFile
+ rootResourceClass = RootResource
+ principalResourceClass = DirectoryPrincipalProvisioningResource
+ calendarResourceClass = CalendarHomeProvisioningFile
+ servertoserverResourceClass = ServerToServerInboxFile
def makeService_Slave(self, options):
#
@@ -413,6 +415,15 @@
root.putChild('principals', principalCollection)
root.putChild('calendars', calendarCollection)
+ if config.EnableServerToServer:
+ log.msg("Setting up server-to-server resource: %r" % (self.servertoserverResourceClass,))
+
+ servertoserver = self.servertoserverResourceClass(
+ os.path.join(config.DocumentRoot, 'inbox'),
+ root,
+ )
+ root.putChild('inbox', servertoserver)
+
# Configure default ACLs on the root resource
log.msg("Setting up default ACEs on root resource")
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20070906/6dcc3d40/attachment.html
More information about the calendarserver-changes
mailing list