[CalendarServer-changes] [5666] CalendarServer/branches/new-store/txcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Tue Jun 1 09:46:05 PDT 2010
Revision: 5666
http://trac.macosforge.org/projects/calendarserver/changeset/5666
Author: glyph at apple.com
Date: 2010-06-01 09:46:01 -0700 (Tue, 01 Jun 2010)
Log Message:
-----------
make more tests implementation-agnostic
Modified Paths:
--------------
CalendarServer/branches/new-store/txcaldav/calendarstore/scheduling.py
CalendarServer/branches/new-store/txcaldav/calendarstore/test/__init__.py
CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_common.py
CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py
CalendarServer/branches/new-store/txcaldav/icalendarstore.py
Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/scheduling.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/scheduling.py 2010-05-31 21:14:21 UTC (rev 5665)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/scheduling.py 2010-06-01 16:46:01 UTC (rev 5666)
@@ -50,10 +50,14 @@
def calendarHomeWithUID(self, uid, create=False):
- # FIXME: implement
-# newHome = self._transaction.calendarHomeWithUID()
+ # FIXME: 'create' flag
+ newHome = self._transaction.calendarHomeWithUID(uid)
# return ImplicitSchedulingCalendarHome(newHome, self)
- return ImplicitSchedulingCalendarHome(None, None)
+ if newHome is None:
+ return None
+ else:
+ # FIXME: relay transaction
+ return ImplicitSchedulingCalendarHome(newHome, None)
@@ -68,21 +72,27 @@
self._transaction = transaction
- def uid(self): ""
- # FIXME: implement
+ def uid(self):
+ return self._calendarHome.uid()
+
def properties(self): ""
# FIXME: implement
+ # return self._calendarHome.properties()
+
def calendars(self): ""
# FIXME: implement
- def createCalendarWithName(self, name): ""
- # FIXME: implement
+ def createCalendarWithName(self, name):
+ self._calendarHome.createCalendarWithName(name)
def removeCalendarWithName(self, name): ""
# FIXME: implement
def calendarWithName(self, name):
- # FIXME: implement
- return ImplicitSchedulingCalendar(self, None)
+ calendar = self._calendarHome.calendarWithName(name)
+ if calendar is not None:
+ return ImplicitSchedulingCalendar(self, calendar)
+ else:
+ return None
@@ -105,21 +115,33 @@
self._parentHome = parentHome
self._subCalendar = subCalendar
+ def name(self):
+ return self._subCalendar.name()
- def ownerCalendarHome(self): ""
- def calendarObjects(self): ""
+ def ownerCalendarHome(self):
+ return self._parentHome
+ def calendarObjects(self):
+ # FIXME: wrap
+ return self._subCalendar.calendarObjects()
def calendarObjectWithUID(self, uid): ""
- def createCalendarObjectWithName(self, name, component): ""
- def removeCalendarObjectWithName(self, name): ""
+ def createCalendarObjectWithName(self, name, component):
+ # FIXME: implement most of StoreCalendarObjectResource here!
+ self._subCalendar.createCalendarObjectWithName(name, component)
+ def removeCalendarObjectWithName(self, name):
+ # FIXME: implement deletion logic here!
+ return self._subCalendar.removeCalendarObjectWithName(name)
def removeCalendarObjectWithUID(self, uid): ""
def syncToken(self): ""
def calendarObjectsInTimeRange(self, start, end, timeZone): ""
def calendarObjectsSinceToken(self, token): ""
- def properties(self): ""
+ def properties(self):
+ # FIXME: probably need to wrap this as well
+ return self._subCalendar.properties()
def calendarObjectWithName(self, name):
- return ImplicitSchedulingCalendarObject()
+ #FIXME: wrap
+ return self._subCalendar.calendarObjectWithName(name)
class ImplicitSchedulingStore(object):
@@ -142,4 +164,5 @@
"""
Wrap an underlying L{ITransaction}.
"""
- return ImplicitSchedulingTransaction(None)
+ return ImplicitSchedulingTransaction(
+ self._calendarStore.newTransaction())
Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/test/__init__.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/test/__init__.py 2010-05-31 21:14:21 UTC (rev 5665)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/test/__init__.py 2010-06-01 16:46:01 UTC (rev 5666)
@@ -1,3 +1,4 @@
+# -*- test-case-name: txcaldav.calendarstore.test -*-
##
# Copyright (c) 2010 Apple Inc. All rights reserved.
#
Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_common.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_common.py 2010-05-31 21:14:21 UTC (rev 5665)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_common.py 2010-06-01 16:46:01 UTC (rev 5666)
@@ -19,10 +19,13 @@
"""
from txcaldav.icalendarstore import ICalendarStore, ICalendarStoreTransaction, \
- ICalendarObject, ICalendarHome, ICalendar
+ ICalendarObject, ICalendarHome, ICalendar, InvalidCalendarComponentError
from twext.python.filepath import CachingFilePath as FilePath
from zope.interface.verify import verifyObject
from zope.interface.exceptions import BrokenMethodImplementation
+from txdav.propertystore.base import PropertyName
+from twext.web2.dav import davxml
+from twext.python.vcomponent import VComponent
storePath = FilePath(__file__).parent().child("calendar_store")
@@ -31,8 +34,54 @@
cal1Root = homeRoot.child("calendar_1")
+calendar1_objectNames = (
+ "1.ics",
+ "2.ics",
+ "3.ics",
+)
+event4_text = (
+ "BEGIN:VCALENDAR\r\n"
+ "VERSION:2.0\r\n"
+ "PRODID:-//Apple Inc.//iCal 4.0.1//EN\r\n"
+ "CALSCALE:GREGORIAN\r\n"
+ "BEGIN:VTIMEZONE\r\n"
+ "TZID:US/Pacific\r\n"
+ "BEGIN:DAYLIGHT\r\n"
+ "TZOFFSETFROM:-0800\r\n"
+ "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\n"
+ "DTSTART:20070311T020000\r\n"
+ "TZNAME:PDT\r\n"
+ "TZOFFSETTO:-0700\r\n"
+ "END:DAYLIGHT\r\n"
+ "BEGIN:STANDARD\r\n"
+ "TZOFFSETFROM:-0700\r\n"
+ "RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\n"
+ "DTSTART:20071104T020000\r\n"
+ "TZNAME:PST\r\n"
+ "TZOFFSETTO:-0800\r\n"
+ "END:STANDARD\r\n"
+ "END:VTIMEZONE\r\n"
+ "BEGIN:VEVENT\r\n"
+ "CREATED:20100203T013849Z\r\n"
+ "UID:uid4\r\n"
+ "DTEND;TZID=US/Pacific:20100207T173000\r\n"
+ "TRANSP:OPAQUE\r\n"
+ "SUMMARY:New Event\r\n"
+ "DTSTART;TZID=US/Pacific:20100207T170000\r\n"
+ "DTSTAMP:20100203T013909Z\r\n"
+ "SEQUENCE:3\r\n"
+ "BEGIN:VALARM\r\n"
+ "X-WR-ALARMUID:1377CCC7-F85C-4610-8583-9513D4B364E1\r\n"
+ "TRIGGER:-PT20M\r\n"
+ "ATTACH;VALUE=URI:Basso\r\n"
+ "ACTION:AUDIO\r\n"
+ "END:VALARM\r\n"
+ "END:VEVENT\r\n"
+ "END:VCALENDAR\r\n"
+)
+
class CommonTests(object):
"""
Tests for common functionality of interfaces defined in
@@ -64,11 +113,46 @@
raise NotImplementedError()
+ lastTransaction = None
+ savedStore = None
+
+ def transactionUnderTest(self):
+ """
+ Create a transaction from C{storeUnderTest} and save it as
+ C[lastTransaction}. Also makes sure to use the same store, saving the
+ value from C{storeUnderTest}.
+ """
+ if self.lastTransaction is not None:
+ return self.lastTransaction
+ if self.savedStore is None:
+ self.savedStore = self.storeUnderTest()
+ txn = self.lastTransaction = self.savedStore.newTransaction()
+ return txn
+
+
+ def commit(self):
+ """
+ Commit the last transaction created from C{transactionUnderTest}, and
+ clear it.
+ """
+ self.lastTransaction.commit()
+ self.lastTransaction = None
+
+
+ def abort(self):
+ """
+ Abort the last transaction created from C[transactionUnderTest}, and
+ clear it.
+ """
+ self.lastTransaction.abort()
+ self.lastTransaction = None
+
+
def homeUnderTest(self):
"""
Get the calendar home detailed by C{requirements['home1']}.
"""
- return self.storeUnderTest().newTransaction().calendarHomeWithUID(
+ return self.transactionUnderTest().calendarHomeWithUID(
"home1")
@@ -140,3 +224,154 @@
L{ICalendarObject} and its required attributes.
"""
self.assertProvides(ICalendarObject, self.calendarObjectUnderTest())
+
+
+ def test_calendarHomeWithUID_exists(self):
+ """
+ Finding an existing calendar home by UID results in an object that
+ provides L{ICalendarHome} and has a C{uid()} method that returns the
+ same value that was passed in.
+ """
+ calendarHome = (self.storeUnderTest().newTransaction()
+ .calendarHomeWithUID("home1"))
+
+ self.assertEquals(calendarHome.uid(), "home1")
+ self.assertProvides(ICalendarHome, calendarHome)
+
+
+ def test_calendarHomeWithUID_absent(self):
+ """
+ L{ICalendarStoreTransaction.calendarHomeWithUID} should return C{None}
+ when asked for a non-existent calendar home.
+ """
+ self.assertEquals(
+ self.storeUnderTest().newTransaction()
+ .calendarHomeWithUID("xyzzy"),
+ None
+ )
+
+
+ def test_createCalendarWithName_absent(self):
+ """
+ L{ICalendarHome.createCalendarWithName} creates a new L{ICalendar} that
+ can be retrieved with L{ICalendarHome.calendarWithName}.
+ """
+ home = self.homeUnderTest()
+ name = "new"
+ self.assertIdentical(home.calendarWithName(name), None)
+ home.createCalendarWithName(name)
+ self.assertNotIdentical(home.calendarWithName(name), None)
+ def checkProperties():
+ calendarProperties = home.calendarWithName(name).properties()
+ self.assertEquals(
+ calendarProperties[
+ PropertyName.fromString(davxml.ResourceType.sname())
+ ],
+ davxml.ResourceType.calendar) #@UndefinedVariable
+ checkProperties()
+ self.commit()
+
+ # Make sure it's available in a new transaction; i.e. test the commit.
+ home = self.homeUnderTest()
+ self.assertNotIdentical(home.calendarWithName(name), None)
+ home = self.calendarStore.newTransaction().calendarHomeWithUID(
+ "home1")
+ # Sanity check: are the properties actually persisted?
+ # FIXME: no independent testing of this right now
+ checkProperties()
+
+
+ def test_calendarObjects(self):
+ """
+ L{ICalendar.calendarObjects} will enumerate the calendar objects present
+ in the filesystem, in name order, but skip those with hidden names.
+ """
+ calendar1 = self.calendarUnderTest()
+ calendarObjects = tuple(calendar1.calendarObjects())
+
+ for calendarObject in calendarObjects:
+ self.assertProvides(ICalendarObject, calendarObject)
+
+ self.assertEquals(
+ tuple(o.name() for o in calendarObjects),
+ calendar1_objectNames
+ )
+
+
+ def test_calendarObjectsWithRemovedObject(self):
+ """
+ L{ICalendar.calendarObjects} will skip those objects which have been
+ removed by L{Calendar.removeCalendarObjectWithName} in the same
+ transaction, even if it has not yet been committed.
+ """
+ calendar1 = self.calendarUnderTest()
+ calendar1.removeCalendarObjectWithName("2.ics")
+ calendarObjects = list(calendar1.calendarObjects())
+ self.assertEquals(set(o.name() for o in calendarObjects),
+ set(calendar1_objectNames) - set(["2.ics"]))
+
+
+ def test_ownerCalendarHome(self):
+ """
+ L{ICalendar.ownerCalendarHome} should match the home UID.
+ """
+ self.assertEquals(
+ self.calendarUnderTest().ownerCalendarHome().uid(),
+ self.homeUnderTest().uid()
+ )
+
+
+ def test_calendarObjectWithName_absent(self):
+ """
+ L{ICalendar.calendarObjectWithName} returns C{None} for calendars which
+ don't exist.
+ """
+ calendar1 = self.calendarUnderTest()
+ self.assertEquals(calendar1.calendarObjectWithName("xyzzy"), None)
+
+
+ def test_name(self):
+ """
+ L{Calendar.name} reflects the name of the calendar.
+ """
+ self.assertEquals(self.calendarUnderTest().name(), "calendar_1")
+
+
+ def test_calendarObjectWithUID_absent(self):
+ """
+ L{ICalendar.calendarObjectWithUID} returns C{None} for calendars which
+ don't exist.
+ """
+ calendar1 = self.calendarUnderTest()
+ self.assertEquals(calendar1.calendarObjectWithUID("xyzzy"), None)
+
+
+ def test_createCalendarObjectWithName_absent(self):
+ """
+ L{ICalendar.createCalendarObjectWithName} will create a new
+ L{ICalendarObject}.
+ """
+ calendar1 = self.calendarUnderTest()
+ name = "4.ics"
+ self.assertIdentical(calendar1.calendarObjectWithName(name), None)
+ component = VComponent.fromString(event4_text)
+ calendar1.createCalendarObjectWithName(name, component)
+
+ calendarObject = calendar1.calendarObjectWithName(name)
+ self.assertEquals(calendarObject.component(), component)
+
+
+ def test_setComponent_uidchanged(self):
+ """
+ L{ICalendarObject.setComponent} raises L{InvalidCalendarComponentError}
+ when given a L{VComponent} whose UID does not match its existing UID.
+ """
+ calendar1 = self.calendarUnderTest()
+ component = VComponent.fromString(event4_text)
+ calendarObject = calendar1.calendarObjectWithName("1.ics")
+ self.assertRaises(
+ InvalidCalendarComponentError,
+ calendarObject.setComponent, component
+ )
+
+
Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py 2010-05-31 21:14:21 UTC (rev 5665)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py 2010-06-01 16:46:01 UTC (rev 5666)
@@ -23,10 +23,7 @@
from twext.python.vcomponent import VComponent
-from twext.web2.dav import davxml
-
from txdav.idav import IPropertyStore
-from txdav.propertystore.base import PropertyName
from txcaldav.icalendarstore import CalendarNameNotAllowedError
from txcaldav.icalendarstore import CalendarObjectNameNotAllowedError
@@ -40,7 +37,8 @@
from txcaldav.calendarstore.file import CalendarStore, CalendarHome
from txcaldav.calendarstore.file import Calendar, CalendarObject
-from txcaldav.calendarstore.test.test_common import CommonTests
+from txcaldav.calendarstore.test.test_common import (
+ CommonTests, calendar1_objectNames, event4_text)
storePath = FilePath(__file__).parent().child("calendar_store")
@@ -50,53 +48,7 @@
"calendar_empty",
)
-calendar1_objectNames = (
- "1.ics",
- "2.ics",
- "3.ics",
-)
-event4_text = (
- "BEGIN:VCALENDAR\r\n"
- "VERSION:2.0\r\n"
- "PRODID:-//Apple Inc.//iCal 4.0.1//EN\r\n"
- "CALSCALE:GREGORIAN\r\n"
- "BEGIN:VTIMEZONE\r\n"
- "TZID:US/Pacific\r\n"
- "BEGIN:DAYLIGHT\r\n"
- "TZOFFSETFROM:-0800\r\n"
- "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\n"
- "DTSTART:20070311T020000\r\n"
- "TZNAME:PDT\r\n"
- "TZOFFSETTO:-0700\r\n"
- "END:DAYLIGHT\r\n"
- "BEGIN:STANDARD\r\n"
- "TZOFFSETFROM:-0700\r\n"
- "RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\n"
- "DTSTART:20071104T020000\r\n"
- "TZNAME:PST\r\n"
- "TZOFFSETTO:-0800\r\n"
- "END:STANDARD\r\n"
- "END:VTIMEZONE\r\n"
- "BEGIN:VEVENT\r\n"
- "CREATED:20100203T013849Z\r\n"
- "UID:uid4\r\n"
- "DTEND;TZID=US/Pacific:20100207T173000\r\n"
- "TRANSP:OPAQUE\r\n"
- "SUMMARY:New Event\r\n"
- "DTSTART;TZID=US/Pacific:20100207T170000\r\n"
- "DTSTAMP:20100203T013909Z\r\n"
- "SEQUENCE:3\r\n"
- "BEGIN:VALARM\r\n"
- "X-WR-ALARMUID:1377CCC7-F85C-4610-8583-9513D4B364E1\r\n"
- "TRIGGER:-PT20M\r\n"
- "ATTACH;VALUE=URI:Basso\r\n"
- "ACTION:AUDIO\r\n"
- "END:VALARM\r\n"
- "END:VEVENT\r\n"
- "END:VCALENDAR\r\n"
-)
-
event1modified_text = event4_text.replace(
"\r\nUID:uid4\r\n",
"\r\nUID:uid1\r\n"
@@ -198,25 +150,6 @@
)
- def test_calendarHomeWithUID_exists(self):
- """
- Find an existing calendar home by UID.
- """
- calendarHome = self.calendarStore.newTransaction().calendarHomeWithUID("home1")
-
- self.failUnless(isinstance(calendarHome, CalendarHome))
-
-
- def test_calendarHomeWithUID_absent(self):
- """
- Missing calendar home.
- """
- self.assertEquals(
- self.calendarStore.newTransaction().calendarHomeWithUID("xyzzy"),
- None
- )
-
-
def test_calendarHomeWithUID_create(self):
"""
Create missing calendar home.
@@ -274,13 +207,6 @@
)
- def test_uid(self):
- """
- UID is correct.
- """
- self.assertEquals(self.home1.uid(), "home1")
-
-
def test_calendars(self):
"""
Find all of the calendars.
@@ -326,30 +252,6 @@
self.assertEquals(self.home1.calendarWithName(name), None)
- def test_createCalendarWithName_absent(self):
- """
- Create a new calendar.
- """
- name = "new"
- assert self.home1.calendarWithName(name) is None
- self.home1.createCalendarWithName(name)
- self.failUnless(self.home1.calendarWithName(name) is not None)
- def checkProperties():
- calendarProperties = self.home1.calendarWithName(name).properties()
- self.assertEquals(
- calendarProperties[
- PropertyName.fromString(davxml.ResourceType.sname())
- ],
- davxml.ResourceType.calendar) #@UndefinedVariable
- checkProperties()
- self.txn.commit()
- self.home1 = self.calendarStore.newTransaction().calendarHomeWithUID(
- "home1")
- # Sanity check: are the properties actually persisted?
- # FIXME: no independent testing of this right now
- checkProperties()
-
-
def test_createCalendarWithName_exists(self):
"""
Attempt to create an existing calendar should raise.
@@ -428,58 +330,6 @@
)
- def test_name(self):
- """
- L{Calendar.name} reflects the name of the calendar.
- """
- self.assertEquals(self.calendar1.name(), "calendar_1")
-
-
- def test_ownerCalendarHome(self):
- """
- Owner is correct.
- """
- # Note that here we know that home1 owns calendar1
- self.assertEquals(
- self.calendar1.ownerCalendarHome().uid(),
- self.home1.uid()
- )
-
-
- def test_calendarObjects(self):
- """
- L{Calendar.calendarObjects} will enumerate the calendar objects present
- in the filesystem, in name order, but skip those with hidden names.
- """
- # Add a dot file to make sure we don't find it
- self.home1._path.child(".foo").createDirectory()
-
- calendarObjects = tuple(self.calendar1.calendarObjects())
-
- for calendarObject in calendarObjects:
- self.failUnless(
- isinstance(calendarObject, CalendarObject),
- calendarObject
- )
-
- self.assertEquals(
- tuple(o.name() for o in calendarObjects),
- calendar1_objectNames
- )
-
-
- def test_calendarObjectsWithRemovedObject(self):
- """
- L{Calendar.calendarObjects will skip those objects which have been
- removed by L{Calendar.removeCalendarObjectWithName} in the same
- transaction, even if it has not yet been committed.
- """
- self.calendar1.removeCalendarObjectWithName("2.ics")
- calendarObjects = list(self.calendar1.calendarObjects())
- self.assertEquals(set(o.name() for o in calendarObjects),
- set(calendar1_objectNames) - set(["2.ics"]))
-
-
def test_calendarObjectWithName_exists(self):
"""
Find existing calendar object by name.
@@ -493,13 +343,6 @@
self.assertEquals(calendarObject.name(), name)
- def test_calendarObjectWithName_absent(self):
- """
- Missing calendar object.
- """
- self.assertEquals(self.calendar1.calendarObjectWithName("xyzzy"), None)
-
-
def test_calendarObjectWithName_dot(self):
"""
Filenames starting with "." are reserved by this
@@ -527,26 +370,6 @@
)
- def test_calendarObjectWithUID_absent(self):
- """
- Missing calendar object.
- """
- self.assertEquals(self.calendar1.calendarObjectWithUID("xyzzy"), None)
-
-
- def test_createCalendarObjectWithName_absent(self):
- """
- Create a new calendar object.
- """
- name = "4.ics"
- self.assertIdentical(self.calendar1.calendarObjectWithName(name), None)
- component = VComponent.fromString(event4_text)
- self.calendar1.createCalendarObjectWithName(name, component)
-
- calendarObject = self.calendar1.calendarObjectWithName(name)
- self.assertEquals(calendarObject.component(), component)
-
-
def test_createCalendarObjectWithName_exists(self):
"""
Attempt to create an existing calendar object should raise.
@@ -830,16 +653,6 @@
self.assertEquals(calendarObject.component(), component)
- def test_setComponent_uidchanged(self):
- component = VComponent.fromString(event4_text)
-
- calendarObject = self.calendar1.calendarObjectWithName("1.ics")
- self.assertRaises(
- InvalidCalendarComponentError,
- calendarObject.setComponent, component
- )
-
-
def test_setComponent_invalid(self):
calendarObject = self.calendar1.calendarObjectWithName("1.ics")
self.assertRaises(
@@ -911,4 +724,13 @@
Create and return a L{CalendarStore} for testing.
"""
setUpCalendarStore(self)
- return self.calendarStore
\ No newline at end of file
+ return self.calendarStore
+
+
+ def test_calendarObjectsWithDotFile(self):
+ """
+ Adding a dotfile to the calendar home should not increase
+ """
+ self.homeUnderTest()._path.child(".foo").createDirectory()
+ self.test_calendarObjects()
+
Modified: CalendarServer/branches/new-store/txcaldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/icalendarstore.py 2010-05-31 21:14:21 UTC (rev 5665)
+++ CalendarServer/branches/new-store/txcaldav/icalendarstore.py 2010-06-01 16:46:01 UTC (rev 5666)
@@ -228,6 +228,16 @@
shared with other principals, granting them read-only or
read/write access.
"""
+
+ def name():
+ """
+ Identify this calendar uniquely, as with
+ L{ICalendarHome.calendarWithName}.
+
+ @return: the name of this calendar.
+ @rtype: C{str}
+ """
+
def ownerCalendarHome():
"""
Retrieve the calendar home for the owner of this calendar.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100601/335734b1/attachment-0001.html>
More information about the calendarserver-changes
mailing list