[CalendarServer-changes] [11216] CalDAVClientLibrary/trunk/caldavclientlibrary

source_changes at macosforge.org source_changes at macosforge.org
Fri May 17 13:57:11 PDT 2013


Revision: 11216
          http://trac.calendarserver.org//changeset/11216
Author:   cdaboo at apple.com
Date:     2013-05-17 13:57:11 -0700 (Fri, 17 May 2013)
Log Message:
-----------
Add support for sharing and notifications processing.

Modified Paths:
--------------
    CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/__init__.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/browser/utils.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/client/clientsession.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/client/principal.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/definitions/csxml.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/makecalendar.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/multiget.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/query.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/makeaddressbook.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/multiget.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/query.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/acl.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/lock.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/principalmatch.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/synccollection.py

Added Paths:
-----------
    CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/notifications.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/share.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/__init__.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/invite.py
    CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/notifications.py

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/__init__.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/__init__.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -30,6 +30,7 @@
     "mkcal",
     "mkdir",
     "mv",
+    "notifications",
     "principal",
     "props",
     "proxies",
@@ -39,6 +40,7 @@
     "quota",
     "rm",
     "server",
+    "share",
     "sync",
     "user",
     "whoami",

Added: CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/notifications.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/notifications.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/notifications.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -0,0 +1,249 @@
+##
+# Copyright (c) 2013 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 caldavclientlibrary.browser import commands
+from caldavclientlibrary.browser import utils
+from caldavclientlibrary.browser.command import Command
+from caldavclientlibrary.browser.command import WrongOptions
+from caldavclientlibrary.browser.subshell import SubShell
+import getopt
+import readline
+import shlex
+
+class Cmd(Command):
+
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("notifications",)
+        self.subshell = None
+
+
+    def execute(self, cmdname, options):
+
+        interactive = False
+
+        try:
+            opts, args = getopt.getopt(shlex.split(options), 'i')
+        except getopt.GetoptError, e:
+            print str(e)
+            print self.usage(cmdname)
+            raise WrongOptions
+
+        for name, _ignore_value in opts:
+
+            if name == "-i":
+                interactive = True
+            else:
+                print "Unknown option: %s" % (name,)
+                print self.usage(cmdname)
+                raise WrongOptions
+
+        if len(args) != 0:
+            print "Wrong number of arguments: %d" % (len(args),)
+            print self.usage(cmdname)
+            raise WrongOptions
+
+        principal = self.shell.account.getPrincipal(None)
+        resource = principal.notification_URL
+
+        notifications = self.shell.account.session.getNotifications(resource)
+        if not notifications:
+            print "No notifications."
+        else:
+            if interactive:
+                self.doInteractiveMode(resource, notifications)
+            else:
+                print utils.printNotificationsList(notifications, self.shell.account)
+
+        return True
+
+
+    def doInteractiveMode(self, resource, invites):
+
+        print "Entering notifications edit mode on resource: %s" % (resource.relativeURL(),)
+        if not self.subshell:
+            self.subshell = SubShell(self.shell, "Share", (
+                commands.help.Cmd(),
+                commands.logging.Cmd(),
+                commands.quit.Cmd(),
+                Accept(),
+                Decline(),
+                Delete(),
+                List(),
+            ))
+        self.subshell.resource = resource
+        self.subshell.account = self.shell.account
+        self.subshell.run()
+
+
+    def usage(self, name):
+        return """Usage: %s [OPTIONS] [PATH]
+PATH is a relative or absolute path.
+
+Options:
+-i    interactive mode for accepting or declining invites.
+    if not present, existing notifications will be printed.
+""" % (name,)
+
+
+    def helpDescription(self):
+        return "Manage sharing notifications of an address book, calendar or address book group."
+
+
+
+class CommonNotificationsCommand(Command):
+
+    def displayNotificationsList(self):
+        # First list the current set
+        notifications = self.shell.shell.account.session.getNotifications(self.shell.resource)
+        if not notifications:
+            print "No notifications."
+        else:
+            print utils.printNotificationsList(notifications, self.shell.account)
+        return notifications
+
+
+
+class Process(CommonNotificationsCommand):
+
+    def __init__(self, accept):
+        super(Command, self).__init__()
+        self.accept = accept
+
+
+    def execute(self, name, options):
+
+        # First list the current set
+        notifications = self.displayNotificationsList()
+        if notifications:
+            # Ask user which one to delete
+            while True:
+                result = raw_input("%s invite at [1 - %d] or cancel [q]: " % (self.cmds[0].title(), len(notifications),))
+                if readline.get_current_history_length():
+                    readline.remove_history_item(readline.get_current_history_length() - 1)
+                if not result:
+                    continue
+                if result[0] == "q":
+                    break
+                try:
+                    number = int(result) - 1
+                except ValueError:
+                    print "Invalid input, try again."
+                    continue
+
+                # Now execute and delete the notification if processed OK
+                if self.shell.shell.account.session.processNotification(notifications[number], self.accept):
+                    self.shell.shell.account.session.deleteResource(notifications[number].url)
+                break
+
+
+
+class Accept(Process):
+
+    def __init__(self):
+        super(Accept, self).__init__(True)
+        self.cmds = ("accept",)
+
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+
+    def helpDescription(self):
+        return "Remove invite on existing resource."
+
+
+
+class Decline(Process):
+
+    def __init__(self):
+        super(Decline, self).__init__(False)
+        self.cmds = ("decline",)
+
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+
+    def helpDescription(self):
+        return "Remove invite on existing resource."
+
+
+
+class Delete(CommonNotificationsCommand):
+
+    def __init__(self):
+        super(Delete, self).__init__()
+        self.cmds = ("delete",)
+
+
+    def execute(self, name, options):
+
+        # First list the current set
+        notifications = self.displayNotificationsList()
+        if notifications:
+            # Ask user which one to delete
+            while True:
+                result = raw_input("%s invite at [1 - %d] or cancel [q]: " % (self.cmds[0].title(), len(notifications),))
+                if readline.get_current_history_length():
+                    readline.remove_history_item(readline.get_current_history_length() - 1)
+                if not result:
+                    continue
+                if result[0] == "q":
+                    break
+                try:
+                    number = int(result) - 1
+                except ValueError:
+                    print "Invalid input, try again."
+                    continue
+
+                # Now delete the notification
+                self.shell.shell.account.session.deleteResource(notifications[number].url)
+                break
+
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+
+    def helpDescription(self):
+        return "Delete notification resource."
+
+
+
+class List(CommonNotificationsCommand):
+
+    def __init__(self):
+        super(List, self).__init__()
+        self.cmds = ("list",)
+
+
+    def execute(self, name, options):
+
+        self.displayNotificationsList()
+        return True
+
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+
+    def helpDescription(self):
+        return "List current invites for user."

Added: CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/share.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/share.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/browser/commands/share.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -0,0 +1,313 @@
+##
+# Copyright (c) 2013 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 caldavclientlibrary.browser import commands
+from caldavclientlibrary.browser import utils
+from caldavclientlibrary.browser.command import Command
+from caldavclientlibrary.browser.command import WrongOptions
+from caldavclientlibrary.browser.subshell import SubShell
+from caldavclientlibrary.protocol.url import URL
+import getopt
+import os
+import readline
+import shlex
+
+class Cmd(Command):
+
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("share",)
+        self.subshell = None
+
+
+    def execute(self, cmdname, options):
+
+        interactive = False
+        path = None
+        addressbook = False
+        calendar = False
+        group = False
+
+        try:
+            opts, args = getopt.getopt(shlex.split(options), 'acgi')
+        except getopt.GetoptError, e:
+            print str(e)
+            print self.usage(cmdname)
+            raise WrongOptions
+
+        for name, _ignore_value in opts:
+
+            if name == "-i":
+                interactive = True
+            elif name == "-a":
+                if calendar or group:
+                    print "Only one of -a, -c, or -g must be present"
+                    print self.usage(cmdname)
+                    raise WrongOptions
+                addressbook = True
+            elif name == "-c":
+                if addressbook or group:
+                    print "Only one of -a, -c, or -g must be present"
+                    print self.usage(cmdname)
+                    raise WrongOptions
+                calendar = True
+            elif name == "-c":
+                if addressbook or calendar:
+                    print "Only one of -a, -c, or -g must be present"
+                    print self.usage(cmdname)
+                    raise WrongOptions
+                group = True
+            else:
+                print "Unknown option: %s" % (name,)
+                print self.usage(cmdname)
+                raise WrongOptions
+
+        if not (addressbook or calendar or group):
+            print "One of -a, -c, or -g must be present"
+            print self.usage(cmdname)
+            raise WrongOptions
+
+        if len(args) > 1:
+            print "Wrong number of arguments: %d" % (len(args),)
+            print self.usage(cmdname)
+            raise WrongOptions
+        elif args:
+            path = args[0]
+            if not path.startswith("/"):
+                path = os.path.join(self.shell.wd, path)
+        else:
+            path = self.shell.wd
+        if not path.endswith("/"):
+            path += "/"
+        resource = URL(url=path)
+
+        invites = self.shell.account.session.getInvites(resource)
+        if invites is None:
+            print "Could not retrieve CS:invite property."
+        else:
+            if interactive:
+                self.doInteractiveMode(resource, invites)
+            else:
+                print utils.printInviteList(invites, self.shell.account)
+
+        return True
+
+
+    def doInteractiveMode(self, resource, invites):
+
+        print "Entering sharing edit mode on resource: %s" % (resource.relativeURL(),)
+        if not self.subshell:
+            self.subshell = SubShell(self.shell, "Share", (
+                commands.help.Cmd(),
+                commands.logging.Cmd(),
+                commands.quit.Cmd(),
+                Add(),
+                Change(),
+                Remove(),
+                List(),
+            ))
+        self.subshell.resource = resource
+        self.subshell.account = self.shell.account
+        self.subshell.run()
+
+
+    def usage(self, name):
+        return """Usage: %s [OPTIONS] [PATH]
+PATH is a relative or absolute path.
+
+Options:
+-i    interactive mode for adding, changing and deleting invitees.
+    if not present, existing invitees will be printed.
+
+-a    sharing for an address book
+-c    sharing for a calendar
+-g    sharing for an address book group
+
+One of -a, -c or -g must be present.
+""" % (name,)
+
+
+    def helpDescription(self):
+        return "Manage sharing of an address book, calendar or address book group."
+
+
+
+class CommonSharingCommand(Command):
+
+    def displayInviteList(self):
+        # First list the current set
+        invites = self.shell.shell.account.session.getInvites(self.shell.resource)
+        if invites is None:
+            print "Could not retrieve CS:invite property."
+        else:
+            print utils.printInviteList(invites, self.shell.account)
+        return invites
+
+
+    def createInvite(self, oldinvite=None):
+
+        if oldinvite is None:
+            href = utils.textInput("Enter principal id: ", None)
+            if href.startswith("user"):
+                href = "/principals/users/%s" % (href,)
+            principal = self.shell.shell.account.getPrincipal(URL(url=href))
+            user_uid = principal.principalURL.relativeURL()
+        else:
+            user_uid = oldinvite.user_uid
+
+        oldmode = "w" if oldinvite is None else ("w" if oldinvite.access == "read-write" else "r")
+        read_write = utils.choiceInput("Read or Read-Write Mode [r/w]: ", ("r", "w",), insert=oldmode)
+        read_write = (read_write == "w")
+
+        summary = utils.textInput("Summary: ", insert=(oldinvite.summary if oldinvite else "Shared"))
+
+        return user_uid, read_write, summary
+
+
+
+class Add(CommonSharingCommand):
+
+    def __init__(self):
+        super(Add, self).__init__()
+        self.cmds = ("add",)
+
+
+    def execute(self, name, options):
+
+        # Try and get the new details
+        user_uid, read_write, summary = self.createInvite()
+
+        # Now execute
+        self.shell.shell.account.session.addInvitee(self.shell.resource, user_uid, read_write, summary)
+
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+
+    def helpDescription(self):
+        return "Add invite to existing resource."
+
+
+
+class Change(CommonSharingCommand):
+
+    def __init__(self):
+        super(Change, self).__init__()
+        self.cmds = ("change",)
+
+
+    def execute(self, name, options):
+
+        # First list the current set
+        invites = self.displayInviteList()
+        if len(invites.invitees):
+            # Ask user which one to delete
+            while True:
+                result = raw_input("Change invite at [1 - %d] or cancel [q]: " % (len(invites.invitees),))
+                if readline.get_current_history_length():
+                    readline.remove_history_item(readline.get_current_history_length() - 1)
+                if not result:
+                    continue
+                if result[0] == "q":
+                    break
+                try:
+                    number = int(result) - 1
+                except ValueError:
+                    print "Invalid input, try again."
+                    continue
+
+                # Try and get the new details
+                user_uid, read_write, summary = self.createInvite(invites.invitees[number])
+
+                # Now execute
+                self.shell.shell.account.session.addInvitee(self.shell.resource, user_uid, read_write, summary)
+                break
+
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+
+    def helpDescription(self):
+        return "Change invite on existing resource."
+
+
+
+class Remove(CommonSharingCommand):
+
+    def __init__(self):
+        super(Remove, self).__init__()
+        self.cmds = ("remove",)
+
+
+    def execute(self, name, options):
+
+        # First list the current set
+        invites = self.displayInviteList()
+        if len(invites.invitees):
+            # Ask user which one to delete
+            while True:
+                result = raw_input("Remove invite [1 - %d] or cancel [q]: " % (len(invites.invitees),))
+                if readline.get_current_history_length():
+                    readline.remove_history_item(readline.get_current_history_length() - 1)
+                if not result:
+                    continue
+                if result[0] == "q":
+                    break
+                try:
+                    number = int(result) - 1
+                except ValueError:
+                    print "Invalid input, try again."
+                    continue
+
+                # Now execute
+                self.shell.shell.account.session.removeInvitee(self.shell.resource, invites.invitees[number])
+                break
+
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+
+    def helpDescription(self):
+        return "Remove invite on existing resource."
+
+
+
+class List(CommonSharingCommand):
+
+    def __init__(self):
+        super(List, self).__init__()
+        self.cmds = ("list",)
+
+
+    def execute(self, name, options):
+
+        self.displayInviteList()
+        return True
+
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+
+    def helpDescription(self):
+        return "List current invitees on existing resource."

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/browser/utils.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/browser/utils.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/browser/utils.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -14,6 +14,8 @@
 # limitations under the License.
 ##
 
+from caldavclientlibrary.protocol.calendarserver.notifications import InviteNotification, \
+    InviteReply
 from caldavclientlibrary.protocol.url import URL
 from caldavclientlibrary.protocol.webdav.definitions import davxml
 import readline
@@ -162,6 +164,59 @@
 
 
 
+def printInviteList(invites, account):
+
+    result = "Organizer: %s (%s)" % (invites.organizer_cn, invites.organizer_uid,)
+    for ctr, user in enumerate(invites.invitees):
+        result += "\n% 2d. %s" % (ctr + 1, printInviteUser(user, account))
+    return result
+
+
+
+def printInviteUser(user, account):
+
+    return "%s (%s) Invite: %s  Access: %s  Summary: %s" % (
+        user.user_cn,
+        user.user_uid,
+        user.mode,
+        user.access,
+        user.summary,
+    )
+
+
+
+def printNotificationsList(notifications, account):
+
+    result = ""
+    if notifications:
+        for ctr, notification in enumerate(notifications):
+            result += "\n% 2d. %s" % (ctr + 1, printNotification(notification, account))
+    else:
+        "No notifications."
+    return result
+
+
+
+def printNotification(notification, account):
+
+    if isinstance(notification, InviteNotification):
+        return "Sharing Invite: From: %s (%s)  Access: %s  Summary: %s  Host-URL: %s" % (
+            notification.organizer_cn,
+            notification.organizer_uid,
+            notification.access,
+            notification.summary,
+            notification.hosturl,
+        )
+    elif isinstance(notification, InviteReply):
+        return "Sharing Reply: From: %s  Result: %s  Summary: %s  Host-URL: %s" % (
+            notification.user_uid,
+            notification.mode,
+            notification.summary,
+            notification.hosturl,
+        )
+
+
+
 def textInput(title, insert=None):
     if insert:
         title = "%s [%s]:" % (title, insert,)

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/client/clientsession.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/client/clientsession.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/client/clientsession.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -15,14 +15,19 @@
 # #
 
 from caldavclientlibrary.client.httpshandler import SmartHTTPConnection
-from caldavclientlibrary.protocol.caldav.definitions import headers
+from caldavclientlibrary.protocol.caldav.definitions import headers, csxml
 from caldavclientlibrary.protocol.caldav.makecalendar import MakeCalendar
 from caldavclientlibrary.protocol.caldav.multiget import Multiget as CalMultiget
 from caldavclientlibrary.protocol.caldav.query import QueryVEVENTTimeRange
+from caldavclientlibrary.protocol.calendarserver.invite import RemoveInvitee, Invites, \
+    AddInvitee
 from caldavclientlibrary.protocol.carddav.makeaddressbook import MakeAddressBook
 from caldavclientlibrary.protocol.carddav.multiget import Multiget as AdbkMultiget
 from caldavclientlibrary.protocol.http.authentication.basic import Basic
 from caldavclientlibrary.protocol.http.authentication.digest import Digest
+import urllib
+from caldavclientlibrary.protocol.calendarserver.notifications import InviteNotification, \
+    InviteReply, ProcessNotification
 try:
     from caldavclientlibrary.protocol.http.authentication.gssapi import Kerberos
 except ImportError:
@@ -46,7 +51,7 @@
 from caldavclientlibrary.protocol.webdav.put import Put
 from caldavclientlibrary.protocol.webdav.session import Session
 from caldavclientlibrary.protocol.webdav.synccollection import SyncCollection
-from xml.etree.ElementTree import Element, tostring
+from xml.etree.ElementTree import Element, tostring, XML
 import types
 
 class CalDAVSession(Session):
@@ -751,6 +756,133 @@
             self.handleHTTPError(request)
 
 
+    def getInvites(self, rurl):
+        """
+        Get the invitation details for the specified resource by reading and parsing the CS:invite WebDAV property.
+
+        @param rurl: the resource whose property is to be read
+        @type rurl: L{URL}
+        """
+
+        assert(isinstance(rurl, URL))
+
+        results, bad = self.getProperties(rurl, (csxml.invite,))
+        if csxml.invite in bad:
+            return None
+        else:
+            return Invites().parseFromInvite(results.get(csxml.invite))
+
+
+    def addInvitee(self, rurl, user_uid, read_write, summary=None):
+        """
+        Add a sharing invite for the specified resource.
+
+        @param rurl: the resource to share
+        @type rurl: L{URL}
+        @param user_uid: short name or full principal path of user to share with
+        @type user_uid: C{str}
+        @param read_write: whether to share read-only C{False} or read-write C{True}
+        @type read_write: C{bool}
+        @param summary: summary description for the share
+        @type summary: C{str}
+        """
+
+        assert(isinstance(rurl, URL))
+
+        # Add invitation POST
+        request = AddInvitee(self, rurl.relativeURL(), user_uid, read_write, summary)
+
+        # Process it
+        self.runSession(request)
+
+        if request.getStatusCode() not in (statuscodes.OK, statuscodes.NoContent,):
+            self.handleHTTPError(request)
+
+
+    def removeInvitee(self, rurl, invitee):
+        """
+        Remove an invite from a shared resource.
+
+        @param rurl: the resource currently being shared
+        @type rurl: L{URL}
+        @param invitee: invite DAV:href for the user being removed
+        @type invitee: C{str}
+        """
+
+        assert(isinstance(rurl, URL))
+
+        # Remove invitation POST
+        request = RemoveInvitee(self, rurl.relativeURL(), invitee)
+
+        # Process it
+        self.runSession(request)
+
+        if request.getStatusCode() not in (statuscodes.OK, statuscodes.NoContent,):
+            self.handleHTTPError(request)
+
+
+    def getNotifications(self, rurl):
+        """
+        Get a list of L{Notification} objects for the specified notification collection.
+
+        @param rurl: a user's notification collection URL
+        @type rurl: L{URL}
+        """
+
+        assert(isinstance(rurl, URL))
+
+        # List all children of the notification collection
+        results = self.getPropertiesOnHierarchy(rurl, (davxml.getcontenttype,))
+        items = results.keys()
+        items.sort()
+        notifications = []
+        for path in items:
+            path = urllib.unquote(path)
+            nurl = URL(url=path)
+            if rurl == nurl:
+                continue
+            props = results[path]
+            if props.get(davxml.getcontenttype, "none").split(";")[0] in ("text/xml", "application/xml"):
+                data, _ignore_etag = self.readData(URL(url=path))
+                node = XML(data)
+                if node.tag == str(csxml.notification):
+                    for child in node.getchildren():
+                        if child.tag == str(csxml.invite_notification):
+                            notifications.append(InviteNotification().parseFromNotification(nurl, child))
+                        elif child.tag == str(csxml.invite_reply):
+                            notifications.append(InviteReply().parseFromNotification(nurl, child))
+
+        return notifications
+
+
+    def processNotification(self, notification, accept):
+        """
+        Accept or decline a sharing invite in the specified notification.
+
+        @param notification: the notification
+        @type notification: L{InviteNotification}
+        @param accept: whether to accept C{True} or decline C{False} the invite
+        @type accept: C{bool}
+        """
+
+        assert(isinstance(notification.url, URL))
+
+        # POST goes to home which is two segments up from the notification resource
+        rurl = notification.url.dirname().dirname()
+
+        # Add invitation POST
+        request = ProcessNotification(self, rurl.relativeURL(), notification, accept)
+
+        # Process it
+        self.runSession(request)
+
+        if request.getStatusCode() not in (statuscodes.OK, statuscodes.NoContent,):
+            self.handleHTTPError(request)
+            return False
+        else:
+            return True
+
+
     def addAttachment(self, rurl, filename, data, contentType, return_representation):
 
         assert(isinstance(rurl, URL))

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/client/principal.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/client/principal.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/client/principal.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -16,7 +16,7 @@
 
 from caldavclientlibrary.client.addressbook import AddressBook
 from caldavclientlibrary.client.calendar import Calendar
-from caldavclientlibrary.protocol.caldav.definitions import caldavxml
+from caldavclientlibrary.protocol.caldav.definitions import caldavxml, csxml
 from caldavclientlibrary.protocol.caldav.definitions import headers
 from caldavclientlibrary.protocol.carddav.definitions import carddavxml
 from caldavclientlibrary.protocol.url import URL
@@ -111,6 +111,7 @@
         self.inboxURL = ""
         self.cuaddrs = ()
         self.adbkhomeset = ()
+        self.notificationsURL = ""
 
         self.proxyFor = None
         self.proxyreadURL = ""
@@ -136,6 +137,7 @@
                 caldavxml.schedule_inbox_URL,
                 caldavxml.calendar_user_address_set,
                 carddavxml.addressbook_home_set,
+                csxml.notification_URL,
             ),
         )
         if results:
@@ -159,6 +161,7 @@
                 self.inboxURL = results.get(caldavxml.schedule_inbox_URL, None)
                 self.cuaddrs = make_tuple(results.get(caldavxml.calendar_user_address_set, ()))
                 self.adbkhomeset = make_tuple(results.get(carddavxml.addressbook_home_set, ()))
+                self.notification_URL = results.get(csxml.notification_URL, None)
 
         # Get proxy resource details if proxy support is available
         if self.session.hasDAVVersion(headers.calendar_proxy) and not self.proxyFor:

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/definitions/csxml.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/definitions/csxml.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/definitions/csxml.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -33,3 +33,35 @@
 xmpp_server = QName(CSNamespace, "xmpp-server")
 xmpp_uri = QName(CSNamespace, "xmpp-uri")
 pushkey = QName(CSNamespace, "pushkey")
+
+# Sharing
+invite = QName(CSNamespace, "invite")
+organizer = QName(CSNamespace, "organizer")
+user = QName(CSNamespace, "user")
+common_name = QName(CSNamespace, "common-name")
+first_name = QName(CSNamespace, "first-name")
+last_name = QName(CSNamespace, "last-name")
+
+invite_noresponse = QName(CSNamespace, "invite-noresponse")
+invite_accepted = QName(CSNamespace, "invite-accepted")
+invite_declined = QName(CSNamespace, "invite-declined")
+invite_invalid = QName(CSNamespace, "invite-invalid")
+
+access = QName(CSNamespace, "access")
+read = QName(CSNamespace, "read")
+summary = QName(CSNamespace, "summary")
+
+read_write = QName(CSNamespace, "read-write")
+
+share = QName(CSNamespace, "share")
+set = QName(CSNamespace, "set")
+remove = QName(CSNamespace, "remove")
+
+notification_URL = QName(CSNamespace, "notification-URL")
+notification = QName(CSNamespace, "notification")
+dtstamp = QName(CSNamespace, "dtstamp")
+invite_notification = QName(CSNamespace, "invite-notification")
+invite_reply = QName(CSNamespace, "invite-reply")
+uid = QName(CSNamespace, "uid")
+hosturl = QName(CSNamespace, "hosturl")
+in_reply_to = QName(CSNamespace, "in-reply-to")

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/makecalendar.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/makecalendar.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/makecalendar.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -40,7 +40,7 @@
             # Write XML info to a string
             os = StringIO()
             self.generateXML(os)
-            self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+            self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
 
 
     def generateXML(self, os):

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/multiget.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/multiget.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/multiget.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -37,7 +37,7 @@
         # Write XML info to a string
         os = StringIO()
         self.generateXML(os)
-        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
 
 
     def generateXML(self, os):

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/query.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/query.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/caldav/query.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -36,7 +36,7 @@
         # Write XML info to a string
         os = StringIO()
         self.generateXML(os)
-        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
 
 
     def generateXML(self, os):

Added: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/__init__.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 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: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/invite.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/invite.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/invite.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -0,0 +1,198 @@
+##
+# Copyright (c) 2013 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 caldavclientlibrary.protocol.caldav.definitions import csxml
+from caldavclientlibrary.protocol.webdav.definitions import davxml
+from caldavclientlibrary.protocol.webdav.post import Post
+from caldavclientlibrary.protocol.http.data.string import RequestDataString
+from xml.etree.ElementTree import Element, SubElement
+from caldavclientlibrary.protocol.utils.xmlhelpers import BetterElementTree
+
+
+def userNameFromNode(node):
+    cn = node.find(str(csxml.common_name))
+    first = node.find(str(csxml.first_name))
+    last = node.find(str(csxml.last_name))
+
+    return cn.text if cn is not None else ("%s %s" % (first.text, last.text) if first is not None or last is not None else "")
+
+
+
+class Invites(object):
+    """
+    Represents a list of invites to sharees for a shared resource.
+    """
+
+    def __init__(self):
+
+        self.organizer_uid = None
+        self.organizer_cn = ""
+        self.invitees = []
+
+
+    def parseFromInvite(self, invite):
+
+        if invite is not None:
+            organizer = invite.find(str(csxml.organizer))
+            if organizer is not None:
+                self.organizer_uid = organizer.find(str(davxml.href)).text
+                self.organizer_cn = userNameFromNode(organizer)
+
+            for user in invite.findall(str(csxml.user)):
+                self.invitees.append(InviteUser().parseFromUser(user))
+
+        return self
+
+
+
+class InviteUser(object):
+    """
+    An invite for a specific sharee.
+    """
+
+    def __init__(self):
+
+        self.user_uid = None
+        self.user_cn = ""
+        self.mode = "unknown"
+        self.access = "unknown"
+        self.summary = "-"
+
+
+    def parseFromUser(self, user):
+
+        self.user_uid = user.find(str(davxml.href)).text
+        self.user_cn = userNameFromNode(user)
+        if user.find(str(csxml.invite_noresponse)) is not None:
+            self.mode = "no-response"
+        elif user.find(str(csxml.invite_accepted)) is not None:
+            self.mode = "accepted"
+        elif user.find(str(csxml.invite_declined)) is not None:
+            self.mode = "declined"
+        elif user.find(str(csxml.invite_invalid)) is not None:
+            self.mode = "invalid"
+
+        access = user.find(str(csxml.access))
+        if access.find(str(csxml.read)) is not None:
+            self.access = "read"
+        elif access.find(str(csxml.read_write)) is not None:
+            self.access = "read-write"
+
+        summary = user.find(str(csxml.summary))
+        if summary is not None:
+            self.summary = summary.text
+
+        return self
+
+
+
+class AddInvitee(Post):
+    """
+    HTTP POST request to add an invite for a sharee.
+    """
+
+    def __init__(self, session, url, user_uid, read_write, summary=None):
+        super(AddInvitee, self).__init__(session, url)
+        self.user_uid = user_uid
+        self.read_write = read_write
+        self.summary = summary if summary is not None else "-"
+
+        self.initRequestData()
+
+
+    def initRequestData(self):
+        # Write XML info to a string
+        os = StringIO()
+        self.generateXML(os)
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
+
+
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <CS:share>
+        #   <CS:set>
+        #     <DAV:href>...</DAV:href>
+        #     <CS:summary>...</CS:summary>
+        #     <CS:read /> | <CS:read-write />
+        #   </CS:set>
+        # </CS:share>
+
+        # <CS:share> element
+        share = Element(csxml.share)
+
+        # <CS:set> element
+        set = SubElement(share, csxml.set)
+
+        # <DAV:href> element
+        href = SubElement(set, davxml.href)
+        href.text = self.user_uid
+
+        # <CS:summary> element
+        summary = SubElement(set, csxml.summary)
+        summary.text = self.summary
+
+        # <CS:read /> | <CS:read-write />
+        SubElement(set, csxml.read_write if self.read_write else csxml.read)
+
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(share)
+        xmldoc.writeUTF8(os)
+
+
+
+class RemoveInvitee(Post):
+    """
+    HTTP POST request to remove an invite for a sharee.
+    """
+
+    def __init__(self, session, url, invitee):
+        super(RemoveInvitee, self).__init__(session, url)
+        self.invitee = invitee
+
+        self.initRequestData()
+
+
+    def initRequestData(self):
+        # Write XML info to a string
+        os = StringIO()
+        self.generateXML(os)
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
+
+
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <CS:share>
+        #   <CS:remove>
+        #     <DAV:href>...</DAV:href>
+        #   </CS:remove>
+        # </CS:share>
+
+        # <CS:share> element
+        share = Element(csxml.share)
+
+        # <CS:remove> element
+        remove = SubElement(share, csxml.remove)
+
+        # <DAV:href> element
+        href = SubElement(remove, davxml.href)
+        href.text = self.invitee.user_uid
+
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(share)
+        xmldoc.writeUTF8(os)

Added: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/notifications.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/notifications.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/calendarserver/notifications.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -0,0 +1,177 @@
+##
+# Copyright (c) 2013 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 caldavclientlibrary.protocol.caldav.definitions import csxml
+from caldavclientlibrary.protocol.webdav.definitions import davxml
+from caldavclientlibrary.protocol.webdav.post import Post
+from caldavclientlibrary.protocol.http.data.string import RequestDataString
+from xml.etree.ElementTree import Element, SubElement
+from caldavclientlibrary.protocol.utils.xmlhelpers import BetterElementTree
+
+
+def userNameFromNode(node):
+    cn = node.find(str(csxml.common_name))
+    first = node.find(str(csxml.first_name))
+    last = node.find(str(csxml.last_name))
+
+    return cn.text if cn is not None else ("%s %s" % (first.text, last.text) if first is not None or last is not None else "")
+
+
+
+class InviteNotification(object):
+    """
+    An invite notification sent to sharees.
+    """
+
+    def __init__(self):
+
+        self.url = None
+        self.uid = ""
+        self.user_uid = ""
+        self.access = "unknown"
+        self.hosturl = ""
+        self.organizer_uid = None
+        self.organizer_cn = None
+        self.summary = "-"
+
+
+    def parseFromNotification(self, url, notification):
+
+        self.url = url
+
+        self.uid = notification.find(str(csxml.uid)).text
+        self.user_uid = notification.find(str(davxml.href)).text
+
+        access = notification.find(str(csxml.access))
+        if access.find(str(csxml.read)) is not None:
+            self.access = "read"
+        elif access.find(str(csxml.read_write)) is not None:
+            self.access = "read-write"
+
+        hosturl = notification.find(str(csxml.hosturl))
+        if hosturl is not None:
+            self.hosturl = hosturl.find(str(davxml.href)).text
+
+        organizer = notification.find(str(csxml.organizer))
+        if organizer is not None:
+            self.organizer_uid = organizer.find(str(davxml.href)).text
+            self.organizer_cn = userNameFromNode(organizer)
+
+        summary = notification.find(str(csxml.summary))
+        if summary is not None:
+            self.summary = summary.text
+
+        return self
+
+
+
+class InviteReply(object):
+    """
+    An invite reply sent to the sharer.
+    """
+
+    def __init__(self):
+
+        self.url = None
+        self.user_uid = ""
+        self.mode = "unknown"
+        self.hosturl = ""
+        self.in_reply_to = ""
+        self.summary = "-"
+
+
+    def parseFromNotification(self, url, notification):
+
+        self.url = url
+
+        self.user_uid = notification.find(str(davxml.href)).text
+
+        if notification.find(str(csxml.invite_accepted)) is not None:
+            self.mode = "accepted"
+        elif notification.find(str(csxml.invite_declined)) is not None:
+            self.mode = "declined"
+
+        hosturl = notification.find(str(csxml.hosturl))
+        if hosturl is not None:
+            self.hosturl = hosturl.find(str(davxml.href)).text
+
+        in_reply_to = notification.find(str(csxml.in_reply_to))
+        if in_reply_to is not None:
+            self.in_reply_to = in_reply_to.text
+
+        summary = notification.find(str(csxml.summary))
+        if summary is not None:
+            self.summary = summary.text
+
+        return self
+
+
+
+class ProcessNotification(Post):
+    """
+    HTTP POST request to accept or decline a sharee's invite notification.
+    """
+
+    def __init__(self, session, url, notification, accepted):
+        super(ProcessNotification, self).__init__(session, url)
+        self.notification = notification
+        self.accepted = accepted
+
+        self.initRequestData()
+
+
+    def initRequestData(self):
+        # Write XML info to a string
+        os = StringIO()
+        self.generateXML(os)
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
+
+
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <CS:invite-reply>
+        #   <DAV:href>...</DAV:href>
+        #   <CS:invite-accepted /> | <CS:invite-declined />
+        #   <CS:hosturl>...</CS:hosturl>
+        #   <CS:in-reply-to>...</CS:in-reply-to>
+        # </CS:invite-reply>
+
+        # <CS:invite-reply> element
+        invite_reply = Element(csxml.invite_reply)
+
+        # <DAV:href> element
+        href = SubElement(invite_reply, davxml.href)
+        href.text = self.notification.user_uid
+
+        # <CS:invite-accepted /> | <CS:invite-declined />
+        SubElement(invite_reply, csxml.invite_accepted if self.accepted else csxml.invite_declined)
+
+        # <CS:hosturl> element
+        hosturl = SubElement(invite_reply, csxml.hosturl)
+
+        # <DAV:href> element
+        href = SubElement(hosturl, davxml.href)
+        href.text = self.notification.hosturl
+
+        # <CS:in-reply-to> element
+        in_reply_to = SubElement(invite_reply, csxml.in_reply_to)
+        in_reply_to.text = self.notification.uid
+
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(invite_reply)
+        xmldoc.writeUTF8(os)

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/makeaddressbook.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/makeaddressbook.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/makeaddressbook.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -37,7 +37,7 @@
         # Write XML info to a string
         os = StringIO()
         self.generateXML(os)
-        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
 
 
     def generateXML(self, os):

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/multiget.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/multiget.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/multiget.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -37,7 +37,7 @@
         # Write XML info to a string
         os = StringIO()
         self.generateXML(os)
-        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
 
 
     def generateXML(self, os):

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/query.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/query.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/carddav/query.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -36,7 +36,7 @@
         # Write XML info to a string
         os = StringIO()
         self.generateXML(os)
-        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
 
 
     def generateXML(self, os):

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/acl.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/acl.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/acl.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -35,7 +35,7 @@
         # Write XML info to a string
         os = StringIO()
         self.generateXML(os)
-        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
 
 
     def generateXML(self, os):

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/lock.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/lock.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/lock.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -62,7 +62,7 @@
         # Write XML info to a string
         os = StringIO()
         self.generateXML(os)
-        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
 
 
     def addHeaders(self, hdrs):

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/principalmatch.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/principalmatch.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/principalmatch.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -38,7 +38,7 @@
         # Write XML info to a string
         os = StringIO()
         self.generateXML(os)
-        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
 
 
     def generateXML(self, os):

Modified: CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/synccollection.py
===================================================================
--- CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/synccollection.py	2013-05-17 16:56:21 UTC (rev 11215)
+++ CalDAVClientLibrary/trunk/caldavclientlibrary/protocol/webdav/synccollection.py	2013-05-17 20:57:11 UTC (rev 11216)
@@ -39,7 +39,7 @@
         # Write XML info to a string
         os = StringIO()
         self.generateXML(os)
-        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+        self.request_data = RequestDataString(os.getvalue(), "text/xml;charset=utf-8")
 
 
     def addHeaders(self, hdrs):
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130517/ede2a3d9/attachment-0001.html>


More information about the calendarserver-changes mailing list