Browse Source

Add a (not tested) CardDAV support

Guillaume Ayoub 14 năm trước cách đây
mục cha
commit
8a4be02075
3 tập tin đã thay đổi với 259 bổ sung193 xóa
  1. 84 74
      radicale/__init__.py
  2. 113 69
      radicale/ical.py
  3. 62 50
      radicale/xmlutils.py

+ 84 - 74
radicale/__init__.py

@@ -107,7 +107,7 @@ class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
 
 
 class Application(object):
-    """WSGI application managing calendars."""
+    """WSGI application managing collections."""
     def __init__(self):
         """Initialize application."""
         super(Application, self).__init__()
@@ -180,8 +180,8 @@ class Application(object):
         else:
             content = None
 
-        # Find calendar(s)
-        items = ical.Calendar.from_path(
+        # Find collection(s)
+        items = ical.Collection.from_path(
             environ["PATH_INFO"], environ.get("HTTP_DEPTH", "0"))
 
         # Get function corresponding to method
@@ -189,7 +189,7 @@ class Application(object):
 
         # Check rights
         if not items or not self.acl:
-            # No calendar or no acl, don't check rights
+            # No collection or no acl, don't check rights
             status, headers, answer = function(environ, items, content, None)
         else:
             # Ask authentication backend to check rights
@@ -203,37 +203,37 @@ class Application(object):
                 user = password = None
 
             last_allowed = None
-            calendars = []
-            for calendar in items:
-                if not isinstance(calendar, ical.Calendar):
+            collections = []
+            for collection in items:
+                if not isinstance(collection, ical.Collection):
                     if last_allowed:
-                        calendars.append(calendar)
+                        collections.append(collection)
                     continue
 
-                if calendar.owner in acl.PUBLIC_USERS:
-                    log.LOGGER.info("Public calendar")
-                    calendars.append(calendar)
+                if collection.owner in acl.PUBLIC_USERS:
+                    log.LOGGER.info("Public collection")
+                    collections.append(collection)
                     last_allowed = True
                 else:
                     log.LOGGER.info(
-                        "Checking rights for calendar owned by %s" % (
-                            calendar.owner or "nobody"))
-                    if self.acl.has_right(calendar.owner, user, password):
+                        "Checking rights for collection owned by %s" % (
+                            collection.owner or "nobody"))
+                    if self.acl.has_right(collection.owner, user, password):
                         log.LOGGER.info(
                             "%s allowed" % (user or "Anonymous user"))
-                        calendars.append(calendar)
+                        collections.append(collection)
                         last_allowed = True
                     else:
                         log.LOGGER.info(
                             "%s refused" % (user or "Anonymous user"))
                         last_allowed = False
 
-            if calendars:
-                # Calendars found
+            if collections:
+                # Collections found
                 status, headers, answer = function(
-                    environ, calendars, content, user)
+                    environ, collections, content, user)
             elif user and last_allowed is None:
-                # Good user and no calendars found, redirect user to home
+                # Good user and no collections found, redirect user to home
                 location = "/%s/" % str(quote(user))
                 log.LOGGER.info("redirecting to %s" % location)
                 status = client.FOUND
@@ -264,21 +264,21 @@ class Application(object):
     # All these functions must have the same parameters, some are useless
     # pylint: disable=W0612,W0613,R0201
 
-    def delete(self, environ, calendars, content, user):
+    def delete(self, environ, collections, content, user):
         """Manage DELETE request."""
-        calendar = calendars[0]
+        collection = collections[0]
 
-        if calendar.local_path == environ["PATH_INFO"].strip("/"):
-            # Path matching the calendar, the item to delete is the calendar
-            item = calendar
+        if collection.local_path == environ["PATH_INFO"].strip("/"):
+            # Path matching the collection, the collection must be deleted
+            item = collection
         else:
             # Try to get an item matching the path
-            item = calendar.get_item(
-                xmlutils.name_from_path(environ["PATH_INFO"], calendar))
+            item = collection.get_item(
+                xmlutils.name_from_path(environ["PATH_INFO"], collection))
 
         if item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag:
             # No ETag precondition or precondition verified, delete item
-            answer = xmlutils.delete(environ["PATH_INFO"], calendar)
+            answer = xmlutils.delete(environ["PATH_INFO"], collection)
             status = client.NO_CONTENT
         else:
             # No item or ETag precondition not verified, do not delete item
@@ -286,7 +286,7 @@ class Application(object):
             status = client.PRECONDITION_FAILED
         return status, {}, answer
 
-    def get(self, environ, calendars, content, user):
+    def get(self, environ, collections, content, user):
         """Manage GET request."""
         # Display a "Radicale works!" message if the root URL is requested
         if environ["PATH_INFO"] == "/":
@@ -294,67 +294,77 @@ class Application(object):
             answer = "<!DOCTYPE html>\n<title>Radicale</title>Radicale works!"
             return client.OK, headers, answer
 
-        calendar = calendars[0]
-        item_name = xmlutils.name_from_path(environ["PATH_INFO"], calendar)
+        collection = collections[0]
+        item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection)
         if item_name:
-            # Get calendar item
-            item = calendar.get_item(item_name)
+            # Get collection item
+            item = collection.get_item(item_name)
             if item:
-                items = calendar.timezones
+                items = collection.timezones
                 items.append(item)
                 answer_text = ical.serialize(
-                    headers=calendar.headers, items=items)
+                    collection.tag, collection.headers, items)
                 etag = item.etag
             else:
                 return client.GONE, {}, None
         else:
-            # Get whole calendar
-            answer_text = calendar.text
-            etag = calendar.etag
+            # Get whole collection
+            answer_text = collection.text
+            etag = collection.etag
 
         headers = {
-            "Content-Type": "text/calendar",
-            "Last-Modified": calendar.last_modified,
+            "Content-Type": collection.mimetype,
+            "Last-Modified": collection.last_modified,
             "ETag": etag}
         answer = answer_text.encode(self.encoding)
         return client.OK, headers, answer
 
-    def head(self, environ, calendars, content, user):
+    def head(self, environ, collections, content, user):
         """Manage HEAD request."""
-        status, headers, answer = self.get(environ, calendars, content, user)
+        status, headers, answer = self.get(environ, collections, content, user)
         return status, headers, None
 
-    def mkcalendar(self, environ, calendars, content, user):
+    def mkcalendar(self, environ, collections, content, user):
         """Manage MKCALENDAR request."""
-        calendar = calendars[0]
+        collection = collections[0]
         props = xmlutils.props_from_request(content)
         timezone = props.get('C:calendar-timezone')
         if timezone:
-            calendar.replace('', timezone)
+            collection.replace('', timezone)
             del props['C:calendar-timezone']
-        with calendar.props as calendar_props:
+        with collection.props as collection_props:
             for key, value in props.items():
-                calendar_props[key] = value
-        calendar.write()
+                collection_props[key] = value
+        collection.write()
         return client.CREATED, {}, None
 
-    def move(self, environ, calendars, content, user):
+    def mkcol(self, environ, collections, content, user):
+        """Manage MKCOL request."""
+        collection = collections[0]
+        props = xmlutils.props_from_request(content)
+        with collection.props as collection_props:
+            for key, value in props.items():
+                collection_props[key] = value
+        collection.write()
+        return client.CREATED, {}, None
+
+    def move(self, environ, collections, content, user):
         """Manage MOVE request."""
-        from_calendar = calendars[0]
+        from_collection = collections[0]
         from_name = xmlutils.name_from_path(
-            environ["PATH_INFO"], from_calendar)
+            environ["PATH_INFO"], from_collection)
         if from_name:
-            item = from_calendar.get_item(from_name)
+            item = from_collection.get_item(from_name)
             if item:
                 # Move the item
                 to_url_parts = urlparse(environ["HTTP_DESTINATION"])
                 if to_url_parts.netloc == environ["HTTP_HOST"]:
                     to_url = to_url_parts.path
                     to_path, to_name = to_url.rstrip("/").rsplit("/", 1)
-                    to_calendar = ical.Calendar.from_path(
+                    to_collection = ical.Collection.from_path(
                         to_path, depth="0")[0]
-                    to_calendar.append(to_name, item.text)
-                    from_calendar.remove(from_name)
+                    to_collection.append(to_name, item.text)
+                    from_collection.remove(from_name)
                     return client.CREATED, {}, None
                 else:
                     # Remote destination server, not supported
@@ -363,60 +373,60 @@ class Application(object):
                 # No item found
                 return client.GONE, {}, None
         else:
-            # Moving calendars, not supported
+            # Moving collections, not supported
             return client.FORBIDDEN, {}, None
 
-    def options(self, environ, calendars, content, user):
+    def options(self, environ, collections, content, user):
         """Manage OPTIONS request."""
         headers = {
-            "Allow": "DELETE, HEAD, GET, MKCALENDAR, MOVE, " \
+            "Allow": "DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, " \
                 "OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT",
-            "DAV": "1, calendar-access"}
+            "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol"}
         return client.OK, headers, None
 
-    def propfind(self, environ, calendars, content, user):
+    def propfind(self, environ, collections, content, user):
         """Manage PROPFIND request."""
         headers = {
-            "DAV": "1, calendar-access",
+            "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol",
             "Content-Type": "text/xml"}
         answer = xmlutils.propfind(
-            environ["PATH_INFO"], content, calendars, user)
+            environ["PATH_INFO"], content, collections, user)
         return client.MULTI_STATUS, headers, answer
 
-    def proppatch(self, environ, calendars, content, user):
+    def proppatch(self, environ, collections, content, user):
         """Manage PROPPATCH request."""
-        calendar = calendars[0]
-        answer = xmlutils.proppatch(environ["PATH_INFO"], content, calendar)
+        collection = collections[0]
+        answer = xmlutils.proppatch(environ["PATH_INFO"], content, collection)
         headers = {
-            "DAV": "1, calendar-access",
+            "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol",
             "Content-Type": "text/xml"}
         return client.MULTI_STATUS, headers, answer
 
-    def put(self, environ, calendars, content, user):
+    def put(self, environ, collections, content, user):
         """Manage PUT request."""
-        calendar = calendars[0]
+        collection = collections[0]
         headers = {}
-        item_name = xmlutils.name_from_path(environ["PATH_INFO"], calendar)
-        item = calendar.get_item(item_name)
+        item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection)
+        item = collection.get_item(item_name)
         if (not item and not environ.get("HTTP_IF_MATCH")) or (
             item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag):
             # PUT allowed in 3 cases
             # Case 1: No item and no ETag precondition: Add new item
             # Case 2: Item and ETag precondition verified: Modify item
             # Case 3: Item and no Etag precondition: Force modifying item
-            xmlutils.put(environ["PATH_INFO"], content, calendar)
+            xmlutils.put(environ["PATH_INFO"], content, collection)
             status = client.CREATED
-            headers["ETag"] = calendar.get_item(item_name).etag
+            headers["ETag"] = collection.get_item(item_name).etag
         else:
             # PUT rejected in all other cases
             status = client.PRECONDITION_FAILED
         return status, headers, None
 
-    def report(self, environ, calendars, content, user):
+    def report(self, environ, collections, content, user):
         """Manage REPORT request."""
-        calendar = calendars[0]
+        collection = collections[0]
         headers = {'Content-Type': 'text/xml'}
-        answer = xmlutils.report(environ["PATH_INFO"], content, calendar)
+        answer = xmlutils.report(environ["PATH_INFO"], content, collection)
         return client.MULTI_STATUS, headers, answer
 
     # pylint: enable=W0612,W0613,R0201

+ 113 - 69
radicale/ical.py

@@ -47,13 +47,17 @@ def open(path, mode="r"):
 # pylint: enable=W0622
 
 
-def serialize(headers=(), items=()):
-    """Return an iCal text corresponding to given ``headers`` and ``items``."""
-    lines = ["BEGIN:VCALENDAR"]
+def serialize(tag, headers=(), items=()):
+    """Return a collection text corresponding to given ``tag``.
+
+    The collection has the given ``headers`` and ``items``.
+
+    """
+    lines = ["BEGIN:%s" % tag]
     for part in (headers, items):
         if part:
             lines.append("\n".join(item.text for item in part))
-    lines.append("END:VCALENDAR\n")
+    lines.append("END:%s\n" % tag)
     return "\n".join(lines)
 
 
@@ -135,37 +139,45 @@ class Header(Item):
     """Internal header class."""
 
 
-class Event(Item):
+class Timezone(Item):
+    """Internal timezone class."""
+    tag = "VTIMEZONE"
+
+
+class Component(Item):
+    """Internal main component of a collection."""
+
+
+class Event(Component):
     """Internal event class."""
     tag = "VEVENT"
+    mimetype = "text/calendar"
 
 
-class Todo(Item):
+class Todo(Component):
     """Internal todo class."""
-    # This is not a TODO!
-    # pylint: disable=W0511
-    tag = "VTODO"
-    # pylint: enable=W0511
+    tag = "VTODO"  # pylint: disable=W0511
+    mimetype = "text/calendar"
 
 
-class Journal(Item):
+class Journal(Component):
     """Internal journal class."""
     tag = "VJOURNAL"
+    mimetype = "text/calendar"
 
 
-class Timezone(Item):
-    """Internal timezone class."""
-    tag = "VTIMEZONE"
-
+class Card(Component):
+    """Internal card class."""
+    tag = "VCARD"
+    mimetype = "text/vcard"
 
-class Calendar(object):
-    """Internal calendar class."""
-    tag = "VCALENDAR"
 
+class Collection(object):
+    """Internal collection item."""
     def __init__(self, path, principal=False):
-        """Initialize the calendar.
+        """Initialize the collection.
 
-        ``path`` must be the normalized relative path of the calendar, using
+        ``path`` must be the normalized relative path of the collection, using
         the slash as the folder delimiter, with no leading nor trailing slash.
 
         """
@@ -174,7 +186,7 @@ class Calendar(object):
         self.path = os.path.join(FOLDER, path.replace("/", os.sep))
         self.props_path = self.path + '.props'
         if principal and split_path and os.path.isdir(self.path):
-            # Already existing principal calendar
+            # Already existing principal collection
             self.owner = split_path[0]
         elif len(split_path) > 1:
             # URL with at least one folder
@@ -186,7 +198,7 @@ class Calendar(object):
 
     @classmethod
     def from_path(cls, path, depth="infinite", include_container=True):
-        """Return a list of calendars and items under the given ``path``.
+        """Return a list of collections and items under the given ``path``.
 
         If ``depth`` is "0", only the actual object under ``path`` is
         returned. Otherwise, also sub-items are appended to the result. If
@@ -218,7 +230,7 @@ class Calendar(object):
                     result.append(cls(path, principal))
                 try:
                     for filename in next(os.walk(abs_path))[2]:
-                        if cls.is_vcalendar(os.path.join(abs_path, filename)):
+                        if cls.is_collection(os.path.join(abs_path, filename)):
                             result.append(cls(os.path.join(path, filename)))
                 except StopIteration:
                     # Directory does not exist yet
@@ -227,17 +239,52 @@ class Calendar(object):
             if depth == "0":
                 result.append(cls(path))
             else:
-                calendar = cls(path, principal)
+                collection = cls(path, principal)
                 if include_container:
-                    result.append(calendar)
-                result.extend(calendar.components)
+                    result.append(collection)
+                result.extend(collection.components)
         return result
 
-    @staticmethod
-    def is_vcalendar(path):
-        """Return ``True`` if there is a VCALENDAR file under ``path``."""
+    def is_collection(self, path):
+        """Return ``True`` if there is a collection file under ``path``."""
+        beginning_string = 'BEGIN:%s' % self.tag
         with open(path) as stream:
-            return 'BEGIN:VCALENDAR' == stream.read(15)
+            beginning_string = stream.read(len(beginning_string))
+
+    @property
+    def items(self):
+        """Get list of all items in collection."""
+        return self._parse(self.text, (Card, Event, Todo, Journal, Timezone))
+
+    @property
+    def components(self):
+        """Get list of all components in collection."""
+        return self._parse(self.text, (Card, Event, Todo, Journal))
+
+    @property
+    def events(self):
+        """Get list of ``Event`` items in collection."""
+        return self._parse(self.text, (Event,))
+
+    @property
+    def cards(self):
+        """Get list of all cards in collection."""
+        return self._parse(self.text, (Card,))
+
+    @property
+    def todos(self):
+        """Get list of ``Todo`` items in collection."""
+        return self._parse(self.text, (Todo,))
+
+    @property
+    def journals(self):
+        """Get list of ``Journal`` items in collection."""
+        return self._parse(self.text, (Journal,))
+
+    @property
+    def timezones(self):
+        """Get list of ``Timezome`` items in collection."""
+        return self._parse(self.text, (Timezone,))
 
     @staticmethod
     def _parse(text, item_types, name=None):
@@ -329,7 +376,7 @@ class Calendar(object):
 
         self._create_dirs(self.path)
 
-        text = serialize(headers, items)
+        text = serialize(self.tag, headers, items)
         return open(self.path, "w").write(text)
 
     @staticmethod
@@ -338,21 +385,48 @@ class Calendar(object):
         if not os.path.exists(os.path.dirname(path)):
             os.makedirs(os.path.dirname(path))
 
+    @property
+    def tag(self):
+        """Type of the collection."""
+        with self.props as props:
+            if "tag" not in props:
+                try:
+                    props["tag"] = open(self.path).readlines()[0][6:].rstrip()
+                except IOError:
+                    props["tag"] = "VCALENDAR"
+            return props["tag"]
+
+    @property
+    def mimetype(self):
+        """Mimetype of the collection."""
+        if self.tag == "VADDRESSBOOK":
+            return "text/vcard"
+        elif self.tag == "VCALENDAR":
+            return "text/calendar"
+
+    @property
+    def resource_type(self):
+        """Resource type of the collection."""
+        if self.tag == "VADDRESSBOOK":
+            return "addressbook"
+        elif self.tag == "VCALENDAR":
+            return "calendar"
+
     @property
     def etag(self):
-        """Etag from calendar."""
+        """Etag from collection."""
         return '"%s"' % hash(self.text)
 
     @property
     def name(self):
-        """Calendar name."""
+        """Collection name."""
         with self.props as props:
             return props.get('D:displayname',
                 self.path.split(os.path.sep)[-1])
 
     @property
     def text(self):
-        """Calendar as plain text."""
+        """Collection as plain text."""
         try:
             return open(self.path).read()
         except IOError:
@@ -360,7 +434,7 @@ class Calendar(object):
 
     @property
     def headers(self):
-        """Find headers items in calendar."""
+        """Find headers items in collection."""
         header_lines = []
 
         lines = unfold(self.text)
@@ -373,39 +447,9 @@ class Calendar(object):
 
         return header_lines
 
-    @property
-    def items(self):
-        """Get list of all items in calendar."""
-        return self._parse(self.text, (Event, Todo, Journal, Timezone))
-
-    @property
-    def components(self):
-        """Get list of all components in calendar."""
-        return self._parse(self.text, (Event, Todo, Journal))
-
-    @property
-    def events(self):
-        """Get list of ``Event`` items in calendar."""
-        return self._parse(self.text, (Event,))
-
-    @property
-    def todos(self):
-        """Get list of ``Todo`` items in calendar."""
-        return self._parse(self.text, (Todo,))
-
-    @property
-    def journals(self):
-        """Get list of ``Journal`` items in calendar."""
-        return self._parse(self.text, (Journal,))
-
-    @property
-    def timezones(self):
-        """Get list of ``Timezome`` items in calendar."""
-        return self._parse(self.text, (Timezone,))
-
     @property
     def last_modified(self):
-        """Get the last time the calendar has been modified.
+        """Get the last time the collection has been modified.
 
         The date is formatted according to rfc1123-5.2.14.
 
@@ -420,7 +464,7 @@ class Calendar(object):
     @property
     @contextmanager
     def props(self):
-        """Get the calendar properties."""
+        """Get the collection properties."""
         # On enter
         properties = {}
         if os.path.exists(self.props_path):
@@ -434,7 +478,7 @@ class Calendar(object):
 
     @property
     def owner_url(self):
-        """Get the calendar URL according to its owner."""
+        """Get the collection URL according to its owner."""
         if self.owner:
             return "/%s/" % self.owner
         else:
@@ -442,5 +486,5 @@ class Calendar(object):
 
     @property
     def url(self):
-        """Get the standard calendar URL."""
+        """Get the standard collection URL."""
         return "/%s/" % self.local_path

+ 62 - 50
radicale/xmlutils.py

@@ -31,7 +31,7 @@ try:
     from collections import OrderedDict
 except ImportError:
     # Python 2.6 has no OrderedDict, use a dict instead
-    OrderedDict = dict # pylint: disable=C0103
+    OrderedDict = dict  # pylint: disable=C0103
 import re
 import xml.etree.ElementTree as ET
 
@@ -40,6 +40,7 @@ from radicale import client, config, ical
 
 NAMESPACES = {
     "C": "urn:ietf:params:xml:ns:caldav",
+    "CR": "urn:ietf:params:xml:ns:carddav",
     "D": "DAV:",
     "CS": "http://calendarserver.org/ns/",
     "ICAL": "http://apple.com/ns/ical/",
@@ -56,7 +57,7 @@ for short, url in NAMESPACES.items():
         ET.register_namespace("" if short == "D" else short, url)
     else:
         # ... and badly with Python 2.6 and 3.1
-        ET._namespace_map[url] = short # pylint: disable=W0212
+        ET._namespace_map[url] = short  # pylint: disable=W0212
 
 
 CLARK_TAG_REGEX = re.compile(r"""
@@ -118,11 +119,12 @@ def _response(code):
     return "HTTP/1.1 %i %s" % (code, client.responses[code])
 
 
-def name_from_path(path, calendar):
+def name_from_path(path, collection):
     """Return Radicale item name from ``path``."""
-    calendar_parts = calendar.local_path.strip("/").split("/")
+    collection_parts = collection.local_path.strip("/").split("/")
     path_parts = path.strip("/").split("/")
-    return path_parts[-1] if (len(path_parts) - len(calendar_parts)) else None
+    if (len(path_parts) - len(collection_parts)):
+        return path_parts[-1]
 
 
 def props_from_request(root, actions=("set", "remove")):
@@ -142,23 +144,29 @@ def props_from_request(root, actions=("set", "remove")):
     if prop_element is not None:
         for prop in prop_element:
             result[_tag_from_clark(prop.tag)] = prop.text
+            if prop.tag == "resourcetype":
+                for resource_type in prop:
+                    if resource_type.tag in ("calendar", "addressbook"):
+                        result["resourcetype"] = \
+                            "V%s" % resource_type.tag.upper()
+                        break
 
     return result
 
 
-def delete(path, calendar):
+def delete(path, collection):
     """Read and answer DELETE requests.
 
     Read rfc4918-9.6 for info.
 
     """
     # Reading request
-    if calendar.local_path == path.strip("/"):
-        # Delete the whole calendar
-        calendar.delete()
+    if collection.local_path == path.strip("/"):
+        # Delete the whole collection
+        collection.delete()
     else:
-        # Remove an item from the calendar
-        calendar.remove(name_from_path(path, calendar))
+        # Remove an item from the collection
+        collection.remove(name_from_path(path, collection))
 
     # Writing answer
     multistatus = ET.Element(_tag("D", "multistatus"))
@@ -176,7 +184,7 @@ def delete(path, calendar):
     return _pretty_xml(multistatus)
 
 
-def propfind(path, xml_request, calendars, user=None):
+def propfind(path, xml_request, collections, user=None):
     """Read and answer PROPFIND requests.
 
     Read rfc4918-9.1 for info.
@@ -191,8 +199,8 @@ def propfind(path, xml_request, calendars, user=None):
     # Writing answer
     multistatus = ET.Element(_tag("D", "multistatus"))
 
-    for calendar in calendars:
-        response = _propfind_response(path, calendar, props, user)
+    for collection in collections:
+        response = _propfind_response(path, collection, props, user)
         multistatus.append(response)
 
     return _pretty_xml(multistatus)
@@ -200,15 +208,15 @@ def propfind(path, xml_request, calendars, user=None):
 
 def _propfind_response(path, item, props, user):
     """Build and return a PROPFIND response."""
-    is_calendar = isinstance(item, ical.Calendar)
-    if is_calendar:
-        with item.props as cal_props:
-            calendar_props = cal_props
+    is_collection = isinstance(item, ical.Collection)
+    if is_collection:
+        with item.props as properties:
+            collection_props = properties
 
     response = ET.Element(_tag("D", "response"))
 
     href = ET.Element(_tag("D", "href"))
-    uri = item.url if is_calendar else "%s/%s" % (path, item.name)
+    uri = item.url if is_collection else "%s/%s" % (path, item.name)
     href.text = uri.replace("//", "/")
     response.append(href)
 
@@ -263,15 +271,15 @@ def _propfind_response(path, item, props, user):
                 report_tag.text = report_name
                 supported.append(report_tag)
                 element.append(supported)
-        elif is_calendar:
+        elif is_collection:
             if tag == _tag("D", "getcontenttype"):
-                element.text = "text/calendar"
+                element.text = item.mimetype
             elif tag == _tag("D", "resourcetype"):
                 if item.is_principal:
                     tag = ET.Element(_tag("D", "principal"))
                     element.append(tag)
                 else:
-                    tag = ET.Element(_tag("C", "calendar"))
+                    tag = ET.Element(_tag("C", item.resource_type))
                     element.append(tag)
                 tag = ET.Element(_tag("D", "collection"))
                 element.append(tag)
@@ -280,16 +288,18 @@ def _propfind_response(path, item, props, user):
             elif tag == _tag("CS", "getctag"):
                 element.text = item.etag
             elif tag == _tag("C", "calendar-timezone"):
-                element.text = ical.serialize(item.headers, item.timezones)
+                element.text = ical.serialize(
+                    item.tag, item.headers, item.timezones)
             else:
                 human_tag = _tag_from_clark(tag)
-                if human_tag in calendar_props:
-                    element.text = calendar_props[human_tag]
+                if human_tag in collection_props:
+                    element.text = collection_props[human_tag]
                 else:
                     is404 = True
-        # Not for calendars
+        # Not for collections
         elif tag == _tag("D", "getcontenttype"):
-            element.text = "text/calendar; component=%s" % item.tag.lower()
+            element.text = "%s; component=%s" % (
+                item.mimetype, item.tag.lower())
         elif tag == _tag("D", "resourcetype"):
             # resourcetype must be returned empty for non-collection elements
             pass
@@ -340,7 +350,7 @@ def _add_propstat_to(element, tag, status_number):
     propstat.append(status)
 
 
-def proppatch(path, xml_request, calendar):
+def proppatch(path, xml_request, collection):
     """Read and answer PROPPATCH requests.
 
     Read rfc4918-9.2 for info.
@@ -361,17 +371,17 @@ def proppatch(path, xml_request, calendar):
     href.text = path
     response.append(href)
 
-    with calendar.props as calendar_props:
+    with collection.props as collection_props:
         for short_name, value in props_to_set.items():
             if short_name == 'C:calendar-timezone':
-                calendar.replace('', value)
-                calendar.write()
+                collection.replace('', value)
+                collection.write()
             else:
-                calendar_props[short_name] = value
+                collection_props[short_name] = value
             _add_propstat_to(response, short_name, 200)
         for short_name in props_to_remove:
             try:
-                del calendar_props[short_name]
+                del collection_props[short_name]
             except KeyError:
                 _add_propstat_to(response, short_name, 412)
             else:
@@ -380,18 +390,18 @@ def proppatch(path, xml_request, calendar):
     return _pretty_xml(multistatus)
 
 
-def put(path, ical_request, calendar):
+def put(path, ical_request, collection):
     """Read PUT requests."""
-    name = name_from_path(path, calendar)
-    if name in (item.name for item in calendar.items):
+    name = name_from_path(path, collection)
+    if name in (item.name for item in collection.items):
         # PUT is modifying an existing item
-        calendar.replace(name, ical_request)
+        collection.replace(name, ical_request)
     else:
         # PUT is adding a new item
-        calendar.append(name, ical_request)
+        collection.append(name, ical_request)
 
 
-def report(path, xml_request, calendar):
+def report(path, xml_request, collection):
     """Read and answer REPORT requests.
 
     Read rfc3253-3.6 for info.
@@ -403,7 +413,7 @@ def report(path, xml_request, calendar):
     prop_element = root.find(_tag("D", "prop"))
     props = [prop.tag for prop in prop_element]
 
-    if calendar:
+    if collection:
         if root.tag == _tag("C", "calendar-multiget"):
             # Read rfc4791-7.9 for info
             hreferences = set(
@@ -417,21 +427,22 @@ def report(path, xml_request, calendar):
     # Writing answer
     multistatus = ET.Element(_tag("D", "multistatus"))
 
-    calendar_items = calendar.items
-    calendar_headers = calendar.headers
-    calendar_timezones = calendar.timezones
+    collection_tag = collection.tag
+    collection_items = collection.items
+    collection_headers = collection.headers
+    collection_timezones = collection.timezones
 
     for hreference in hreferences:
-        # Check if the reference is an item or a calendar
-        name = name_from_path(hreference, calendar)
+        # Check if the reference is an item or a collection
+        name = name_from_path(hreference, collection)
         if name:
             # Reference is an item
             path = "/".join(hreference.split("/")[:-1]) + "/"
-            items = (item for item in calendar_items if item.name == name)
+            items = (item for item in collection_items if item.name == name)
         else:
-            # Reference is a calendar
+            # Reference is a collection
             path = hreference
-            items = calendar.components
+            items = collection.components
 
         for item in items:
             response = ET.Element(_tag("D", "response"))
@@ -452,9 +463,10 @@ def report(path, xml_request, calendar):
                 if tag == _tag("D", "getetag"):
                     element.text = item.etag
                 elif tag == _tag("C", "calendar-data"):
-                    if isinstance(item, (ical.Event, ical.Todo, ical.Journal)):
+                    if isinstance(item, ical.Component):
                         element.text = ical.serialize(
-                            calendar_headers, calendar_timezones + [item])
+                            collection_tag, collection_headers,
+                            collection_timezones + [item])
                 prop.append(element)
 
             status = ET.Element(_tag("D", "status"))