[CalendarServer-changes] [5081] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Feb 9 13:07:31 PST 2010


Revision: 5081
          http://trac.macosforge.org/projects/calendarserver/changeset/5081
Author:   glyph at apple.com
Date:     2010-02-09 13:07:27 -0800 (Tue, 09 Feb 2010)
Log Message:
-----------
Land the merge of ContactsServer on trunk.

Modified Paths:
--------------
    CalendarServer/trunk/README
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/conf/auth/augments-test.xml
    CalendarServer/trunk/lib-patches/vobject/vobject.icalendar.patch
    CalendarServer/trunk/run
    CalendarServer/trunk/setup.py
    CalendarServer/trunk/support/Makefile.Apple
    CalendarServer/trunk/support/directorysetup.py
    CalendarServer/trunk/support/submit
    CalendarServer/trunk/twisted/plugins/caldav.py
    CalendarServer/trunk/twistedcaldav/__init__.py
    CalendarServer/trunk/twistedcaldav/directory/aggregate.py
    CalendarServer/trunk/twistedcaldav/directory/augment.py
    CalendarServer/trunk/twistedcaldav/directory/directory.py
    CalendarServer/trunk/twistedcaldav/directory/idirectory.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
    CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py
    CalendarServer/trunk/twistedcaldav/directory/xmlfile.py
    CalendarServer/trunk/twistedcaldav/method/__init__.py
    CalendarServer/trunk/twistedcaldav/method/copymove.py
    CalendarServer/trunk/twistedcaldav/method/delete_common.py
    CalendarServer/trunk/twistedcaldav/method/mkcol.py
    CalendarServer/trunk/twistedcaldav/method/put.py
    CalendarServer/trunk/twistedcaldav/method/report_common.py
    CalendarServer/trunk/twistedcaldav/query/__init__.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/static.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py

Added Paths:
-----------
    CalendarServer/trunk/bin/carddavd
    CalendarServer/trunk/calendarserver/tap/carddav.py
    CalendarServer/trunk/conf/carddavd-test.plist
    CalendarServer/trunk/conf/carddavd.plist.default
    CalendarServer/trunk/contrib/SBS/conf/90-addressBookServer.plist
    CalendarServer/trunk/contrib/launchd/addressbookserver.plist
    CalendarServer/trunk/doc/carddavd.8
    CalendarServer/trunk/lib-patches/vobject/vobject.base.patch
    CalendarServer/trunk/lib-patches/vobject/vobject.vcard.patch
    CalendarServer/trunk/testcarddav
    CalendarServer/trunk/twisted/plugins/carddav.py
    CalendarServer/trunk/twistedcaldav/carddavxml.py
    CalendarServer/trunk/twistedcaldav/directory/addressbook.py
    CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
    CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py
    CalendarServer/trunk/twistedcaldav/method/copymove_contact.py
    CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py
    CalendarServer/trunk/twistedcaldav/method/report_addressbook_multiget.py
    CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py
    CalendarServer/trunk/twistedcaldav/mkcolxml.py
    CalendarServer/trunk/twistedcaldav/query/addressbookquery.py
    CalendarServer/trunk/twistedcaldav/report_addressbook_findshared.py
    CalendarServer/trunk/twistedcaldav/vcard.py
    CalendarServer/trunk/twistedcaldav/vcardindex.py

Property Changed:
----------------
    CalendarServer/trunk/
    CalendarServer/trunk/twistedcaldav/test/data/


Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075

Modified: CalendarServer/trunk/README
===================================================================
--- CalendarServer/trunk/README	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/README	2010-02-09 21:07:27 UTC (rev 5081)
@@ -2,7 +2,7 @@
 ===============
 
 This is the core code base for Darwin Calendar Server, which is a
-CalDAV, WebDAV, and HTTP server.
+CalDAV, CardDAV, WebDAV, and HTTP server.
 
 For general information about the server, see:
 

Copied: CalendarServer/trunk/bin/carddavd (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/bin/carddavd)
===================================================================
--- CalendarServer/trunk/bin/carddavd	                        (rev 0)
+++ CalendarServer/trunk/bin/carddavd	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,123 @@
+#!/bin/bash
+
+##
+# Copyright (c) 2005-2009 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.
+##
+
+#PATH
+#PYTHONPATH
+
+daemonize="";
+username="";
+groupname="";
+configfile="";
+twistdpath="$(type -p twistd)";
+plugin_name="carddav";
+service_type="";
+profile="";
+twistd_reactor="";
+child_reactor=""
+
+py_version ()
+{
+  local python="$1"; shift
+  echo "$("${python}" -c "from distutils.sysconfig import get_python_version; print get_python_version()")";
+}
+
+try_python ()
+{
+  local python="$1"; shift
+
+  if [ -z "${python}" ]; then return 1; fi;
+
+  if ! type "${python}" > /dev/null 2>&1; then return 1; fi;
+  local py_version="$(py_version "${python}")";
+  if [ "${py_version/./}" -lt "24" ]; then return 1; fi;
+
+  return 0;
+}
+
+for v in "" "2.6" "2.5"; do
+  for p in                                                              \
+    "${PYTHON:=}"                                                       \
+    "python${v}"                                                        \
+    "/usr/local/bin/python${v}"                                         \
+    "/usr/local/python/bin/python${v}"                                  \
+    "/usr/local/python${v}/bin/python${v}"                              \
+    "/opt/bin/python${v}"                                               \
+    "/opt/python/bin/python${v}"                                        \
+    "/opt/python${v}/bin/python${v}"                                    \
+    "/Library/Frameworks/Python.framework/Versions/${v}/bin/python"     \
+    "/opt/local/bin/python${v}"                                         \
+    "/sw/bin/python${v}"                                                \
+    ;
+  do
+    if try_python "${p}"; then python="${p}"; break; fi;
+  done;
+  if [ -n "${python:-}" ]; then break; fi;
+done;
+
+if [ -z "${python:-}" ]; then
+  echo "No suitable python found.";
+  exit 1;
+fi;
+
+usage ()
+{
+    program="$(basename "$0")";
+
+    if [ "${1--}" != "-" ]; then echo "$@"; echo; fi;
+
+    echo "Usage: ${program} [-hX] [-u username] [-g groupname] [-T twistd] [-t type] [-f carddavd.plist] [-p statsfile]";
+    echo "Options:";
+    echo "        -h Print this help and exit";
+    echo "        -X Do not daemonize";
+    echo "        -u User name to run as";
+    echo "        -g Group name to run as";
+    echo "        -f Configuration file to read";
+    echo "        -T Path to twistd binary";
+    echo "        -t Process type (Master, Slave or Combined)";
+    echo "        -p Path to the desired pstats file.";
+    echo "        -R The Twisted Reactor to run [${reactor}]";
+
+    if [ "${1-}" == "-" ]; then return 0; fi;
+    exit 64;
+}
+
+while getopts 'hXu:g:f:T:P:t:p:R:' option; do
+    case "${option}" in
+        '?') usage; ;;
+        'h') usage -; exit 0; ;;
+        'X') daemonize="-n"; ;;
+        'f') configfile="-f ${OPTARG}"; ;;
+        'T') twistdpath="${OPTARG}"; ;;
+        'u') username="-u ${OPTARG}"; ;;
+        'g') groupname="-g ${OPTARG}"; ;;
+        'P') plugin_name="${OPTARG}"; ;;
+        't') service_type="-o ProcessType=${OPTARG}"; ;;
+        'p') profile="-o Profiling/Enabled=True -o Profiling/BaseDirectory=${OPTARG}"; ;;
+        'R') twistd_reactor="--reactor=${OPTARG}"; child_reactor="-o Twisted/reactor=${OPTARG}"; ;;
+    esac;
+done;
+
+shift $((${OPTIND} - 1));
+
+if [ $# != 0 ]; then usage "Unrecognized arguments:" "$@"; fi;
+
+export PYTHONPATH
+
+echo exec "${python}" "${twistdpath}" "${twistd_reactor}" ${daemonize} ${username} ${groupname} "${plugin_name}" ${configfile} ${service_type} ${profile} "${child_reactor}";
+
+exec "${python}" "${twistdpath}" ${twistd_reactor} ${daemonize} ${username} ${groupname} "${plugin_name}" ${configfile} ${service_type} ${profile} ${child_reactor};

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -527,6 +527,7 @@
             directories.append(wikiDirectory)
 
         directory = AggregateDirectoryService(directories)
+        self.directory = directory
 
         if sudoDirectory:
             directory.userRecordTypes.insert(0,
@@ -667,6 +668,7 @@
             "/principals/",
             directory,
         )
+        self.principalCollection = principalCollection
 
         self.log_info("Setting up calendar collection: %r"
                       % (self.calendarResourceClass,))
@@ -683,6 +685,7 @@
             config.DocumentRoot,
             principalCollections=(principalCollection,),
         )
+        self.root = root
 
         root.putChild("principals", principalCollection)
         root.putChild("calendars", calendarCollection)

Copied: CalendarServer/trunk/calendarserver/tap/carddav.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/calendarserver/tap/carddav.py)
===================================================================
--- CalendarServer/trunk/calendarserver/tap/carddav.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/tap/carddav.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,114 @@
+##
+# Copyright (c) 2005-2009 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.
+##
+
+__all__ = [
+    "CardDAVServiceMaker",
+]
+
+import errno
+import os
+
+from zope.interface import implements
+
+from twisted.python.filepath import FilePath
+from twisted.plugin import IPlugin
+from twisted.internet.reactor import callLater
+from twisted.application.service import IServiceMaker
+
+from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
+
+from twistedcaldav.config import config
+from twistedcaldav.static import AddressBookHomeProvisioningFile, DirectoryBackedAddressBookFile
+from twext.log import Logger
+
+log = Logger()
+
+from calendarserver.tap.caldav import CalDAVServiceMaker
+
+try:
+    from twistedcaldav.authkerb import NegotiateCredentialFactory
+except ImportError:
+    NegotiateCredentialFactory = None
+
+from calendarserver.provision.root import RootResource as _ParentRootResource
+
+
+
+class RootResource (_ParentRootResource):
+    """
+    A special root resource that contains support checking SACLs
+    as well as adding responseFilters.
+    """
+
+    saclService = "addressbook"
+
+
+
+class CardDAVServiceMaker (CalDAVServiceMaker):
+    implements(IPlugin, IServiceMaker)
+
+    tapname = "carddav"
+    description = "Darwin Contacts Server"
+
+    #
+    # Default resource classes
+    #
+    rootResourceClass            = RootResource
+    principalResourceClass       = DirectoryPrincipalProvisioningResource
+    addressBookResourceClass     = AddressBookHomeProvisioningFile
+    directoryBackedAddressBookResourceClass = DirectoryBackedAddressBookFile
+
+    def makeService_Slave(self, options):
+        result = super(CardDAVServiceMaker, self).makeService_Slave(options)
+
+        directory = self.directory
+        principalCollection = self.principalCollection
+
+        if config.EnableCardDAV:
+            log.info("Setting up address book collection: %r" % (self.addressBookResourceClass,))
+    
+            addressBookCollection = self.addressBookResourceClass(
+                os.path.join(config.DocumentRoot, "addressbooks"),
+                directory, "/addressbooks/"
+            )
+            
+            directoryPath = os.path.join(config.DocumentRoot, "directory")
+            doBacking = config.DirectoryAddressBook and config.EnableSearchAddressBook
+            if doBacking:
+                log.info("Setting up directory address book: %r" % (self.directoryBackedAddressBookResourceClass,))
+    
+                directoryBackedAddressBookCollection = self.directoryBackedAddressBookResourceClass(
+                    directoryPath,
+                    principalCollections=(principalCollection,)
+                )
+                # do this after process is owned by carddav user, not root
+                callLater(1.0, directoryBackedAddressBookCollection.provisionDirectory)
+            else:
+                # remove /directory from previous runs that may have created it
+                try:
+                    FilePath(directoryPath).remove()
+                    self.log_info("Deleted: %s" %    directoryPath)
+                except (OSError, IOError), e:
+                    if e.errno != errno.ENOENT:
+                        self.log_error("Could not delete: %s : %r" %  (directoryPath, e,))
+            root = self.root
+
+            root.putChild('addressbooks', addressBookCollection)
+            if doBacking:
+                root.putChild('directory', directoryBackedAddressBookCollection)
+        return result
+
+    makeService_Single   = makeService_Slave

Modified: CalendarServer/trunk/conf/auth/augments-test.xml
===================================================================
--- CalendarServer/trunk/conf/auth/augments-test.xml	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/conf/auth/augments-test.xml	2010-02-09 21:07:27 UTC (rev 5081)
@@ -23,17 +23,20 @@
     <guid>Default</guid>
     <enable>true</enable>
     <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
   </record>
   <record repeat="10">
     <guid>location%02d</guid>
     <enable>true</enable>
     <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
     <auto-schedule>true</auto-schedule>
   </record>
   <record repeat="10">
     <guid>resource%02d</guid>
     <enable>true</enable>
     <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
     <auto-schedule>true</auto-schedule>
   </record>
   <record repeat="4">

Copied: CalendarServer/trunk/conf/carddavd-test.plist (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/conf/carddavd-test.plist)
===================================================================
--- CalendarServer/trunk/conf/carddavd-test.plist	                        (rev 0)
+++ CalendarServer/trunk/conf/carddavd-test.plist	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,768 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2006-2009 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.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+
+    <!--
+        Public network address information
+
+        This is the server's public network address, which is provided to
+        clients in URLs and the like.  It may or may not be the network
+        address that the server is listening to directly, though it is by
+        default.  For example, it may be the address of a load balancer or
+        proxy which forwards connections to the server.
+      -->
+
+    <!-- Network host name [empty = system host name] -->
+    <key>ServerHostName</key>
+    <string></string> <!-- The hostname clients use when connecting -->
+
+    <key>EnableCardDAV</key>
+    <true/>
+
+    <!-- HTTP port [0 = disable HTTP] -->
+    <key>HTTPPort</key>
+    <integer>8800</integer>
+
+    <!-- SSL port [0 = disable HTTPS] -->
+    <!-- (Must also configure SSLCertificate and SSLPrivateKey below) -->
+    <key>SSLPort</key>
+    <integer>8843</integer>
+
+    <!-- Redirect non-SSL ports to an SSL port -->
+    <key>RedirectHTTPToHTTPS</key>
+    <false/>
+
+    <!--
+        Network address configuration information
+
+        This configures the actual network address that the server binds to.
+      -->
+
+    <!-- List of IP addresses to bind to [empty = all] -->
+    <key>BindAddresses</key>
+    <array>
+    </array>
+
+    <!-- List of port numbers to bind to for HTTP [empty = same as "Port"] -->
+    <key>BindHTTPPorts</key>
+    <array>
+    </array>
+
+    <!-- List of port numbers to bind to for SSL [empty = same as "SSLPort"] -->
+    <key>BindSSLPorts</key>
+    <array>
+    </array>
+
+
+    <!--
+        Data Store
+      -->
+
+    <!-- Data root -->
+    <key>DataRoot</key>
+    <string>data/</string>
+
+    <!-- Document root -->
+    <key>DocumentRoot</key>
+    <string>twistedcaldav/test/data/</string>
+
+    <!-- Child aliases -->
+    <key>Aliases</key>
+    <dict>
+      <!--
+      <key>foo</key>
+      <dict>
+        <key>path</key>
+        <string>/path/to/foo</string>
+      </dict>
+       -->
+    </dict>
+
+    <!-- User quota (in bytes) -->
+    <key>UserQuota</key>
+    <integer>104857600</integer><!-- 100Mb -->
+
+    <!-- Attachment size limit (in bytes) -->
+    <key>MaximumAttachmentSize</key>
+    <integer>1048576</integer><!-- 1Mb -->
+
+    <!-- Maximum number of unique attendees per entire event -->
+    <!-- 0 for no limit -->
+    <key>MaxAttendeesPerInstance</key>
+    <integer>100</integer>
+
+    <!-- Maximum number of instances allowed for a single RRULE -->
+    <!-- 0 for no limit -->
+    <key>MaxInstancesForRRULE</key>
+    <integer>400</integer>
+
+
+    <!--
+        Directory service
+
+        A directory service provides information about principals (eg.
+        users, groups, locations and resources) to the server.
+
+        A variety of directory services are available for use.
+      -->
+
+    <!-- XML File Directory Service -->
+    <key>DirectoryService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
+      
+      <key>params</key>
+      <dict>
+        <key>xmlFile</key>
+        <string>conf/auth/accounts-test.xml</string>
+      </dict>
+    </dict>
+    
+    <!-- Open Directory Service (Mac OS X) -->
+    <!--
+    <key>DirectoryService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.appleopendirectory.OpenDirectoryService</string>
+      
+      <key>params</key>
+      <dict>
+        <key>node</key>
+        <string>/Search</string>
+        <key>cacheTimeout</key>
+        <integer>30</integer>
+      </dict>
+    </dict>
+    -->
+
+    <!--
+        Augment service
+
+        Augments for the directory service records to add calendar specific attributes.
+
+        A variety of augment services are available for use.
+        When using a partitioned server, a service that can be accessed from each host will be needed.
+      -->
+
+    <!-- XML File Augment Service -->
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentXMLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>xmlFiles</key>
+        <array>
+	      <string>conf/auth/augments-test.xml</string>
+        </array>
+      </dict>
+    </dict>
+    
+    <!-- Sqlite Augment Service -->
+    <!--
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentSqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>/etc/carddavd/augments.sqlite</string>
+      </dict>
+    </dict>
+     -->
+
+    <!-- PostgreSQL Augment Service -->
+    <!--
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentPostgreSQLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>augments</string>
+      </dict>
+    </dict>
+     -->
+
+    <!-- Sqlite ProxyDB Service -->
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>data/proxies.sqlite</string>
+      </dict>
+    </dict>
+
+    <!-- PostgreSQL ProxyDB Service -->
+    <!--
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>proxies</string>
+      </dict>
+    </dict>
+     -->
+
+	<key>ProxyLoadFromFile</key>
+    <string>conf/auth/proxies-test.xml</string>
+
+    <!--
+        Special principals
+
+        These principals are granted special access and/or perform
+        special roles on the server.
+      -->
+
+    <!-- Principals with "DAV:all" access (relative URLs) -->
+    <key>AdminPrincipals</key>
+    <array>
+      <string>/principals/__uids__/admin/</string>
+    </array>
+
+    <!-- Principals with "DAV:read" access (relative URLs) -->
+    <key>ReadPrincipals</key>
+    <array>
+      <!-- <string>/principals/__uids__/983C8238-FB6B-4D92-9242-89C0A39E5F81/</string> -->
+    </array>
+
+    <!-- Principals that can pose as other principals -->
+    <key>SudoersFile</key>
+    <string>conf/sudoers.plist</string>
+
+    <!-- Create "proxy access" principals -->
+    <key>EnableProxyPrincipals</key>
+    <true/>
+
+
+    <!--
+        Permissions
+      -->
+
+    <!-- Anonymous read access for root resource -->
+    <key>EnableAnonymousReadRoot</key>
+    <!-- Note: this _must_ be false if EnableCardDAV is true: some CardDAV
+         clients require a 403 at / to establish authentication. -->
+    <false/>
+
+    <!-- Anonymous read access for resource hierarchy -->
+    <key>EnableAnonymousReadNav</key>
+    <false/>
+
+    <!-- Enables directory listings for principals -->
+    <key>EnablePrincipalListings</key>
+    <true/>
+
+    <!-- Render calendar collections as a monolithic iCalendar object -->
+    <key>EnableMonolithicCalendars</key>
+    <true/>
+
+
+    <!--
+        Authentication
+      -->
+
+    <key>Authentication</key>
+    <dict>
+
+      <!-- Clear text; best avoided -->
+      <key>Basic</key>
+      <dict>
+        <key>Enabled</key>
+        <true/>
+      </dict>
+
+      <!-- Digest challenge/response -->
+      <key>Digest</key>
+      <dict>
+        <key>Enabled</key>
+        <true/>
+        <key>Algorithm</key>
+        <string>md5</string>
+        <key>Qop</key>
+        <string></string>
+      </dict>
+
+      <!-- Kerberos/SPNEGO -->
+      <key>Kerberos</key>
+      <dict>
+        <key>Enabled</key>
+        <false/>
+        <key>ServicePrincipal</key>
+        <string></string>
+      </dict>
+
+      <!-- Wikiserver authentication (Mac OS X) -->
+      <key>Wiki</key>
+      <dict>
+        <key>Enabled</key>
+        <false/>
+        <key>Cookie</key>
+        <string>sessionID</string>
+        <key>URL</key>
+        <string>http://127.0.0.1/RPC2</string>
+        <key>UserMethod</key>
+        <string>userForSession</string>
+        <key>WikiMethod</key>
+        <string>accessLevelForUserWikiCalendar</string>
+      </dict>
+
+    </dict>
+
+
+    <!--
+        Logging
+      -->
+
+    <!-- Apache-style access log -->
+    <key>AccessLogFile</key>
+    <string>logs/access.log</string>
+    <key>RotateAccessLog</key>
+    <false/>
+    <key>MoreAccessLogData</key>
+    <true/>
+
+    <!-- Server activity log -->
+    <key>ErrorLogFile</key>
+    <string>logs/error.log</string>
+
+    <!-- Log levels -->
+    <key>DefaultLogLevel</key>
+    <string>info</string> <!-- debug, info, warn, error -->
+
+    <!-- Log level overrides for specific functionality -->
+    <key>LogLevels</key>
+    <dict>
+      <!--
+      <key>twistedcaldav.directory.appleopendirectory.OpenDirectoryService</key>
+      <string>debug</string>
+      -->
+    </dict>
+
+    <!-- Global server stats --> 
+    <key>GlobalStatsSocket</key> 
+    <string>logs/carddavd-stats.sock</string> 
+
+    <!-- Global server stats logging period --> 
+    <key>GlobalStatsLoggingPeriod</key> 
+    <integer>60</integer> 
+
+    <!-- Global server stats logging frequency [0 = disable stats] --> 
+    <key>GlobalStatsLoggingFrequency</key> 
+    <integer>12</integer>
+
+    <!-- Server statistics file -->
+    <key>ServerStatsFile</key>
+    <string>logs/stats.plist</string>
+
+    <!-- Server process ID file -->
+    <key>PIDFile</key>
+    <string>logs/carddavd.pid</string>
+
+
+    <!--
+        Accounting
+      -->
+
+    <!-- Enable accounting for certain operations -->
+    <key>AccountingCategories</key>
+    <dict>
+      <key>iTIP</key>
+      <false/>
+      <key>HTTP</key>
+      <false/>
+    </dict>
+    <!-- Enable accounting for specific principals -->
+    <key>AccountingPrincipals</key>
+    <array>
+      <!-- <string>/principals/__uids__/454D85C0-09F0-4DC6-A3C6-97DFEB4622CD/</string> -->
+    </array>
+
+
+    <!--
+        SSL/TLS
+      -->
+
+    <!-- Public key -->
+    <key>SSLCertificate</key>
+    <string>twistedcaldav/test/data/server.pem</string>
+
+    <!-- SSL authority chain (for intermediate certs) -->
+    <key>SSLAuthorityChain</key>
+    <string></string>
+
+    <!-- Private key -->
+    <key>SSLPrivateKey</key>
+    <string>twistedcaldav/test/data/server.pem</string>
+
+
+    <!--
+        Process management
+      -->
+
+    <key>UserName</key>
+    <string></string>
+
+    <key>GroupName</key>
+    <string></string>
+
+    <key>ProcessType</key>
+    <string>Combined</string>
+
+    <key>MultiProcess</key>
+    <dict>
+      <key>ProcessCount</key>
+      <integer>2</integer> <!-- 0 = larger of: 4 or (2 * CPU count) -->
+    </dict>
+
+
+    <!--
+        Notifications
+      -->
+
+    <key>Notifications</key>
+    <dict>
+      <!-- Time spent coalescing notifications before delivery -->
+      <key>CoalesceSeconds</key>
+      <integer>3</integer>
+
+      <key>InternalNotificationHost</key>
+      <string>localhost</string>
+
+      <key>InternalNotificationPort</key>
+      <integer>62309</integer>
+
+      <key>Services</key>
+      <dict>
+        <key>SimpleLineNotifier</key>
+        <dict>
+          <!-- Simple line notification service (for testing) -->
+          <key>Service</key>
+          <string>twistedcaldav.notify.SimpleLineNotifierService</string>
+          <key>Enabled</key>
+          <false/>
+          <key>Port</key>
+          <integer>62308</integer>
+        </dict>
+
+        <key>XMPPNotifier</key>
+        <dict>
+          <!-- XMPP notification service -->
+          <key>Service</key>
+          <string>twistedcaldav.notify.XMPPNotifierService</string>
+          <key>Enabled</key>
+          <false/>
+
+          <!-- XMPP host and port to contact -->
+          <key>Host</key>
+          <string>xmpp.host.name</string>
+          <key>Port</key>
+          <integer>5222</integer>
+
+          <!-- Jabber ID and password for the server -->
+          <key>JID</key>
+          <string>jid at xmpp.host.name/resource</string>
+          <key>Password</key>
+          <string>password_goes_here</string>
+
+          <!-- PubSub service address -->
+          <key>ServiceAddress</key>
+          <string>pubsub.xmpp.host.name</string>
+
+          <key>NodeConfiguration</key>
+          <dict>
+            <key>pubsub#deliver_payloads</key>
+            <string>1</string>
+            <key>pubsub#persist_items</key>
+            <string>1</string>
+          </dict>
+
+          <!-- Sends a presence notification to XMPP server at this interval (prevents disconnect) -->
+          <key>KeepAliveSeconds</key>
+          <integer>120</integer>
+
+          <!-- Sends a pubsub publish to a particular heartbeat node at this interval -->
+          <key>HeartbeatMinutes</key>
+          <integer>30</integer>
+
+          <!-- List of glob-like expressions defining which XMPP JIDs can converse with the server (for debugging) -->
+          <key>AllowedJIDs</key>
+          <array>
+            <!--
+            <string>*.example.com</string>
+             -->
+          </array>
+        </dict>
+      </dict>
+    </dict>
+
+
+    <!--
+        Server-to-server protocol
+      -->
+
+    <key>Scheduling</key>
+    <dict>
+
+      <!-- CalDAV protocol options -->
+      <key>CalDAV</key>
+      <dict>
+        <key>EmailDomain</key>
+        <string></string>
+        <key>HTTPDomain</key>
+        <string></string>
+        <key>AddressPatterns</key>
+        <array>
+        </array>
+        <key>OldDraftCompatibility</key>
+        <true/>
+        <key>ScheduleTagCompatibility</key>
+        <true/>
+        <key>EnablePrivateComments</key>
+        <true/>
+      </dict>
+
+      <!-- iSchedule protocol options -->
+      <key>iSchedule</key>
+      <dict>
+        <key>Enabled</key>
+        <false/>
+        <key>AddressPatterns</key>
+        <array>
+        </array>
+        <key>Servers</key>
+        <string>conf/servertoserver-test.xml</string>
+      </dict>
+
+      <!-- iMIP protocol options -->
+      <key>iMIP</key>
+      <dict>
+        <key>Enabled</key>
+        <false/>
+        <key>MailGatewayServer</key>
+        <string>localhost</string>
+        <key>MailGatewayPort</key>
+        <integer>62310</integer>
+        <key>Sending</key>
+        <dict>
+          <key>Server</key>
+          <string></string>
+          <key>Port</key>
+          <integer>587</integer>
+          <key>UseSSL</key>
+          <true/>
+          <key>Username</key>
+          <string></string>
+          <key>Password</key>
+          <string></string>
+          <key>Address</key>
+          <string></string> <!-- Address email will be sent from -->
+        </dict>
+        <key>Receiving</key>
+        <dict>
+          <key>Server</key>
+          <string></string>
+          <key>Port</key>
+          <integer>995</integer>
+          <key>Type</key>
+          <string></string> <!-- Either "pop" or "imap" -->
+          <key>UseSSL</key>
+          <true/>
+          <key>Username</key>
+          <string></string>
+          <key>Password</key>
+          <string></string>
+          <key>PollingSeconds</key>
+          <integer>30</integer>
+        </dict>
+        <key>AddressPatterns</key>
+        <array>
+          <string>mailto:.*</string>
+        </array>
+      </dict>
+
+	  <!-- General options for scheduling -->
+	  <key>Options</key>
+	  <dict>
+        <key>AllowGroupAsOrganizer</key>
+        <false/>
+        <key>AllowLocationAsOrganizer</key>
+        <false/>
+        <key>AllowResourceAsOrganizer</key>
+        <false/>
+       </dict>
+
+    </dict>
+
+
+    <!--
+        Free-busy URL protocol
+      -->
+
+    <key>FreeBusyURL</key>
+    <dict>
+      <key>Enabled</key>
+      <true/>
+      <key>TimePeriod</key>
+      <integer>14</integer>
+      <key>AnonymousAccess</key>
+      <false/>
+    </dict>
+
+
+    <!--
+        Non-standard CalDAV extensions
+      -->
+
+    <!-- Calendar Drop Box -->
+    <key>EnableDropBox</key>
+    <true/>
+
+    <!-- Private Events -->
+    <key>EnablePrivateEvents</key>
+    <true/>
+
+    <!-- Timezone Service -->
+    <key>EnableTimezoneService</key>
+    <true/>
+
+
+    <!--
+        Miscellaneous items
+      -->
+
+    <!-- Service ACLs (Mac OS X) -->
+    <key>EnableSACLs</key>
+    <false/>
+
+    <!-- Web-based administration -->
+    <key>EnableWebAdmin</key>
+    <true/>
+
+    <!-- Support for Content-Encoding compression options as specified in RFC2616 Section 3.5 -->
+    <key>ResponseCompression</key>
+    <false/>
+    
+    <!-- The retry-after value (in seconds) to return with a 503 error. -->
+    <key>HTTPRetryAfter</key>
+    <integer>180</integer>
+
+    <!-- A unix socket used for communication between the child and master processes.
+         An empty value tells the server to use a tcp socket instead. -->
+    <key>ControlSocket</key>
+    <string>logs/carddavd.sock</string>
+
+    <!-- Support for Memcached -->
+    <key>Memcached</key>
+    <dict>
+      <key>MaxClients</key>
+      <integer>5</integer>
+      <key>memcached</key>
+      <string>memcached</string> <!-- Find in PATH -->
+      <key>Options</key>
+      <array>
+        <!--<string>-vv</string>-->
+      </array>
+    </dict>
+
+    <!-- Response Caching -->
+    <key>ResponseCacheTimeout</key>
+    <integer>30</integer> <!-- in minutes -->
+
+
+    <!--
+        Twisted
+      -->
+
+    <key>Twisted</key>
+    <dict>
+      <key>twistd</key>
+      <string>../Twisted/bin/twistd</string>
+    </dict>
+
+
+    <key>Localization</key>
+    <dict>
+      <key>LocalesDirectory</key>
+      <string>locales</string>
+      <key>Language</key>
+      <string>English</string>
+    </dict>
+
+    <!--
+        Directory Address Book
+      -->
+    
+    <!--  Disable Directory Address Book-->
+    <!--
+      <key>DirectoryAddressBook</key>
+      <false/>
+    -->
+    
+    <!-- Open Directory-backed Directory Address Book -->
+    <key>DirectoryAddressBook</key>
+    <dict>
+
+      <key>type</key>
+      <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
+      <key>params</key>
+      <dict>
+        <key>xmlFile</key>
+        <string>conf/auth/accounts-test.xml</string>
+      </dict>
+    </dict>
+
+    <!--  allow unauthenticated users to access the directory address book -->
+    <key>AnonymousDirectoryAddressBookAccess</key>
+    <false/>
+    
+    <!--  enable search address book in user's home -->
+    <key>EnableSearchAddressBook</key>
+    <true/>
+    
+    <!--  enable searchall address book in user's home -->
+    <key>EnableSearchAllAddressBook</key>
+    <true/>
+    
+    <!--  enable find share report -->
+    <key>EnableFindSharedReport</key>
+    <true/>
+
+    </dict>
+</plist>

Copied: CalendarServer/trunk/conf/carddavd.plist.default (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/conf/carddavd.plist.default)
===================================================================
--- CalendarServer/trunk/conf/carddavd.plist.default	                        (rev 0)
+++ CalendarServer/trunk/conf/carddavd.plist.default	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,443 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2006-2009 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.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+
+    <!--
+        Public network address information
+
+        This is the server's public network address, which is provided to
+        clients in URLs and the like.  It may or may not be the network
+        address that the server is listening to directly, though it is by
+        default.  For example, it may be the address of a load balancer or
+        proxy which forwards connections to the server.
+      -->
+
+    <!-- Network host name [empty = system host name] -->
+    <key>ServerHostName</key>
+    <string></string> <!-- The hostname clients use when connecting -->
+
+    <!-- HTTP port [0 = disable HTTP] -->
+    <key>HTTPPort</key>
+    <integer>8800</integer>
+
+    <!-- SSL port [0 = disable HTTPS] -->
+    <!-- (Must also configure SSLCertificate and SSLPrivateKey below) -->
+    <!--
+    <key>SSLPort</key>
+    <integer>8843</integer>
+    -->
+
+    <!-- Redirect non-SSL ports to an SSL port (if configured for SSL) -->
+    <key>RedirectHTTPToHTTPS</key>
+    <false/>
+
+
+    <!--
+        Network address configuration information
+
+        This configures the actual network address that the server binds to.
+      -->
+
+    <!-- List of IP addresses to bind to [empty = all] -->
+    <key>BindAddresses</key>
+    <array>
+    </array>
+
+    <!-- List of port numbers to bind to for HTTP [empty = same as "Port"] -->
+    <key>BindHTTPPorts</key>
+    <array>
+    </array>
+
+    <!-- List of port numbers to bind to for SSL [empty = same as "SSLPort"] -->
+    <key>BindSSLPorts</key>
+    <array>
+    </array>
+
+
+    <!--
+        Data Store
+      -->
+
+    <!-- Data root -->
+    <key>DataRoot</key>
+    <string>/Library/AddressBookServer/Data/</string>
+
+    <!-- Document root -->
+    <key>DocumentRoot</key>
+    <string>/Library/AddressBookServer/Documents</string>
+
+    <!-- User quota (in bytes) -->
+    <key>UserQuota</key>
+    <integer>104857600</integer><!-- 104857600 = 100Mb ; use 0 for no quota -->
+
+    <!--
+        Directory service
+
+        A directory service provides information about principals (eg.
+        users, groups, locations and resources) to the server.
+
+        A variety of directory services are available for use.
+      -->
+
+    <!-- XML File Directory Service -->
+    <!--
+    <key>DirectoryService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
+      
+      <key>params</key>
+      <dict>
+        <key>xmlFile</key>
+        <string>/etc/carddavd/accounts.xml</string>
+      </dict>
+    </dict>
+    -->
+    
+    <!-- Cache-on-Demand Open Directory Service (Mac OS X) -->
+    <key>DirectoryService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.appleopendirectory.OpenDirectoryService</string>
+      
+      <key>params</key>
+      <dict>
+        <key>node</key>
+        <string>/Search</string>
+        <key>maxAvailableWaitSeconds</key>
+        <integer>120</integer>			 <!-- Wait for all nodes to be available before starting server.  -1 to wait forever, 0 to not wait -->
+        <key>node</key>
+        <string>/Search</string>
+        <key>restrictEnabledRecords</key>
+        <false/>
+        <key>restrictToGroup</key>
+        <string></string>
+        <key>cacheTimeout</key>
+        <integer>30</integer>
+      </dict>
+    </dict>
+
+    <!--
+        Special principals
+
+        These principals are granted special access and/or perform
+        special roles on the server.
+      -->
+
+    <!-- Principals with "DAV:all" access (relative URLs) -->
+    <key>AdminPrincipals</key>
+    <array>
+      <!-- <string>/principals/__uids__/AEB68DD7-D2B8-4D4D-A574-2A4533DF36A4/</string> -->
+    </array>
+
+    <!-- Principals with "DAV:read" access (relative URLs) -->
+    <key>ReadPrincipals</key>
+    <array>
+      <!-- <string>/principals/__uids__/983C8238-FB6B-4D92-9242-89C0A39E5F81/</string> -->
+    </array>
+
+    <!-- Principals that can pose as other principals -->
+    <key>SudoersFile</key>
+    <string></string>  <!-- empty to disable <string>/etc/carddavd/sudoers.plist</string> -->
+
+    <!--
+        Permissions
+      -->
+
+    <!-- Anonymous read access for root resource -->
+    <key>EnableAnonymousReadRoot</key>
+    <false/>
+
+    <!-- Anonymous read access for resource hierarchy -->
+    <key>EnableAnonymousReadNav</key>
+    <false/>
+
+    <!-- Enables directory listings for principals -->
+    <key>EnablePrincipalListings</key>
+    <false/>
+
+
+    <!--
+        Authentication
+      -->
+
+    <key>Authentication</key>
+    <dict>
+
+      <!-- Clear text; best avoided -->
+      <key>Basic</key>
+      <dict>
+        <key>Enabled</key>
+        <false/>
+      </dict>
+
+      <!-- Digest challenge/response -->
+      <key>Digest</key>
+      <dict>
+        <key>Enabled</key>
+        <true/>
+        <key>Algorithm</key>
+        <string>md5</string>
+        <key>Qop</key>
+        <string></string>
+      </dict>
+
+      <!-- Kerberos/SPNEGO -->
+      <key>Kerberos</key>
+      <dict>
+        <key>Enabled</key>
+        <true/>
+        <key>ServicePrincipal</key>
+        <string></string>
+      </dict>
+
+    </dict>
+
+
+    <!--
+        Logging
+      -->
+
+    <!-- Apache-style access log -->
+    <key>AccessLogFile</key>
+    <string>/var/log/carddavd/access.log</string>
+    <key>RotateAccessLog</key>
+    <false/>
+
+    <!-- Server activity log -->
+    <key>ErrorLogFile</key>
+    <string>/var/log/carddavd/error.log</string>
+
+    <!-- Log levels -->
+    <key>DefaultLogLevel</key>
+    <string>warn</string> <!-- debug, info, warn, error -->
+
+    <!-- Server statistics file -->
+    <key>ServerStatsFile</key>
+    <string>/var/run/carddavd/stats.plist</string>
+
+    <!-- Server process ID file -->
+    <key>PIDFile</key>
+    <string>/var/run/carddavd.pid</string>
+
+
+    <!-- Global server stats -->
+    <key>GlobalStatsSocket</key>
+    <string>/var/run/carddavd-stats.sock</string>
+
+    <!--
+	To snapshot every five minutes over the last hour:
+	    GlobalStatsLoggingPeriod : 60
+	    GlobalStatsLoggingFrequency : 12
+	Set GlobalStatsLoggingFrequency to 0 to disable the stats
+      -->
+
+    <!-- Global server stats logging period -->
+    <key>GlobalStatsLoggingPeriod</key>
+    <integer>60</integer>
+
+    <!-- Global server stats logging frequency -->
+    <key>GlobalStatsLoggingFrequency</key>
+    <integer>12</integer>
+
+    <!--
+        SSL/TLS
+      -->
+
+    <!-- Public key -->
+    <key>SSLCertificate</key>
+    <string></string>
+
+    <!-- Private key -->
+    <key>SSLPrivateKey</key>
+    <string></string>
+
+
+    <!--
+        Process management
+      -->
+
+    <key>UserName</key>
+    <string>_carddav</string>
+
+    <key>GroupName</key>
+    <string>_carddav</string>
+
+    <key>ProcessType</key>
+    <string>Combined</string>
+
+    <key>MultiProcess</key>
+    <dict>
+      <key>ProcessCount</key>
+      <integer>0</integer> <!-- 0 = larger of: 2 or CPU count -->
+    </dict>
+
+
+
+
+    <!--
+        Miscellaneous items
+      -->
+
+    <!-- Maximum results returned by addressbook-query report -->
+    <key>MaxAddressBookQueryResults</key>
+    <integer>1000</integer>
+
+    <!-- Maximum hrefs in addressbook-multiget report -->
+    <key>MaxAddressBookMultigetHrefs</key>
+    <integer>5000</integer>
+
+    <!-- Service ACLs (Mac OS X) -->
+    <key>EnableSACLs</key>
+    <true/>
+
+    <!-- Support for Content-Encoding compression options as specified in RFC2616 Section 3.5 -->
+    <key>ResponseCompression</key>
+    <true/>
+
+    <!--
+        Directory Address Book
+      -->
+    
+    <!--  Disable Directory Address Book-->
+    <!--
+      <key>DirectoryAddressBook</key>
+      <false/>
+    -->
+    
+    <!-- Open Directory-backed Directory Address Book -->
+    <key>DirectoryAddressBook</key>
+    <dict>
+        <key>type</key>
+        <string>twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService</string>
+      
+        <key>params</key>
+        <dict>
+     	  <!-- Search for people records -->
+          <key>queryPeopleRecords</key>
+          <true/>
+     	  <!-- Search for people records in this directory service node -->
+          <key>peopleNode</key>
+          <string>/Search/Contacts</string>
+     	  <!-- Search for user records -->
+          <key>queryUserRecords</key>
+          <true/>
+          <key>userNode</key>
+     	  <!-- Search for user records in this directory service node -->
+          <string>/Search</string>
+      	  <!-- query in directory service local node -->
+          <key>queryDSLocal</key>
+          <false/>
+      	  <!-- minutes to keep directory service local node in memory before refresh -->
+          <key>dsLocalCacheTimeout</key>
+          <integer>30</integer>
+     	  <!-- approx. maximum number of records returned from a directory service query -->
+          <key>maxDSQueryRecords</key>
+          <integer>150</integer> <!-- use 0 to have server calculate the maximum based on MaxAddressBookQueryResults, MaxAddressBookMultigetHrefs keys -->
+     	  <!-- ignore system records like "root" when creating vCards -->
+          <key>ignoreSystemRecords</key>
+          <true/>
+       	  <!-- True to use a directory service query for each addressBook-query to the directory address book -->
+          <key>liveQuery</key>
+          <true/>
+       	  	  <!-- fake the eTag.  If false all directory service attributes are used to calculate the eTag -->
+              <key>fakeETag</key>
+              <true/>
+          <!-- True to store the results on disk after a directory service query. True is usually much slower. -->
+          <key>cacheQuery</key>
+          <false/>
+           	  <!-- If cacheQuery is True and liveQuery is False, the minutes to keep the results from a complete directory service all record query. -->
+              <key>cacheTimeout</key>
+              <integer>30</integer>
+          <!-- add this key to use additional directory service attributes in queries.  Needed for some queries with directory service templates.
+          <key>additionalAttributes</key>
+          <array>
+             <string>dsAttrTypeNative:sampleAIMPreferred</string>
+             <string>dsAttrTypeNative:sampleManager</string>
+          </array>
+          -->
+          <!-- add this key to limit directory service attributes used to make vCard properties
+          		When using directory service templates, list should include only mapped attributes.
+          <key>allowedAttributes</key>
+          <array>
+            <string>dsAttrTypeStandard:AddressLine1</string>
+            <string>dsAttrTypeStandard:AddressLine2</string>
+            <string>dsAttrTypeStandard:AddressLine3</string>
+            <string>dsAttrTypeStandard:Birthday</string>
+            <string>dsAttrTypeStandard:Building</string>
+            <string>dsAttrTypeStandard:City</string>
+            <string>dsAttrTypeStandard:Comment</string>
+            <string>dsAttrTypeStandard:Company</string>
+            <string>dsAttrTypeStandard:Country</string>
+            <string>dsAttrTypeStandard:CreationTimestamp</string>
+            <string>dsAttrTypeStandard:Department</string>
+            <string>dsAttrTypeStandard:EMailAddress</string>
+            <string>dsAttrTypeStandard:EMailContacts</string>
+            <string>dsAttrTypeStandard:FAXNumber</string>
+            <string>dsAttrTypeStandard:FirstName</string>
+            <string>dsAttrTypeStandard:HomePhoneNumber</string>
+            <string>dsAttrTypeStandard:IMHandle</string>
+            <string>dsAttrTypeStandard:JPEGPhoto</string>
+            <string>dsAttrTypeStandard:JobTitle</string>
+            <string>dsAttrTypeStandard:LastName</string>
+            <string>dsAttrTypeStandard:MapCoordinates</string>
+            <string>dsAttrTypeStandard:MiddleName</string>
+            <string>dsAttrTypeStandard:MobileNumber</string>
+            <string>dsAttrTypeStandard:ModificationTimestamp</string>
+            <string>dsAttrTypeStandard:NamePrefix</string>
+            <string>dsAttrTypeStandard:NameSuffix</string>
+            <string>dsAttrTypeStandard:NickName</string>
+            <string>dsAttrTypeStandard:Note</string>
+            <string>dsAttrTypeStandard:OrganizationName</string>
+            <string>dsAttrTypeStandard:PGPPublicKey</string>
+            <string>dsAttrTypeStandard:PagerNumber</string>
+            <string>dsAttrTypeStandard:PhoneContacts</string>
+            <string>dsAttrTypeStandard:PhoneNumber</string>
+            <string>dsAttrTypeStandard:PostalAddress</string>
+            <string>dsAttrTypeStandard:PostalAddressContacts</string>
+            <string>dsAttrTypeStandard:PostalCode</string>
+            <string>dsAttrTypeStandard:Relationships</string>
+            <string>dsAttrTypeStandard:State</string>
+            <string>dsAttrTypeStandard:Street</string>
+            <string>dsAttrTypeStandard:URL</string>
+            <string>dsAttrTypeStandard:UserCertificate</string>
+            <string>dsAttrTypeStandard:UserPKCS12Data</string>
+            <string>dsAttrTypeStandard:UserSMIMECertificate</string>
+            <string>dsAttrTypeStandard:WeblogURI</string>
+          </array>
+          -->
+        </dict>
+    </dict>
+      
+    <!--  allow unauthenticated users to access the directory address book -->
+    <key>AnonymousDirectoryAddressBookAccess</key>
+    <false/>
+    
+    <!--  enable search address book in user's home -->
+    <key>EnableSearchAddressBook</key>
+    <false/>
+    
+    <!--  enable find share report -->
+    <key>EnableFindSharedReport</key>
+    <false/>
+
+    </dict>
+</plist>

Copied: CalendarServer/trunk/contrib/SBS/conf/90-addressBookServer.plist (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/contrib/SBS/conf/90-addressBookServer.plist)
===================================================================
--- CalendarServer/trunk/contrib/SBS/conf/90-addressBookServer.plist	                        (rev 0)
+++ CalendarServer/trunk/contrib/SBS/conf/90-addressBookServer.plist	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>ServiceName</key>
+    <string>addressBookServer</string>
+    
+    <key>RestoreBinaryPath</key>
+    <string>/usr/libexec/server_backup/calendarServer_restore</string>
+    <key>RestoreLog</key>
+    <string>/private/var/log/server_backup/addressBookServer_restore.log</string>
+
+    <key>Version</key>
+    <string>10.6</string>
+</dict>
+</plist>

Copied: CalendarServer/trunk/contrib/launchd/addressbookserver.plist (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/contrib/launchd/addressbookserver.plist)
===================================================================
--- CalendarServer/trunk/contrib/launchd/addressbookserver.plist	                        (rev 0)
+++ CalendarServer/trunk/contrib/launchd/addressbookserver.plist	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Copyright (c) 2006-2009 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.
+ -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+
+  <key>Label</key>
+  <string>org.addressbookserver.addressbookserver</string>
+
+  <key>Disabled</key>
+  <true/>
+
+  <key>ProgramArguments</key>
+  <array>
+    <string>/usr/sbin/carddavd</string>
+    <string>-X</string>
+    <string>-R</string>
+    <string>caldav_kqueue</string>
+  </array>
+
+  <key>InitGroups</key>
+  <true/>
+
+  <key>KeepAlive</key>
+  <true/>
+
+  <key>ThrottleInterval</key>
+  <integer>60</integer>
+  
+  <key>StandardOutPath</key>
+  <string>/var/log/carddavd/error.log</string>
+
+  <key>StandardErrorPath</key>
+  <string>/var/log/carddavd/error.log</string>
+
+  <key>HardResourceLimits</key>
+  <dict>
+    <key>NumberOfFiles</key>
+    <integer>12000</integer>
+  </dict>
+
+  <key>SoftResourceLimits</key>
+  <dict>
+    <key>NumberOfFiles</key>
+    <integer>12000</integer>
+  </dict>
+
+</dict>
+</plist>

Copied: CalendarServer/trunk/doc/carddavd.8 (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/doc/carddavd.8)
===================================================================
--- CalendarServer/trunk/doc/carddavd.8	                        (rev 0)
+++ CalendarServer/trunk/doc/carddavd.8	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,90 @@
+.\"
+.\" Copyright (c) 2006-2008 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.
+.\"
+.\" The following requests are required for all man pages.
+.Dd April 20, 2009
+.Dt CARDDAVD 8
+.Os
+.Sh NAME
+.Nm carddavd
+.Nd Darwin Address Book Server Control Interface
+.Sh SYNOPSIS
+.Nm
+.Op Fl hX 
+.Op Fl u Ar username
+.Op Fl g Ar groupname
+.Op Fl T Ar twistd
+.Op Fl f Ar carddavd.plist
+.Sh DESCRIPTION
+.Nm
+is a front end to the Darwin Address Book Server.  The Darwin Address Book 
+Server is a web server which implements the HTTP, WebDAV, WebDAV ACL,
+and CardDAV protocols.
+.Pp
+.Nm
+is a simple tool for starting the server.
+.Sh OPTIONS
+.Bl -tag -width flag
+.It Fl h
+Displays usage information
+.It Fl X
+Starts the server but does not daemonize it.
+.It Fl u Ar username
+Drops privileges to the given username.
+.It Fl g Ar groupname
+Drops privileges to the given groupname.
+.It Fl f Ar carddavd.plist
+Specifies the path of the configuration file to read.
+.It Fl T Ar twistd
+Specifies the path to the twistd binary.
+.El
+.Sh FILES
+.Bl -tag -width flag
+.It /etc/carddavd/carddavd.plist
+The Address Book Server configuration file.  It is an XML property list
+specifying server options such as the port to bind to, whether to use
+SSL, and the names of other configuration files.
+.It /etc/carddavd/server.pem
+PEM-format server keys for use with SSL.
+.It /Library/AddressBookServer/Documents
+The server's document root, which is used as the backing store for
+the HTTP resources on the server.
+.It /var/log/carddavd/access.log
+The server's access log file, in a format similar to Apache HTTPd's
+access log.
+.It /var/log/carddavd/error.log
+The server's main log file.
+.It /var/run/carddavd.pid
+The server's process ID file.
+.It /usr/share/carddavd
+Server implementation and support files.
+.El
+.Sh SEE ALSO
+.Xr caldavd 8 ,
+.Xr httpd 8 ,
+.Xr serveradmin 8
+.Sh STANDARDS
+.Nm
+complies with
+draft-ietf-vcarddav-carddav (CardDAV),
+RFC 2518 (WebDAV) and
+RFC 3744 (WebDAV ACL).
+.Sh HISTORY
+.Nm
+was first introduced as part of Darwin 10 and Mac OS 10.6.
+.Sh BUGS
+This version of
+.Nm
+is still in development and testing.

Copied: CalendarServer/trunk/lib-patches/vobject/vobject.base.patch (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/lib-patches/vobject/vobject.base.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/vobject/vobject.base.patch	                        (rev 0)
+++ CalendarServer/trunk/lib-patches/vobject/vobject.base.patch	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,20 @@
+Index: vobject/base.py
+===================================================================
+--- vobject/base.py	(revision 219)
++++ vobject/base.py	(working copy)
+@@ -1051,10 +1051,11 @@
+                         component=stack.pop()
+                         if versionLine is not None:
+                             component.setBehaviorFromVersionLine(versionLine)
+-                        else:
+-                            behavior = getBehavior(component.name)
+-                            if behavior:
+-                                component.setBehavior(behavior)
++                        # calendarserver.org: be strict about what we accept
++                        #else:
++                        #    behavior = getBehavior(component.name)
++                        #    if behavior:
++                        #        component.setBehavior(behavior)
+                         if validate: component.validate(raiseException=True)
+                         if transform: component.transformChildrenToNative()
+                         yield component #EXIT POINT

Modified: CalendarServer/trunk/lib-patches/vobject/vobject.icalendar.patch
===================================================================
--- CalendarServer/trunk/lib-patches/vobject/vobject.icalendar.patch	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/lib-patches/vobject/vobject.icalendar.patch	2010-02-09 21:07:27 UTC (rev 5081)
@@ -2,7 +2,21 @@
 ===================================================================
 --- vobject/icalendar.py	(revision 219)
 +++ vobject/icalendar.py	(working copy)
-@@ -1675,9 +1675,10 @@
+@@ -1622,8 +1622,12 @@
+ 
+ # DQUOTE included to work around iCal's penchant for backslash escaping it,
+ # although it isn't actually supposed to be escaped according to rfc2445 TEXT
+-escapableCharList = '\\;,Nn"'
+ 
++# COLON included to work around Address Book's escaping it in vCard data,
++# despite the fact that it is similarly not supposed to be escaped.
++escapableCharList = '\\;:,Nn"'
++strictEscapableCharList = '\\;,Nn'
++
+ def stringToTextValues(s, listSeparator=',', charList=None, strict=False):
+     """Returns list of strings."""
+     
+@@ -1675,9 +1679,10 @@
                  else:
                      current.append(char)
              else:

Copied: CalendarServer/trunk/lib-patches/vobject/vobject.vcard.patch (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/lib-patches/vobject/vobject.vcard.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/vobject/vobject.vcard.patch	                        (rev 0)
+++ CalendarServer/trunk/lib-patches/vobject/vobject.vcard.patch	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,13 @@
+Index: vobject/vcard.py
+===================================================================
+--- vobject/vcard.py	(revision 219)
++++ vobject/vcard.py	(working copy)
+@@ -210,7 +210,7 @@
+ def splitFields(string):
+     """Return a list of strings or lists from a Name or Address."""
+     return [toListOrString(i) for i in
+-            stringToTextValues(string, listSeparator=';', charList=';')]
++            stringToTextValues(string, listSeparator=';')]
+ 
+ def toList(stringOrList):
+     if isinstance(stringOrList, basestring):

Modified: CalendarServer/trunk/run
===================================================================
--- CalendarServer/trunk/run	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/run	2010-02-09 21:07:27 UTC (rev 5081)
@@ -37,6 +37,7 @@
 
 . support/build.sh;
 
+DAVD="cal"
 
 # Echo the usage for the main 'run' script, then exit with an error.
 usage () {
@@ -56,9 +57,9 @@
   echo "	-f  Force setup to run";
   echo "	-n  Do not run setup";
   echo "	-p  Print PYTHONPATH value for server and exit";
-  echo "	-d  Run caldavd as a daemon";
-  echo "	-k  Stop caldavd";
-  echo "	-r  Restart caldavd";
+  echo "	-d  Run ${DAVD}davd as a daemon";
+  echo "	-k  Stop ${DAVD}davd";
+  echo "	-r  Restart ${DAVD}davd";
   echo "	-K  Print value of configuration key and exit";
   echo "	-i  Perform a system install into dst; implies -s";
   echo "	-I  Perform a home install into dst; implies -s";
@@ -76,10 +77,11 @@
 # Parse command-line options to set up state which controls the behavior of the
 # functions in build.sh.
 parse_options () {
-  while getopts 'hvgsfnpdkrK:i:I:t:S:P:R:' option; do
+  while getopts 'ahvgsfnpdkrK:i:I:t:S:P:R:' option; do
     case "$option" in
       '?') usage; ;;
       'h') usage -; exit 0; ;;
+      'a') DAVD=card; plugin_name="carddav"; ;;
       'v')       verbose="-v"; ;;
       'f')   force_setup="true"; ;;
       'k')          kill="true"; ;;
@@ -116,7 +118,7 @@
       echo "Missing config file: ${config}";
       echo "You might want to start by copying the test configuration:";
       echo "";
-      echo "  cp conf/caldavd-test.plist conf/caldavd-dev.plist";
+      echo "  cp conf/${DAVD}davd-test.plist conf/caldavd-dev.plist";
       echo "";
       exit 1;
     fi;
@@ -129,7 +131,7 @@
     echo "";
     echo "Starting server...";
     exec ${caldavd_wrapper_command}                   \
-        "${caldav}/bin/caldavd" ${daemonize}          \
+        "${caldav}/bin/${DAVD}davd" ${daemonize}      \
         -f "${config}"                                \
         -T "${twisted}/bin/twistd"                    \
         -P "${plugin_name}"                           \

Modified: CalendarServer/trunk/setup.py
===================================================================
--- CalendarServer/trunk/setup.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/setup.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -46,9 +46,9 @@
 # Options
 #
 
-description = "CalDAV protocol extensions to twisted.web2.dav",
+description = "CardDAV protocol extensions to twisted.web2.dav",
 long_description = """
-Extends twisted.web2.dav to implement CalDAV-aware resources and methods.
+Extends twisted.web2.dav to implement CardDAV-aware resources and methods.
 """
 
 classifiers = None
@@ -109,9 +109,11 @@
                          "bin/caldavd",
                          "bin/calendarserver_export",
                          "bin/calendarserver_manage_principals",
-                         "bin/calendarserver_command_gateway"
+                         "bin/calendarserver_command_gateway",
+                         "bin/carddavd",
                        ],
-    data_files       = [ ("caldavd", ["conf/caldavd.plist"]) ],
+    data_files       = [ ("caldavd", ["conf/caldavd.plist"]),
+                         ("carddavd", ["conf/carddavd.plist.default" ])],
     ext_modules      = extensions,
     py_modules       = ["kqreactor", "memcacheclient"],
 )

Modified: CalendarServer/trunk/support/Makefile.Apple
===================================================================
--- CalendarServer/trunk/support/Makefile.Apple	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/support/Makefile.Apple	2010-02-09 21:07:27 UTC (rev 5081)
@@ -4,7 +4,7 @@
 #
 # This is only useful internally at Apple, probably.
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -86,8 +86,8 @@
 	$(_v) cd $(BuildDirectory)/Twisted && $(TwistedSubEnvironment) $(PYTHON) twisted/web2/topfiles/setup.py   install $(PY_INSTALL_FLAGS)
 	$(_v) cd $(BuildDirectory)/Twisted && $(TwistedSubEnvironment) $(PYTHON) twisted/words/topfiles/setup.py  install $(PY_INSTALL_FLAGS)
 	$(_v) for so in $$(find "$(DSTROOT)$(PY_HOME)/lib" -type f -name '*.so'); do $(STRIP) -Sx "$${so}"; done
-	@echo "Removing plugins:"
-	$(_v) find "$(DSTROOT)$(PY_HOME)/lib/python/twisted/plugins/" -type f ! -name 'caldav.*' ! -name 'twisted_reactors.*' ! -name 'twisted_trial.*' ! -name '__init__.*' ! -name 'kqueuereactor.*' -print -exec rm '{}' ';'
+	$(_v) echo "Removing plugins:"
+	$(_v) find "$(DSTROOT)$(PY_HOME)/lib/python/twisted/plugins/" -type f ! -name 'caldav.*' ! -name 'carddav.*' ! -name 'twisted_reactors.*' ! -name 'twisted_trial.*' ! -name '__init__.*' ! -name 'kqueuereactor.*' -print -exec rm '{}' ';'
 	$(_v) rm -fr "$(DSTROOT)$(PY_HOME)/lib/python/twisted/python/zsh"
 	$(_v) rm -f  "$(DSTROOT)$(PY_HOME)/lib/python/twisted/python/zshcomp.py"
 	$(_v) rm -f  "$(DSTROOT)$(PY_HOME)/lib/python/twisted/python/zshcomp.py"
@@ -101,6 +101,7 @@
 	@echo "Installing manual pages..."
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(MANDIR)/man8"
 	$(_v) $(INSTALL_FILE) "$(Sources)/doc/caldavd.8"                          "$(DSTROOT)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/carddavd.8"                          "$(DSTROOT)$(MANDIR)/man8"
 	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_export.8"            "$(DSTROOT)$(MANDIR)/man8"
 	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_manage_principals.8" "$(DSTROOT)$(MANDIR)/man8"
 	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_command_gateway.8"   "$(DSTROOT)$(MANDIR)/man8"
@@ -108,8 +109,10 @@
 	@echo "Installing launchd config..."
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(NSLOCALDIR)/$(NSLIBRARYSUBDIR)/$(Project)"
 	$(_v) $(INSTALL_DIRECTORY) -m 0755 "$(DSTROOT)$(VARDIR)/log/caldavd"
+	$(_v) $(INSTALL_DIRECTORY) -m 0755 "$(DSTROOT)$(VARDIR)/log/carddavd"
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(NSLIBRARYDIR)/LaunchDaemons"
 	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/launchd/calendarserver.plist" "$(DSTROOT)$(NSLIBRARYDIR)/LaunchDaemons/org.calendarserver.calendarserver.plist"
+	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/launchd/addressbookserver.plist" "$(DSTROOT)$(NSLIBRARYDIR)/LaunchDaemons/org.addressbookserver.addressbookserver.plist"
 	@echo "Installing migration config..."
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)/System/Library/ServerSetup/MigrationExtras"
 	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/59_calendarmigrator.py" "$(DSTROOT)/System/Library/ServerSetup/MigrationExtras/59_calendarmigrator.py"

Modified: CalendarServer/trunk/support/directorysetup.py
===================================================================
--- CalendarServer/trunk/support/directorysetup.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/support/directorysetup.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 #
 ##
-# Copyright (c) 2007 Apple Inc. All rights reserved.
+# Copyright (c) 2007-2009 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.
@@ -51,7 +51,7 @@
 
 def initComputerRecord(admin_user, admin_pswd, node, recordname):
     plistdefault = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
         <key>ReplicaName</key>
@@ -65,7 +65,7 @@
     plistdefault = plistdefault.replace('\n', '')
 
     plist_good = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
     <dict>
         <key>ReplicaName</key>
@@ -138,12 +138,12 @@
                     <key>http</key>
                     <dict>
                         <key>port</key>
-                        <integer>8008</integer>
+                        <integer>8800</integer>
                     </dict>
                     <key>https</key>
                     <dict>
                         <key>port</key>
-                        <integer>8443</integer>
+                        <integer>8843</integer>
                     </dict>
                 </dict>
 

Modified: CalendarServer/trunk/support/submit
===================================================================
--- CalendarServer/trunk/support/submit	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/support/submit	2010-02-09 21:07:27 UTC (rev 5081)
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 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.
@@ -85,7 +85,7 @@
 
 if [ $# != 0 ]; then usage "Unrecognized arguments:" "$@"; fi;
 
- project="CalendarServer";
+ project="AddressBookServer";
      uri="$(svn info "${src}" --xml | sed -n 's|^.*<url>\(.*\)</url>.*$|\1|p')";
 revision="$(svnversion "${src}")";
 
@@ -106,7 +106,8 @@
     revision="$(echo "${revision}" | sed 's|M$||g')";
   fi;
 
-  project_version="${project}-$((${version} + ${revision} / 10000 % 100)).$((${revision} / 100 % 100)).$((${revision} % 100))";
+#  project_version="${project}-$((${version} + ${revision} / 10000 % 100)).$((${revision} / 100 % 100)).$((${revision} % 100))";
+  project_version="${project}-${version}"
 
   #
   # Make sure changes are checked in.
@@ -120,13 +121,13 @@
 # Do submission
 #
 
-tmp="$(mktemp -d -t CalendarServer_build)";
+tmp="$(mktemp -d -t AddressBookServer_build)";
 wc="${tmp}/${project_version}";
 
 if "${build}"; then
   echo "";
   echo "Copying ${src}...";
-  ignores="$(mktemp -t CalendarServer_ignores)";
+  ignores="$(mktemp -t AddressBookServer_ignores)";
   svn st --no-ignore | sed -n -e 's|^I......||p' > "${ignores}";
   rsync -av --exclude=".svn" --exclude="_trial_temp" --exclude-from="${ignores}" "${src}/" "${wc}";
   rm "${ignores}";
@@ -158,7 +159,7 @@
 
 echo "";
 echo "Preparing sources for ${project_version}...";
-make -C "${wc}" prep;
+make SRCROOT="${wc}" -C "${wc}" prep;
 
 if "${build}"; then
   echo "";

Copied: CalendarServer/trunk/testcarddav (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/testcarddav)
===================================================================
--- CalendarServer/trunk/testcarddav	                        (rev 0)
+++ CalendarServer/trunk/testcarddav	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+wd="$(cd "$(dirname "$0")" && pwd)";
+cdt="${wd}/../CalDAVTester";
+
+##
+# Command line handling
+##
+
+verbose="";
+serverinfo="${cdt}/scripts/server/serverinfo-carddav.xml";
+documentroot="${wd}/twistedcaldav/test/data";
+
+usage ()
+{
+  program="$(basename "$0")";
+  echo "Usage: ${program} [-v] [-d docroot] [-s serverinfo]";
+  echo "Options:";
+  echo "        -h  Print this help and exit";
+  echo "        -d  Set the document root";
+  echo "        -s  Set the serverinfo.xml";
+  echo "        -v  Verbose.";
+
+  if [ "${1-}" == "-" ]; then return 0; fi;
+  exit 64;
+}
+
+while getopts 'hvd:s:' option; do
+  case "$option" in 
+    '?') usage; ;;
+    'h') usage -; exit 0; ;;
+    'd') documentroot="${OPTARG}"; ;;
+    's')   serverinfo="${OPTARG}"; ;;
+    'v')      verbose="v"; ;;
+  esac;
+done;
+
+shift $((${OPTIND} - 1));
+
+if [ $# == 0 ]; then
+  set - "--all";
+fi;
+
+##
+# Do The Right Thing
+##
+
+export PYTHONPATH=$("${wd}/run" -p);
+
+if [ ! -e "${documentroot}/addressbooks/__uids__/user01" ]; then
+  curl "http://localhost:8800/addressbooks/__uids__/user01/";
+fi;
+
+cd "${cdt}" && python testcaldav.py -s "${serverinfo}" "$@";


Property changes on: CalendarServer/trunk/testcarddav
___________________________________________________________________
Added: svn:executable
   + *

Modified: CalendarServer/trunk/twisted/plugins/caldav.py
===================================================================
--- CalendarServer/trunk/twisted/plugins/caldav.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twisted/plugins/caldav.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -1,3 +1,19 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
 from zope.interface import implements
 from twisted.plugin import IPlugin
 from twisted.application.service import IServiceMaker

Copied: CalendarServer/trunk/twisted/plugins/carddav.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twisted/plugins/carddav.py)
===================================================================
--- CalendarServer/trunk/twisted/plugins/carddav.py	                        (rev 0)
+++ CalendarServer/trunk/twisted/plugins/carddav.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.plugins.caldav import TAP
+
+TwistedCardDAV = TAP("calendarserver.tap.carddav.CardDAVServiceMaker")

Modified: CalendarServer/trunk/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/__init__.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/__init__.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -43,10 +43,14 @@
 
 import twisted.web2.dav.davxml
 import twistedcaldav.caldavxml
+import twistedcaldav.carddavxml
+import twistedcaldav.mkcolxml
 import twistedcaldav.customxml
 
 twisted.web2.dav.davxml.registerElements(twistedcaldav.caldavxml)
 twisted.web2.dav.davxml.registerElements(twistedcaldav.customxml)
+twisted.web2.dav.davxml.registerElements(twistedcaldav.carddavxml)
+twisted.web2.dav.davxml.registerElements(twistedcaldav.mkcolxml)
 
 #
 # DefaultHTTPHandler

Copied: CalendarServer/trunk/twistedcaldav/carddavxml.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/carddavxml.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/carddavxml.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/carddavxml.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,776 @@
+##
+# Copyright (c) 2005-2009 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.
+##
+
+"""
+CardDAV XML Support.
+
+This module provides XML utilities for use with CardDAV.
+
+This API is considered private to static.py and is therefore subject to
+change.
+
+See draft spec: 
+"""
+
+from twistedcaldav.vcard import Property as iProperty
+from twistedcaldav.vcard import Component
+
+from twisted.web2.dav import davxml
+
+##
+# CardDAV objects
+##
+
+carddav_namespace = "urn:ietf:params:xml:ns:carddav"
+addressbookserver_namespace = "http://addressbookserver.org/ns/"
+
+carddav_compliance = (
+    "addressbook",
+)
+
+class CardDAVElement (davxml.WebDAVElement):
+    """
+    CardDAV XML element.
+    """
+    namespace = carddav_namespace
+
+class CardDAVEmptyElement (davxml.WebDAVEmptyElement):
+    """
+    CardDAV element with no contents.
+    """
+    namespace = carddav_namespace
+
+class CardDAVTextElement (davxml.WebDAVTextElement):
+    """
+    CardDAV element containing PCDATA.
+    """
+    namespace = carddav_namespace
+
+class CardDAVFilterElement (CardDAVElement):
+    """
+    CardDAV filter element.
+    """
+    def __init__(self, *children, **attributes):
+
+        super(CardDAVFilterElement, self).__init__(*children, **attributes)
+
+        qualifier = None
+        filters = []
+
+        for child in self.children:
+            qname = child.qname()
+            
+            if qname in (
+                (carddav_namespace, "is-not-defined"),
+            ):
+                if qualifier is not None:
+                    raise ValueError("Only one of CardDAV:is-not-defined allowed")
+                qualifier = child
+
+            else:
+                filters.append(child)
+
+        if qualifier and (len(filters) != 0):
+            raise ValueError("No other tests allowed when CardDAV:is-not-defined is present")
+        
+        if self.qname() == (carddav_namespace, "prop-filter"):
+            propfilter_test = attributes.get("test", "anyof")
+            if propfilter_test not in ("anyof", "allof"):
+                raise ValueError("Test must be only one of anyof, allof")
+        else:
+            propfilter_test = "anyof"
+
+        self.propfilter_test = propfilter_test
+        self.qualifier   = qualifier
+        self.filters     = filters
+        self.filter_name = attributes["name"]
+        if isinstance(self.filter_name, unicode):
+            self.filter_name = self.filter_name.encode("utf-8")
+        self.defined     = not self.qualifier or (self.qualifier.qname() != (carddav_namespace, "is-not-defined"))
+
+    def match(self, item):
+        """
+        Returns True if the given address book item (either a property or parameter value)
+        matches this filter, False otherwise.
+        """
+        
+        # Always return True for the is-not-defined case as the result of this will
+        # be negated by the caller
+        if not self.defined: return True
+
+        if self.qualifier and not self.qualifier.match(item): return False
+
+        if len(self.filters) > 0:
+            allof = self.propfilter_test == "allof"
+            for filter in self.filters:
+                if allof != filter._match(item):
+                    return not allof
+            return allof
+        else:
+            return True
+
+class AddressBookHomeSet (CardDAVElement):
+    """
+    The address book collections URLs for this principal.
+    (CardDAV, section 7.1.1)
+    """
+    name = "addressbook-home-set"
+    hidden = True
+
+    allowed_children = { (davxml.dav_namespace, "href"): (0, None) }
+
+class AddressBookDescription (CardDAVTextElement):
+    """
+    Provides a human-readable description of what this address book collection
+    represents.
+    (CardDAV, section 62.1)
+    """
+    name = "addressbook-description"
+    hidden = True
+    # May be protected; but we'll let the client set this if they like.
+
+class SupportedAddressData (CardDAVElement):
+    """
+    Specifies restrictions on an address book collection.
+    (CardDAV, section 6.2.2)
+    """
+    name = "supported-address-data"
+    hidden = True
+    protected = True
+
+    allowed_children = { (carddav_namespace, "addressbook-data"): (0, None) }
+
+class MaxResourceSize (CardDAVTextElement):
+    """
+    Specifies restrictions on an address book collection.
+    (CardDAV, section 6.2.3)
+    """
+    name = "max-resource-size"
+    hidden = True
+    protected = True
+
+class AddressBook (CardDAVEmptyElement):
+    """
+    Denotes an address book collection.
+    (CardDAV, sections 5.2)
+    """
+    name = "addressbook"
+
+class SearchAddressBook (CardDAVEmptyElement):
+    """
+    Denotes a search address book collection, that will respond 
+    to query reports by querying the user-readable address books 
+    on this server not cached by AddressBook.app.
+    For version 1.0, this object simply redirects queries to the open directory address book
+    """
+    name = "searchaddressbook"
+
+class SearchAllAddressBook (CardDAVEmptyElement):
+    """
+    Denotes a search address book collection, that will respond 
+    to query reports by querying the user-readable address books 
+    on this server.
+    For version 1.0, this will include the user's private address book,
+    the open directory address book, and user-readable group (shared) address books.
+    """
+    name = "searchalladdressbook"
+
+class AddressBookQuery (CardDAVElement):
+    """
+    Defines a report for querying address book data.
+    (CardDAV, section 8.6)
+    """
+    name = "addressbook-query"
+
+    allowed_children = {
+        (davxml.dav_namespace, "allprop" ): (0, None),
+        (davxml.dav_namespace, "propname"): (0, None),
+        (davxml.dav_namespace, "prop"    ): (0, None),
+        (carddav_namespace,    "filter"  ): (0, 1), # Actually (1, 1) unless element is empty
+        (carddav_namespace,    "limit"    ): (0, None),
+    }
+
+    def __init__(self, *children, **attributes):
+        super(AddressBookQuery, self).__init__(*children, **attributes)
+
+        query = None
+        filter = None
+        limit = None
+
+        for child in self.children:
+            qname = child.qname()
+
+            if qname in (
+                (davxml.dav_namespace, "allprop" ),
+                (davxml.dav_namespace, "propname"),
+                (davxml.dav_namespace, "prop"    ),
+            ):
+                if query is not None:
+                    raise ValueError("Only one of CardDAV:allprop, CardDAV:propname, CardDAV:prop allowed")
+                query = child
+
+            elif qname == (carddav_namespace, "filter"):
+                filter = child
+            elif qname == (carddav_namespace, "limit"):
+                # type check 
+                child.childOfType(NResults)
+                limit = child
+
+            else:
+                raise AssertionError("We shouldn't be here")
+
+        if len(self.children) > 0:
+            if filter is None:
+                raise ValueError("CARDDAV:filter required")
+
+        self.query  = query
+        self.filter = filter
+        self.limit = limit
+
+class AddressData (CardDAVElement):
+    """
+    Defines which parts of a address component object should be returned by a
+    report.
+    (CardDAV, section 10.4)
+    """
+    name = "address-data"
+
+    allowed_children = {
+        (carddav_namespace, "allprop"): (0, 1),
+        (carddav_namespace, "prop"): (0, None),
+        davxml.PCDATAElement:        (0, None),
+    }
+    allowed_attributes = {
+        "content-type": False,
+        "version"     : False,
+    }
+
+    @classmethod
+    def fromAddress(clazz, address):
+        assert address.name() == "VCARD", "Not a vCard: %r" % (address,)
+        return clazz(davxml.PCDATAElement(str(address)))
+
+    @classmethod
+    def fromAddressData(clazz, addressdata):
+        """
+        Return a AddressData element comprised of the supplied address data.
+        @param addressdata: a string of valid address data.
+        @return: a L{Addressata} element.
+        """
+        return clazz(davxml.PCDATAElement(addressdata))
+
+    def __init__(self, *children, **attributes):
+        super(AddressData, self).__init__(*children, **attributes)
+
+        properties = None
+        data       = None
+
+        for child in self.children:
+            qname = child.qname()
+
+            if qname == (carddav_namespace, "allprop"):
+                if properties is not None:
+                    raise ValueError(
+                        "CardDAV:allprop and CardDAV:prop may not be combined"
+                    )
+                properties = child
+
+            elif qname == (carddav_namespace, "prop"):
+                try:
+                    properties.append(child)
+                except AttributeError:
+                    if properties is None:
+                        properties = [child]
+                    else:
+                        raise ValueError("CardDAV:allprop and CardDAV:prop may not be combined")
+
+            elif isinstance(child, davxml.PCDATAElement):
+                if data is None:
+                    data = child
+                else:
+                    data += child
+
+            else: raise AssertionError("We shouldn't be here")
+
+
+        self.properties = properties
+
+        if data is not None:
+            try:
+                if properties is not None:
+                    raise ValueError("Only one of allprop, prop (%r) or PCDATA (%r) allowed"% (properties, str(data)))
+            except ValueError:
+                if not data.isWhitespace(): raise
+            else:
+                # Since we've already combined PCDATA elements, we'd may as well
+                # optimize them originals away
+                self.children = (data,)
+
+        if "content-type" in attributes:
+            self.content_type = attributes["content-type"]
+        else:
+            self.content_type = "text/vcard"
+
+        if "version" in attributes:
+            self.version = attributes["version"]
+        else:
+            self.version = "3.0"
+
+    def verifyTypeVersion(self, types_and_versions):
+        """
+        Make sure any content-type and version matches at least one of the supplied set.
+        
+        @param types_and_versions: a list of (content-type, version) tuples to test against.
+        @return:                   True if there is at least one match, False otherwise.
+        """
+        for item in types_and_versions:
+            if (item[0] == self.content_type) and (item[1] == self.version):
+                return True
+        
+        return False
+
+    def elementFromResource(self, resource):
+        """
+        Return a new AddressData element comprised of the possibly filtered
+        address data from the specified resource. If no filter is being applied
+        read the data directly from the resource without parsing it. If a filter
+        is required, parse the vCard data and filter using this AddressData.
+        @param resource: the resource whose address data is to be returned.
+        @return: an L{AddressData} with the (filtered) address data.
+        """
+        # Check for filtering or not
+        if self.children:
+            filtered = self.getFromvCard(resource.vCard())
+            return AddressData.fromAddress(filtered)
+        else:
+            return resource.vCardXML()
+
+    def elementFromAddress(self, address):
+        """
+        Return a new AddressData element comprised of the possibly filtered
+        address.
+        @param address: the address that is to be filtered and returned.
+        @return: an L{AddressData} with the (filtered) address data.
+        """
+        
+        # Check for filtering or not
+        filtered = self.getFromvCard(address)
+        return AddressData.fromAddress(filtered)
+
+    def getFromvCard(self, address):
+        """
+        Returns an address object containing the data in the given vCard
+        which is specified by this AddressData.
+        """
+        if address.name() != "VCARD":
+            raise ValueError("Not a vCard: %r" % (address,))
+
+        # Empty element: get all data
+        if not self.children: return address
+
+        # property filtering
+        # copy requested properties
+        vcard = Component("VCARD")
+        allProps = True
+        for property in self.children:
+            if isinstance(property, Property):
+                allProps = False
+                for addressProperty in address.properties(property.attributes["name"]):
+                    vcard.addProperty(addressProperty)
+        
+        # add required properties
+        if allProps:
+            vcard = address
+        else:
+            for requiredProperty in ('N', 'FN', 'VERSION'):
+                if not vcard.hasProperty(requiredProperty):
+                    vcard.addProperty(address.getProperty(requiredProperty))
+
+        return vcard
+
+    def address(self):
+        """
+        Returns an address component derived from this element.
+        """
+        for data in self.children:
+            if not isinstance(data, davxml.PCDATAElement):
+                return None
+            else:
+                # We guaranteed in __init__() that there is only one child...
+                break
+
+        return None # TODO: iComponent.fromString(str(data))
+
+
+class AllProperties (CardDAVEmptyElement):
+    """
+    Specifies that all properties shall be returned.
+    (CardDAV, section 10.4.1)
+    """
+    name = "allprop"
+
+class Property (CardDAVEmptyElement):
+    """
+    Defines a property to return in a response.
+    (CardDAV, section 10.4.2)
+    """
+    name = "prop"
+
+    allowed_attributes = {
+        "name"   : True,
+        "novalue": False,
+    }
+
+    def __init__(self, *children, **attributes):
+        super(Property, self).__init__(*children, **attributes)
+
+        self.property_name = attributes["name"]
+
+        if "novalue" in attributes:
+            novalue = attributes["novalue"]
+            if novalue == "yes":
+                self.novalue = True
+            elif novalue == "no":
+                self.novalue = False
+            else:
+                raise ValueError("Invalid novalue: %r" % (novalue,))
+        else:
+            self.novalue = False
+
+class Filter (CardDAVElement):
+    """
+    Determines which matching components are returned.
+    (CardDAV, section 10.5)
+    """
+    name = "filter"
+
+    allowed_children = { (carddav_namespace, "prop-filter"): (0, None) }
+    allowed_attributes = { "test": False }
+
+
+    def __init__(self, *children, **attributes):
+
+        super(Filter, self).__init__(*children, **attributes)
+
+        filter_test = attributes.get("test", "anyof")
+        if filter_test not in ("anyof", "allof"):
+            raise ValueError("Test must be only one of anyof, allof")
+        
+        self.filter_test = filter_test
+
+    def match(self, vcard):
+        """
+        Returns True if the given address property matches this filter, False
+        otherwise. Empty element means always match.
+        """
+ 
+        if len(self.children) > 0:
+            allof = self.filter_test == "allof"
+            for propfilter in self.children:
+                if allof != propfilter._match(vcard):
+                    return not allof
+            return allof
+        else:
+            return True
+
+    def valid(self):
+        """
+        Indicate whether this filter element's structure is valid wrt vCard
+        data object model.
+        
+        @return: True if valid, False otherwise
+        """
+        
+        # Test each property
+        for propfilter in self.children:
+            if not propfilter.valid():
+                return False
+        else:
+            return True
+        
+class PropertyFilter (CardDAVFilterElement):
+    """
+    Limits a search to specific properties.
+    (CardDAV-access-09, section 10.5.1)
+    """
+    name = "prop-filter"
+
+    allowed_children = {
+        (carddav_namespace, "is-not-defined" ): (0, 1),
+        (carddav_namespace, "text-match"     ): (0, None),
+        (carddav_namespace, "param-filter"   ): (0, None),
+    }
+    allowed_attributes = {
+        "name": True,
+        "test": False,
+    }
+
+    def _match(self, vcard):
+        # At least one property must match (or is-not-defined is set)
+        for property in vcard.properties():
+            if property.name() == self.filter_name and self.match(property): break
+        else:
+            return not self.defined
+        return self.defined
+
+    def valid(self):
+        """
+        Indicate whether this filter element's structure is valid wrt vCard
+        data object model.
+        
+        @return:      True if valid, False otherwise
+        """
+        
+        # No tests
+        return True
+
+class ParameterFilter (CardDAVFilterElement):
+    """
+    Limits a search to specific parameters.
+    (CardDAV, section 10.5.2)
+    """
+    name = "param-filter"
+
+    allowed_children = {
+        (carddav_namespace, "is-not-defined" ): (0, 1),
+        (carddav_namespace, "text-match"     ): (0, 1),
+    }
+    allowed_attributes = { "name": True }
+
+    def _match(self, property):
+
+        # At least one parameter must match (or is-not-defined is set)
+        result = not self.defined
+        for parameterName in property.params().keys():
+            if parameterName == self.filter_name and self.match(property.params()[parameterName]):
+                result = self.defined
+                break
+
+        return result
+
+class Limit (davxml.WebDAVElement):
+    """
+    Client supplied limit for reports.
+    """
+    namespace = carddav_namespace
+    name = "limit"
+    allowed_children = {
+        (carddav_namespace, "nresults" )  : (1, 1),
+    }
+
+class NResults (davxml.WebDAVTextElement):
+    """
+    Number of results limit.
+    """
+    namespace = carddav_namespace
+    name = "nresults"
+
+
+class IsNotDefined (CardDAVEmptyElement):
+    """
+    Specifies that the named vCard item does not exist.
+    (CardDAV, section 10.5.3)
+    """
+    name = "is-not-defined"
+
+    def match(self, component):
+        # Oddly, this needs always to return True so that it appears there is
+        # a match - but we then "negate" the result if is-not-defined is set.
+        # Actually this method should never be called as we special case the
+        # is-not-defined option.
+        return True
+
+class TextMatch (CardDAVTextElement):
+    """
+    Specifies a substring match on a property or parameter value.
+    (CardDAV, section 10.5.4)
+    """
+    name = "text-match"
+
+    def fromString(clazz, string): #@NoSelf
+        if type(string) is str:
+            return clazz(davxml.PCDATAElement(string))
+        elif type(string) is unicode:
+            return clazz(davxml.PCDATAElement(string.encode("utf-8")))
+        else:
+            return clazz(davxml.PCDATAElement(str(string)))
+
+    fromString = classmethod(fromString)
+
+    allowed_attributes = {
+        "collation": False,
+        "negate-condition": False,
+        "match-type": False
+    }
+
+    def __init__(self, *children, **attributes):
+        super(TextMatch, self).__init__(*children, **attributes)
+
+        if "collation" in attributes:
+            self.collation = attributes["collation"]
+        else:
+            self.collation = "i;unicode-casemap"
+
+        if "negate-condition" in attributes:
+            self.negate = attributes["negate-condition"]
+            if self.negate not in ("yes", "no"):
+                self.negate = "no"
+            self.negate = {"yes": True, "no": False}[self.negate]
+        else:
+            self.negate = False
+
+        if "match-type" in attributes:
+            self.match_type = attributes["match-type"]
+            if self.match_type not in (
+                "equals",
+                "contains",
+                "starts-with",
+                "ends-with",
+            ):
+                self.match_type = "contains"
+        else:
+            self.match_type = "contains"
+
+    def _match(self, item):
+        """
+        Match the text for the item.
+        If the item is a property, then match the property value,
+        otherwise it may be a list of parameter values - try to match anyone of those
+        """
+        if item is None: return False
+
+        if isinstance(item, iProperty):
+            values = [item.value()]
+        else:
+            values = item
+
+        test = unicode(str(self), "utf-8").lower()
+
+        def _textCompare(s):
+            s = s.lower()
+            
+            #print("test=%r, s=%r, matchType=%r" % (test, s, self.match_type))
+            
+            if self.match_type == "equals":
+                return s == test
+            elif self.match_type == "contains":
+                return s.find(test) != -1 
+            elif self.match_type == "starts-with":
+                return s.startswith(test)
+            elif self.match_type == "ends-with":
+                return s.endswith(test)
+            else:
+                return False
+
+        for value in values:
+            # NB Its possible that we have a text list value which appears as a Python list,
+            # so we need to check for that an iterate over the list.
+            if isinstance(value, list):
+                for subvalue in value:
+                    if _textCompare(unicode(subvalue)):
+                        return not self.negate
+            else:
+                if _textCompare(unicode(value)):
+                    return not self.negate
+        
+        return self.negate
+
+    def match(self, item):
+        return self._match(item)
+
+class AddressBookMultiGet (CardDAVElement):
+    """
+    CardDAV report used to retrieve specific vCard items via their URIs.
+    (CardDAV, section 10.6)
+    """
+    name = "addressbook-multiget"
+
+    # To allow for an empty element in a supported-report-set property we need
+    # to relax the child restrictions
+    allowed_children = {
+        (davxml.dav_namespace, "allprop" ): (0, 1),
+        (davxml.dav_namespace, "propname"): (0, 1),
+        (davxml.dav_namespace, "prop"    ): (0, 1),
+        (davxml.dav_namespace, "href"    ): (0, None),    # Actually ought to be (1, None)
+    }
+
+    def __init__(self, *children, **attributes):
+        super(AddressBookMultiGet, self).__init__(*children, **attributes)
+
+        property = None
+        resources = []
+
+        for child in self.children:
+            qname = child.qname()
+
+            if qname in (
+                (davxml.dav_namespace, "allprop" ),
+                (davxml.dav_namespace, "propname"),
+                (davxml.dav_namespace, "prop"    ),
+            ):
+                if property is not None:
+                    raise ValueError("Only one of DAV:allprop, DAV:propname, DAV:prop allowed")
+                property = child
+
+            elif qname == (davxml.dav_namespace, "href"):
+                resources.append(child)
+
+        self.property  = property
+        self.resources = resources
+
+class AddressBookFindShared (davxml.WebDAVElement):
+    """
+    Report used to retrieve shared address books accessible for a user principal
+    """
+    name = "addressbook-findshared"
+    namespace = addressbookserver_namespace
+
+    allowed_children = {
+#         (davxml.dav_namespace, "href"    ): (0, None),    # Actually ought to be (1, None)
+    }
+
+
+class NoUIDConflict(CardDAVElement):
+    """
+    CardDAV precondition used to indicate a UID conflict during PUT/COPY/MOVE.
+    The conflicting resource href must be returned as a child.
+    """
+    name = "no-uid-conflict"
+
+    allowed_children = { (davxml.dav_namespace, "href"): (1, 1) }
+    
+class SupportedFilter(CardDAVElement):
+    """
+    CardDAV precondition used to indicate an unsupported component type in a
+    query filter.
+    The conflicting filter elements are returned.
+    """
+    name = "supported-filter"
+
+    allowed_children = {
+        (carddav_namespace, "prop-filter" ): (0, None),
+        (carddav_namespace, "param-filter"): (0, None)
+    }
+    
+##
+# Extensions to davxml.ResourceType
+##
+
+def _isAddressBook(self): return bool(self.childrenOfType(AddressBook))
+davxml.ResourceType.isAddressBook = _isAddressBook
+davxml.ResourceType.addressbook = davxml.ResourceType(davxml.Collection(), AddressBook())
+davxml.ResourceType.searchaddressbook = davxml.ResourceType(davxml.Collection(), SearchAddressBook())
+davxml.ResourceType.searchalladdressbook = davxml.ResourceType(davxml.Collection(), SearchAllAddressBook())

Copied: CalendarServer/trunk/twistedcaldav/directory/addressbook.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/directory/addressbook.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/addressbook.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/addressbook.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,452 @@
+##
+# Copyright (c) 2006-2009 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.
+##
+
+"""
+Implements a directory-backed addressbook hierarchy.
+"""
+
+__all__ = [
+    "uidsResourceName",
+   #"DirectoryAddressBookProvisioningResource",
+    "DirectoryAddressBookHomeProvisioningResource",
+    "DirectoryAddressBookHomeTypeProvisioningResource",
+    "DirectoryAddressBookHomeUIDProvisioningResource",
+    "DirectoryAddressBookHomeResource",
+]
+
+from twisted.internet.defer import succeed
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.http import HTTPError
+from twisted.web2.dav.util import joinURL
+from twisted.web2.dav.resource import TwistedACLInheritable, TwistedQuotaRootProperty
+
+from twistedcaldav.config import config
+from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVResource
+from twistedcaldav.resource import CalDAVResource, SearchAddressBookResource, SearchAllAddressBookResource
+from twistedcaldav.directory.idirectory import IDirectoryService
+from twistedcaldav.directory.resource import AutoProvisioningResourceMixIn
+
+from twistedcaldav.directory.directory import DirectoryService
+
+from twistedcaldav.report_addressbook_findshared import getReadWriteSharedAddressBookGroups, getReadOnlySharedAddressBookGroups, getWritersGroupForSharedAddressBookGroup
+
+# Use __underbars__ convention to avoid conflicts with directory resource types.
+uidsResourceName = "__uids__"
+
+
+class DirectoryAddressBookProvisioningResource (
+    AutoProvisioningResourceMixIn,
+    ReadOnlyResourceMixIn,
+    DAVResource,
+):
+    def defaultAccessControlList(self):
+        return config.ProvisioningResourceACL
+
+    def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
+        # Permissions here are fixed, and are not subject to inherritance rules, etc.
+        return succeed(self.defaultAccessControlList())
+
+
+class DirectoryAddressBookHomeProvisioningResource (DirectoryAddressBookProvisioningResource):
+    """
+    Resource which provisions address book home collections as needed.    
+    """
+    def __init__(self, directory, url):
+        """
+        @param directory: an L{IDirectoryService} to provision address books from.
+        @param url: the canonical URL for the resource.
+        """
+        assert directory is not None
+        assert url.endswith("/"), "Collection URL must end in '/'"
+
+        DAVResource.__init__(self)
+
+        self.directory = IDirectoryService(directory)
+        self._url = url
+
+        # FIXME: Smells like a hack
+        directory.addressBookHomesCollection = self
+
+        #
+        # Create children
+        #
+        def provisionChild(name):
+            self.putChild(name, self.provisionChild(name))
+
+        for recordType in self.directory.recordTypes():
+            provisionChild(recordType)
+
+        provisionChild(uidsResourceName)
+
+    def provisionChild(self, recordType):
+        raise NotImplementedError("Subclass must implement provisionChild()")
+
+    def url(self):
+        return self._url
+
+    def getChild(self, name):
+        return self.putChildren.get(name, None)
+
+    def listChildren(self):
+        return self.directory.recordTypes()
+
+    def principalCollections(self):
+        # FIXME: directory.principalCollection smells like a hack
+        # See DirectoryPrincipalProvisioningResource.__init__()
+        return self.directory.principalCollection.principalCollections()
+
+    def principalForRecord(self, record):
+        # FIXME: directory.principalCollection smells like a hack
+        # See DirectoryPrincipalProvisioningResource.__init__()
+        return self.directory.principalCollection.principalForRecord(record)
+
+    def homeForDirectoryRecord(self, record):
+        uidResource = self.getChild(uidsResourceName)
+        if uidResource is None:
+            return None
+        else:
+            return uidResource.getChild(record.guid)
+
+    ##
+    # DAV
+    ##
+    
+    def isCollection(self):
+        return True
+
+
+class DirectoryAddressBookHomeTypeProvisioningResource (DirectoryAddressBookProvisioningResource):
+    """
+    Resource which provisions address book home collections of a specific
+    record type as needed.
+    """
+    def __init__(self, parent, recordType):
+        """
+        @param parent: the parent of this resource
+        @param recordType: the directory record type to provision.
+        """
+        assert parent is not None
+        assert recordType is not None
+
+        DAVResource.__init__(self)
+
+        self.directory = parent.directory
+        self.recordType = recordType
+        self._parent = parent
+
+    def url(self):
+        return joinURL(self._parent.url(), self.recordType)
+
+    def getChild(self, name, record=None):
+        self.provision()
+        if name == "":
+            return self
+
+        if record is None:
+            record = self.directory.recordWithShortName(self.recordType, name)
+            if record is None:
+                return None
+
+        return self._parent.homeForDirectoryRecord(record)
+
+    def listChildren(self):
+        if config.EnablePrincipalListings:
+
+            def _recordShortnameExpand():
+                for record in self.directory.listRecords(self.recordType):
+                    if record.enabledForAddressBooks:
+                        for shortName in record.shortNames:
+                            yield shortName
+
+            return _recordShortnameExpand()
+        else:
+            # Not a listable collection
+            raise HTTPError(responsecode.FORBIDDEN)
+
+    ##
+    # DAV
+    ##
+    
+    def isCollection(self):
+        return True
+
+    def http_COPY(self, request):
+        return responsecode.FORBIDDEN
+
+    ##
+    # ACL
+    ##
+
+    def principalCollections(self):
+        return self._parent.principalCollections()
+
+    def principalForRecord(self, record):
+        return self._parent.principalForRecord(record)
+
+
+class DirectoryAddressBookHomeUIDProvisioningResource (DirectoryAddressBookProvisioningResource):
+    def __init__(self, parent):
+        """
+        @param parent: the parent of this resource
+        """
+        assert parent is not None
+
+        DAVResource.__init__(self)
+
+        self.directory = parent.directory
+        self.parent = parent
+
+    def url(self):
+        return joinURL(self.parent.url(), uidsResourceName)
+
+    def getChild(self, name, record=None):
+        self.provision()
+        if name == "":
+            return self
+
+        if record is None:
+            record = self.directory.recordWithUID(name)
+            if record is None:
+                return None
+
+        return self.provisionChild(name)
+
+    def listChildren(self):
+        # Not a listable collection
+        raise HTTPError(responsecode.FORBIDDEN)
+
+    ##
+    # DAV
+    ##
+    
+    def isCollection(self):
+        return True
+
+    ##
+    # ACL
+    ##
+
+    def principalCollections(self):
+        return self.parent.principalCollections()
+
+    def principalForRecord(self, record):
+        return self.parent.principalForRecord(record)
+
+
+class DirectoryAddressBookHomeResource (AutoProvisioningResourceMixIn, CalDAVResource):
+    """
+    Address book home collection resource.
+    """
+    def __init__(self, parent, record):
+        """
+        @param path: the path to the file which will back the resource.
+        """
+        assert parent is not None
+        assert record is not None
+
+        CalDAVResource.__init__(self)
+
+        self.record = record
+        self.parent = parent
+        
+        # Cache children which must be of a specific type
+        childlist = ()
+        if config.EnableSearchAddressBook and config.DirectoryAddressBook:
+            childlist += (("search" , SearchAddressBookResource ), )
+        if config.EnableSearchAllAddressBook:
+            childlist += (("searchall" , SearchAllAddressBookResource ),)
+        
+        for name, cls in childlist:
+            child = self.provisionChild(name)
+            assert isinstance(child, cls), "Child %r not a %s: %r" % (name, cls.__name__, child)
+            self.putChild(name, child)
+
+
+    def provisionDefaultAddressBooks(self):
+        self.provision()
+
+        childName = "addressbook"
+        child = self.provisionChild(childName)
+        assert isinstance(child, CalDAVResource), "Child %r is not a %s: %r" % (childName, CalDAVResource.__name__, child) #@UndefinedVariable
+
+        return child.createAddressBookCollection()
+
+    def provisionChild(self, name):
+        raise NotImplementedError("Subclass must implement provisionChild()")
+
+    def url(self):
+        return joinURL(self.parent.url(), self.record.guid)
+        ##
+        ## While the underlying primary location is GUID-based, we want
+        ## the canonical user-facing location to be recordType &
+        ## shortName-based, because that's friendlier.
+        ##
+        #return joinURL(self.parent.parent.getChild(self.record.recordType).url(), self.record.shortName)
+
+    ##
+    # DAV
+    ##
+    
+    def isCollection(self):
+        return True
+
+    ##
+    # ACL
+    ##
+
+    def owner(self, request):
+        return succeed(davxml.HRef(self.principalForRecord().principalURL()))
+
+    def _determineGroupAccessMode(self):
+        """
+        Determines whether this record (assumed to be a group) is provisioned in the list of read-write address books or read-only address books
+        Returns:
+            "ReadWrite", "ReadOnly" or None
+        """
+        
+        members = getReadWriteSharedAddressBookGroups(self.record.service)      # list of members of the "ab_readwrite" group
+        if self.record in members:                                              # membership must be explicit in the "ab_readwrite" group - no membership expansion is done
+            return "ReadWrite"
+
+        members = getReadOnlySharedAddressBookGroups(self.record.service)       # list of members of the "ab_readonly" group
+        if self.record in members:                                              # membership must be explicit in the "ab_readonly" group - no membership expansion is done
+            return "ReadOnly"
+            
+        return None
+    
+    def _getWritersURL(self):
+        """
+           Looks for a "-writers" group, and if found, extract the principal URL to it
+        """
+
+        writerRecord = getWritersGroupForSharedAddressBookGroup(self.record)
+        if writerRecord == None:
+            return None
+        
+        
+        # TO-DO: Need better way to build the principal URL to the "-writers" group
+        #principalURL = "/principals/__uids__/%s/" % writerRecord.guid
+        
+        principalURL = None
+        for principalCollection in self.principalCollections():     # based on principalForCalendarUserAddress in resource.CalDAVResource
+            groups = principalCollection.getChild(DirectoryService.recordType_groups)       # get only the "groups" collection within the parent collection
+            if groups:
+                p = groups.principalForRecord(writerRecord)
+                if p is not None:
+                    principalURL = p.principalURL()
+                    break
+        
+        return principalURL
+        
+    def ownerPrincipal(self, request):
+        return succeed(self.principalForRecord())
+
+    def defaultAccessControlList(self):
+        myPrincipal = self.principalForRecord()
+        
+        if self.record.recordType != DirectoryService.recordType_groups:
+            # Original ACE logic
+            aces = (
+                # DAV:read access for authenticated users.
+                davxml.ACE(
+                    davxml.Principal(davxml.Authenticated()),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+                    ),
+                ),
+                # Inheritable DAV:all access for the resource's associated principal.
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(myPrincipal.principalURL())),
+                    davxml.Grant(davxml.Privilege(davxml.All())),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ),
+            )
+        else:
+            # Determine access for this group (members are read-write or members are read-only)
+            accMode = self._determineGroupAccessMode()
+                        
+            aces = ()
+                 
+            if accMode == "ReadWrite":
+                aces += (davxml.ACE(
+                    davxml.Principal(davxml.HRef(myPrincipal.principalURL())),
+                    davxml.Grant(davxml.Privilege(davxml.All())),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ), )            
+            elif accMode == "ReadOnly":
+                aces += (davxml.ACE(
+                    davxml.Principal(davxml.HRef(myPrincipal.principalURL())),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+                    ),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ), )          
+                
+                # Look for a "-writers" group and add those members with read-write access
+                writerURL = self._getWritersURL()
+                if writerURL:
+                    aces += (davxml.ACE(
+                        davxml.Principal(davxml.HRef(writerURL)),
+                        davxml.Grant(davxml.Privilege(davxml.All())),
+                        davxml.Protected(),
+                        TwistedACLInheritable(),
+                    ), )            
+            else:
+                pass
+
+        # Give read access to config.ReadPrincipals
+        aces += config.ReadACEs
+
+        # Give all access to config.AdminPrincipals
+        aces += config.AdminACEs
+        
+        return davxml.ACL(*aces)
+
+    def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
+        # Permissions here are fixed, and are not subject to inheritance rules, etc.
+        return succeed(self.defaultAccessControlList())
+
+    def principalCollections(self):
+        return self.parent.principalCollections()
+
+    def principalForRecord(self):
+        return self.parent.principalForRecord(self.record)
+
+    ##
+    # Quota
+    ##
+
+    def hasQuotaRoot(self, request):
+        """
+        @return: a C{True} if this resource has quota root, C{False} otherwise.
+        """
+        return self.hasDeadProperty(TwistedQuotaRootProperty) or config.UserQuota > 0
+    
+    def quotaRoot(self, request):
+        """
+        @return: a C{int} containing the maximum allowed bytes if this collection
+            is quota-controlled, or C{None} if not quota controlled.
+        """
+        if self.hasDeadProperty(TwistedQuotaRootProperty):
+            return int(str(self.readDeadProperty(TwistedQuotaRootProperty)))
+        else:
+            return config.UserQuota if config.UserQuota > 0 else None

Modified: CalendarServer/trunk/twistedcaldav/directory/aggregate.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/aggregate.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/directory/aggregate.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -86,6 +86,20 @@
 
     calendarHomesCollection = property(_getCalendarHomesCollection, _setCalendarHomesCollection)
 
+    #
+    # Define addressBookHomesCollection as a property so we can set it on contained services
+    # See AddressBookHomeProvisioningFile.__init__()
+    #
+    def _getAddressBookHomesCollection(self):
+        return self._addressBookHomesCollection
+
+    def _setAddressBookHomesCollection(self, value):
+        for service in self._recordTypes.values():
+            service.addressBookHomesCollection = value
+        self._addressBookHomesCollection = value
+
+    addressBookHomesCollection = property(_getAddressBookHomesCollection, _setAddressBookHomesCollection)
+
     def recordTypes(self):
         return set(self._recordTypes)
 

Modified: CalendarServer/trunk/twistedcaldav/directory/augment.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/augment.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/directory/augment.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -40,11 +40,13 @@
         hostedAt="",
         enabledForCalendaring=False,
         autoSchedule=False,
+        enabledForAddressBooks=False,
     ):
         self.guid = guid
         self.enabled = enabled
         self.hostedAt = hostedAt
         self.enabledForCalendaring = enabledForCalendaring
+        self.enabledForAddressBooks = enabledForAddressBooks
         self.autoSchedule = autoSchedule
 
 class AugmentDB(object):

Modified: CalendarServer/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -56,6 +56,7 @@
     realmName = None
 
     recordType_users = "users"
+    recordType_people = "people"
     recordType_groups = "groups"
     recordType_locations = "locations"
     recordType_resources = "resources"
@@ -79,6 +80,20 @@
     baseGUID = None
     guid = property(_generatedGUID)
 
+    # Needed by twistedcaldav.directorybackedaddressbook
+    liveQuery = False
+
+    def available(self):
+        """
+        By default, the directory is available.  This may return a boolean or a
+        Deferred which fires a boolean.
+
+        A return value of "False" means that the directory is currently
+        unavailable due to the service starting up.
+        """
+        return True
+    # end directorybackedaddressbook requirements
+
     ##
     # ICredentialsChecker
     ##
@@ -324,6 +339,8 @@
         self, service, recordType, guid,
         shortNames=(), authIDs=set(), fullName=None,
         firstName=None, lastName=None, emailAddresses=set(),
+        calendarUserAddresses=set(), autoSchedule=False, enabledForCalendaring=None,
+        enabledForAddressBooks=None,
         uid=None,
     ):
         assert service.realmName is not None
@@ -351,9 +368,11 @@
         self.firstName             = firstName
         self.lastName              = lastName
         self.emailAddresses        = emailAddresses
-        self.enabledForCalendaring = False
-        self.autoSchedule          = False
+        self.enabledForCalendaring = enabledForCalendaring
+        self.autoSchedule          = autoSchedule
+        self.enabledForAddressBooks = enabledForAddressBooks
 
+
     def get_calendarUserAddresses(self):
         """
         Dynamically construct a calendarUserAddresses attribute which describes
@@ -395,6 +414,7 @@
             self.enabled = augment.enabled
             self.hostedAt = augment.hostedAt
             self.enabledForCalendaring = augment.enabledForCalendaring
+            self.enabledForAddressBooks = augment.enabledForAddressBooks
             self.autoSchedule = augment.autoSchedule
 
             if self.enabledForCalendaring and self.recordType == self.service.recordType_groups:

Modified: CalendarServer/trunk/twistedcaldav/directory/idirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/idirectory.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/directory/idirectory.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -105,6 +105,7 @@
     lastName              = Attribute("The last name of this record.")
     emailAddress          = Attribute("The email address of this record.")
     enabledForCalendaring = Attribute("Determines whether this record should be provisioned with a calendar home.")
+    enabledForAddressBooks = Attribute("Determines whether this record should be provisioned with an address book home.")
     calendarUserAddresses = Attribute(
         """
         An iterable of C{str}s representing calendar user addresses for this

Copied: CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/directory/opendirectorybacker.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,2071 @@
+##
+# Copyright (c) 2006-2009 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.
+##
+
+
+"""
+Apple Open Directory directory service implementation for backing up directory-backed address books
+"""
+
+__all__ = [
+    "OpenDirectoryBackingService", "VCardRecord",
+]
+
+import traceback
+import hashlib
+
+import os
+import sys
+import time
+
+from os import listdir
+from os.path import join, abspath
+from tempfile import mkstemp, gettempdir
+from random import random
+from urllib import unquote
+
+import opendirectory
+import dsattributes
+
+from dsquery import match, expression
+from socket import getfqdn
+
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator
+from twisted.internet.threads import deferToThread
+from twisted.python.filepath import FilePath
+from twisted.web2.dav import davxml
+from twisted.web2.dav.element.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
+from twisted.web2.dav.resource import DAVPropertyMixIn
+from twisted.web2.dav.util import joinURL
+from twisted.web2.http_headers import MimeType, generateContentType
+
+
+from twistedcaldav import customxml, carddavxml
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.config import config
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.ical import iCalendarProductID
+from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
+from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
+from twistedcaldav.static import CalDAVFile
+from twistedcaldav.vcard import Component, Property
+
+from xmlrpclib import datetime
+from vobject.vcard import Name, Address
+
+
+class OpenDirectoryBackingService(DirectoryService):
+    """
+    Open Directory implementation of L{IDirectoryService}.
+    """
+
+    baseGUID = "BF07A1A2-5BB5-4A4D-A59A-67260EA7E143"
+    
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.realmName, )
+
+    def __init__(self, params):
+        self._actuallyConfigure(**params)
+
+    def _actuallyConfigure(
+        self, queryPeopleRecords=True, 
+        peopleNode = "/Search/Contacts",
+        queryUserRecords=True, 
+        userNode = "/Search",
+        maxDSQueryRecords = 150,            # maximum number of records requested for any ds query
+        
+        queryDSLocal = False,              #query in DSLocal -- debug
+        dsLocalCacheTimeout = 30,
+        ignoreSystemRecords = True,
+        
+        liveQuery = True,                    # query directory service as needed
+        fakeETag = True,                # eTag is not reliable if True 
+        
+        cacheQuery = False,
+        cacheTimeout=30,                # cache timeout            
+        
+        addDSAttrXProperties=False,        # add dsattributes to vcards as "X-" attributes
+        standardizeSyntheticUIDs = False,  # use simple synthetic UIDs --- good for testing
+        appleInternalServer=False,
+        
+        additionalAttributes=[],
+        allowedAttributes=[],
+        directoryBackedAddressBook=None
+    ):
+        """
+        @queryPeopleRecords: C{True} to query for People records
+        @queryUserRecords: C{True} to query for User records
+        @maxDSQueryRecords: maximum number of (unfiltered) ds records retrieved before raising 
+            NumberOfMatchesWithinLimits exception or returning results
+        @dsLocalCacheTimeout: how log to keep cache of DSLocal records
+        @liveQuery: C{True} to query the directory as needed
+        @fakeETag: C{True} to use a fake eTag; allows ds queries with partial attributes
+        @cacheQuery: C{True} to query the directory and cache results
+        @cacheTimeout: if caching, the average cache timeout
+        @standardizeSyntheticUIDs: C{True} when creating synthetic UID (==f(Node, Type, Record Name)), 
+            use a standard Node name. This allows testing with the same UID on different hosts
+        @allowedAttributes: list of DSAttributes that are used to create VCards
+
+        """
+        assert directoryBackedAddressBook is not None
+        self.directoryBackedAddressBook = directoryBackedAddressBook
+
+        self.peopleDirectory = None
+        self.peopleNode = None
+        self.userDirectory = None
+        self.userNode = None
+        
+        self.realmName = None # needed for super
+        
+        if queryPeopleRecords or not queryUserRecords:
+            self.peopleNode = peopleNode
+            try:
+                self.peopleDirectory = opendirectory.odInit(peopleNode)
+            except opendirectory.ODError, e:
+                self.log_error("Open Directory (node=%s) Initialization error: %s" % (peopleNode, e))
+                raise
+            self.realmName = peopleNode
+
+        if queryUserRecords:
+            if self.peopleNode == userNode:          # use sane directory and node if they are equal
+                self.userNode = self.peopleNode
+                self.userDirectory = self.peopleDirectory
+            else:
+                self.userNode = userNode
+                try:
+                    self.userDirectory = opendirectory.odInit(userNode)
+                except opendirectory.ODError, e:
+                    self.log_error("Open Directory (node=%s) Initialization error: %s" % (userNode, e))
+                    raise
+                if self.realmName:
+                    self.realmName += "+" + userNode
+                else:
+                    self.realmName = userNode
+        
+        
+        self.maxDSQueryRecords = maxDSQueryRecords
+
+        self.ignoreSystemRecords = ignoreSystemRecords
+        self.queryDSLocal = queryDSLocal
+        self.dsLocalCacheTimeout = dsLocalCacheTimeout
+
+        self.liveQuery = liveQuery or not cacheQuery
+        self.fakeETag = fakeETag
+
+        self.cacheQuery = cacheQuery
+        
+        self.cacheTimeout = cacheTimeout if cacheTimeout > 0 else 30
+        
+        self.addDSAttrXProperties = addDSAttrXProperties
+        self.standardizeSyntheticUIDs = standardizeSyntheticUIDs
+        self.appleInternalServer = appleInternalServer
+        
+        self.additionalAttributes = additionalAttributes
+        # filter allows attributes, but make sure there are a minimum of attributes for functionality
+        if allowedAttributes:
+            self.allowedDSQueryAttributes = sorted(list(set(
+                                                [attr for attr in VCardRecord.allDSQueryAttributes
+                                                    if (isinstance(attr, str) and attr in allowedAttributes) or
+                                                       (isinstance(attr, tuple) and attr[0] in allowedAttributes)] +
+                                                VCardRecord.dsqueryAttributesForProperty.get("X-INTERNAL-REQUIRED")
+                                                )))
+            if (self.allowedDSQueryAttributes != VCardRecord.allDSQueryAttributes):
+                self.log_info("Allowed DS query attributes = %r" % (self.allowedDSQueryAttributes, ))
+        else:
+            self.allowedDSQueryAttributes = VCardRecord.allDSQueryAttributes
+        
+        #self.returnedAttributes = VCardRecord.allDSQueryAttributes
+        self.returnedAttributes = self.allowedDSQueryAttributes
+        
+            
+        
+        
+        self._dsLocalRecords = []
+        self._nextDSLocalQueryTime = 0
+        
+        # get this now once
+        hostname = getfqdn()
+        if hostname:
+            self.defaultNodeName = "/LDAPv3/" + hostname
+        else:
+            self.defaultNodeName = None
+        
+        #cleanup
+        self._cleanupTime = time.time()
+        
+        # file system locks
+        self._initLockPath = join(config.DocumentRoot, ".directory_address_book_create_lock")
+        self._createdLockPath = join(config.DocumentRoot, ".directory_address_book_created_lock")
+        self._updateLockPath = join(config.DocumentRoot, ".directory_address_book_update_lock")
+        self._tmpDirAddressBookLockPath = join(config.DocumentRoot, ".directory_address_book_tmpFolder_lock")
+        
+        self._updateLock = MemcacheLock("OpenDirectoryBacker", self._updateLockPath)
+        self._tmpDirAddressBookLock = MemcacheLock("OpenDirectoryBacker", self._tmpDirAddressBookLockPath)        
+                
+        # optimization so we don't have to always get create lock
+        self._triedCreateLock = False
+        self._created = False
+
+
+    def __cmp__(self, other):
+        if not isinstance(other, DirectoryRecord):
+            return super(DirectoryRecord, self).__eq__(other)
+
+        for attr in ("directory", "node"):
+            diff = cmp(getattr(self, attr), getattr(other, attr))
+            if diff != 0:
+                return diff
+        return 0
+
+    def __hash__(self):
+        h = hash(self.__class__)
+        for attr in ("directory", "node"):
+            h = (h + hash(getattr(self, attr))) & sys.maxint
+        return h
+    
+    @inlineCallbacks
+    def available(self):
+        if not self._triedCreateLock:
+            returnValue( False )
+        elif not self._created:
+            createdLock = MemcacheLock("OpenDirectoryBacker", self._createdLockPath)
+            self.log_debug("blocking on lock of: \"%s\")" % self._createdLockPath)
+            self._created = (yield createdLock.locked())
+        
+        returnValue(self._created)
+        
+    
+    def updateLock(self):
+        return self._updateLock
+
+    
+    @inlineCallbacks
+    def createCache(self):
+        """
+        If caching, create the cache for the first time.
+        """
+        
+        if not self.liveQuery:
+            self.log_info("loading directory address book")
+    
+            # get init lock
+            initLock = MemcacheLock("OpenDirectoryBacker", self._initLockPath, timeout=0)
+            self.log_debug("Attempt lock of: \"%s\")" % self._initLockPath)
+            gotCreateLock = False
+            try:
+                yield initLock.acquire()
+                gotCreateLock = True
+            except MemcacheLockTimeoutError:
+                pass
+                
+            self._triedCreateLock = True
+            
+            if gotCreateLock:
+                self.log_debug("Got lock!")
+                yield self._refreshCache( flushCache=False, creating=True )
+            else:
+                self.log_debug("Could not get lock - directory address book will be filled by peer")
+                        
+        
+
+    @inlineCallbacks
+    def _refreshCache(self, flushCache=False, creating=False, reschedule=True, query=None, attributes=None, keepLock=False, clear=False, maxRecords=0 ):
+        """
+        refresh the cache.
+        """
+
+        #print("_refreshCache:, flushCache=%s, creating=%s, reschedule=%s, query = %s" % (flushCache, creating, reschedule, "None" if query is None else query.generate(),))
+
+        def refreshLater():
+            #
+            # Add jitter/fuzz factor to avoid stampede for large OD query
+            #
+            cacheTimeout = min(self.cacheTimeout, 60) * 60
+            cacheTimeout = (cacheTimeout * random()) - (cacheTimeout / 2)
+            cacheTimeout += self.cacheTimeout * 60
+            reactor.callLater(cacheTimeout, self._refreshCache) #@UndefinedVariable
+            self.log_info("Refresh directory address book in %d minutes %d seconds" % divmod(cacheTimeout, 60))          
+
+        def cleanupLater():
+            
+            # try to cancel previous call if last clean up was less than 15 minutes ago
+            if (time.time() - self._cleanupTime) < 15*60:
+                try:
+                    self._lastCleanupCall.cancel()
+                except:
+                    pass
+            
+            #
+            # Add jitter/fuzz factor 
+            #
+            nom = 120
+            later = nom* (random() + .5)
+            self._lastCleanupCall = reactor.callLater(later, removeTmpAddressBooks) #@UndefinedVariable
+            self.log_info("Remove temporary directory address books in %d minutes %d seconds" % divmod(later, 60))          
+
+
+        def getTmpDirAndTmpFilePrefixSuffix():
+            # need to have temp file on same volumes as documents so that move works
+            absDocPath = abspath(config.DocumentRoot)
+            if absDocPath.startswith("/Volumes/"):
+                tmpDir = absDocPath
+                prefix = ".directoryAddressBook-"
+            else:
+                tmpDir = gettempdir()
+                prefix = "directoryAddressBook-"
+            
+            return (tmpDir, prefix, ".tmp")
+            
+        def makeTmpFilename():
+            tmpDir, prefix, suffix = getTmpDirAndTmpFilePrefixSuffix()
+            fd, fname = mkstemp(suffix=suffix, prefix=prefix, dir=tmpDir)
+            os.close(fd)
+            os.remove(fname)
+            return fname
+        
+        @inlineCallbacks
+        def removeTmpAddressBooks():
+            self.log_info("Checking for temporary directory address books")
+            tmpDir, prefix, suffix = getTmpDirAndTmpFilePrefixSuffix()
+
+            tmpDirLock = self._tmpDirAddressBookLock
+            self.log_debug("blocking on lock of: \"%s\")" % self._tmpDirAddressBookLockPath)
+            yield tmpDirLock.acquire()
+            
+            try:
+                for name in listdir(tmpDir):
+                    if name.startswith(prefix) and name.endswith(suffix):
+                        try:
+                            path = join(tmpDir, name)
+                            self.log_info("Deleting temporary directory address book at: %s" %    path)
+                            FilePath(path).remove()
+                            self.log_debug("Done deleting")
+                        except:
+                            self.log_info("Deletion failed")
+            finally:
+                self.log_debug("unlocking: \"%s\")" % self._tmpDirAddressBookLockPath)
+                yield tmpDirLock.release()
+            
+            self._cleanupTime = time.time()
+
+        
+        updateLock = None
+        limited = False
+        try:
+            
+            try:
+                # get the records
+                if clear:
+                    records = {}
+                else:
+                    records, limited = (yield self._getDirectoryRecords(query, attributes, maxRecords))
+                
+                # calculate the hash
+                # simple for now, could use MD5 digest if too many collisions     
+                newAddressBookCTag = customxml.GETCTag(str(hash(self.baseGUID + ":" + self.realmName + ":" + "".join(str(hash(records[key])) for key in records.keys()))))
+                
+                # get the old hash
+                oldAddressBookCTag = ""
+                updateLock = self.updateLock()
+                self.log_debug("blocking on lock of: \"%s\")" % self._updateLockPath)
+                yield updateLock.acquire()
+                
+                if not flushCache:
+                    # get update lock
+                    try:
+                        oldAddressBookCTag = self.directoryBackedAddressBook.readDeadProperty((calendarserver_namespace, "getctag"))
+                    except:
+                        oldAddressBookCTag = ""
+    
+                self.log_debug("Comparing {http://calendarserver.org/ns/}getctag: new = %s, old = %s" % (newAddressBookCTag, oldAddressBookCTag))
+                if str(newAddressBookCTag) != str(oldAddressBookCTag):
+                    
+                    self.log_debug("unlocking: \"%s\")" % self._updateLockPath)
+                    yield updateLock.release()
+                    updateLock = None
+                    
+                    tmpDirLock = self._tmpDirAddressBookLock
+                    self.log_debug("blocking on lock of: \"%s\")" % self._tmpDirAddressBookLockPath)
+                    yield tmpDirLock.acquire()
+                    
+                    try:
+                        self.log_info("Filling directory address book")
+                        startTime = time.time()
+                        newAddressBook = CalDAVFile(makeTmpFilename())
+                        yield newAddressBook.createAddressBookCollection()
+                        for key, record in records.items():
+                            try:
+                                vcard = record.vCard()
+                                # make up a destination 
+
+                                fileName = unquote(record.uriName())
+                                destination = CalDAVFile(join(newAddressBook.fp.path, fileName))
+                                destination_uri =  record.hRef()
+
+                                self.log_debug("Adding \"%s\", uri=\"%s\"" % (fileName, destination_uri, ))
+                                self.log_debug("VCard text =\n%s" % (record.vCardText(), ))
+                                
+                                yield StoreAddressObjectResource( request = None,
+                                                            sourceadbk = False,
+                                                            destinationadbk = True,
+                                                            destination = destination,
+                                                            destination_uri = destination_uri,
+                                                            destinationparent = newAddressBook,
+                                                            vcard = vcard,
+                                                            indexdestination = False,
+                                                            updatedestinationctag = False,
+                                                            ).run()
+                            except:
+                                self.log_info("Could not add record %s" % (record,))
+                                del records[key]
+                                newAddressBookCTag = customxml.GETCTag(str(hash(self.baseGUID + ":" + self.realmName + ":" + "".join(str(hash(records[key])) for key in records.keys()))))
+                        
+                        self.log_info("Indexing new directory address book")
+                        newAddressBook.index().recreate()
+                        elaspedTime = time.time()-startTime
+                        self.log_info("Timing: Fill address book: %.1f ms (%d vcards, %.2f vcards/sec)" % (elaspedTime*1000, len(records), len(records)/elaspedTime))
+                        
+                        updateLock = self.updateLock()
+                        self.log_debug("blocking on lock of: \"%s\")" % self._updateLockPath)
+                        yield updateLock.acquire()
+
+                        self.log_debug("Swapping in new directory address book")
+                        
+                        # move old address book out of the way
+                        if self.directoryBackedAddressBook.fp.exists():               
+                            os.rename(self.directoryBackedAddressBook.fp.path, makeTmpFilename())
+        
+                        #move new one into place
+                        os.rename(newAddressBook.fp.path, self.directoryBackedAddressBook.fp.path)
+                        self.directoryBackedAddressBook.fp.restat()
+                        
+                        self.directoryBackedAddressBook.writeDeadProperty(newAddressBookCTag)
+                        if hasattr(self.directoryBackedAddressBook, 'cacheNotifier'):
+                            yield self.directoryBackedAddressBook.cacheNotifier.changed()
+                        #else:
+                        #    self.log_info("%r does not have a cacheNotifier but the CTag changed"
+                        #                              % (self.directoryBackedAddressBook,))
+                    finally:
+                        self.log_debug("unlocking: \"%s\")" % self._tmpDirAddressBookLockPath)
+                        yield tmpDirLock.release()
+    
+                if not keepLock:
+                    self.log_debug("unlocking: \"%s\")" % self._updateLockPath)
+                    yield updateLock.release()
+                    updateLock = None
+                    
+            except:
+                cleanupLater()
+                if reschedule:
+                    refreshLater() 
+                raise
+            
+            if creating:
+                createdLock = MemcacheLock("OpenDirectoryBacker", self._createdLockPath)
+                self.log_debug("blocking on lock of: \"%s\")" % self._createdLockPath)
+                yield createdLock.acquire()
+            
+            cleanupLater()
+            if reschedule:
+                refreshLater() 
+        
+        except:
+            if updateLock:
+                yield updateLock.release()
+            raise
+
+        returnValue( (updateLock, limited) )
+
+
+
+    def _getDSLocalRecords(self):
+        
+        def generateDSLocalRecords():
+                        
+            records = {}
+            
+            recordTypes = [dsattributes.kDSStdRecordTypePeople, dsattributes.kDSStdRecordTypeUsers, ]
+            try:
+                localNodeDirectory = opendirectory.odInit("/Local/Default")
+                self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
+                        "/DSLocal",
+                        recordTypes,
+                        self.returnedAttributes,
+                    ))
+                results = opendirectory.listAllRecordsWithAttributes_list(
+                        localNodeDirectory,
+                        recordTypes,
+                        self.returnedAttributes,
+                    )
+            except opendirectory.ODError, ex:
+                self.log_error("Open Directory (node=%s) error: %s" % ("/Local/Default", str(ex)))
+                raise
+            
+            self._dsLocalRecords = []        
+            for (recordShortName, value) in results: #@UnusedVariable
+                
+                record = VCardRecord(self, value, "/Local/Default")
+
+                if self.ignoreSystemRecords:
+                    # remove system users and people
+                    if record.guid.startswith("FFFFEEEE-DDDD-CCCC-BBBB-AAAA"):
+                        self.log_info("Ignoring vcard for system record %s"  % (record,))
+                        continue
+
+                if record.guid in records:
+                    self.log_info("Record skipped due to conflict (duplicate uuid): %s" % (record,))
+                else:
+                    try:
+                        vCardText = record.vCardText()
+                    except:
+                        traceback.print_exc()
+                        self.log_info("Could not get vcard for record %s" % (record,))
+                    else:
+                        self.log_debug("VCard text =\n%s" % (vCardText, ))
+                        records[record.guid] = record                   
+    
+            return records
+        
+
+        if not self.liveQuery or not self.queryDSLocal:
+            return {}
+        
+        if time.time() > self._nextDSLocalQueryTime:
+            self._dsLocalRecords = generateDSLocalRecords()
+            # Add jitter/fuzz factor 
+            self._nextDSLocalQueryTime = time.time() + self.dsLocalCacheTimeout * (random() + 0.5)  * 60
+
+        return self._dsLocalRecords
+    
+
+    @inlineCallbacks
+    def _getDirectoryRecords(self, query=None, attributes=None, maxRecords=0 ):
+        """
+        Get a list of filtered VCardRecord for the given query with the given attributes.
+        query == None gets all records. attribute == None gets VCardRecord.allDSQueryAttributes
+        """
+        limited = False
+        queryResults = (yield self._queryDirectory(query, attributes, maxRecords ))
+        if maxRecords and len(queryResults) >= maxRecords:
+            limited = True
+            self.log_debug("Directory address book record limit (= %d) reached." % (maxRecords, ))
+
+        self.log_debug("Query done. Inspecting %s results" % len(queryResults))
+
+        records = self._getDSLocalRecords().copy()
+        self.log_debug("Adding %s DSLocal results" % len(records.keys()))
+        
+        for (recordShortName, value) in queryResults: #@UnusedVariable
+            
+            record = VCardRecord(self, value, self.defaultNodeName)
+
+            if self.ignoreSystemRecords:
+                # remove system users and people
+                if record.guid.startswith("FFFFEEEE-DDDD-CCCC-BBBB-AAAA"):
+                    self.log_info("Ignoring vcard for system record %s"  % (record,))
+                    continue
+        
+            if record.guid in records:
+                self.log_info("Ignoring vcard for record due to conflict (duplicate uuid): %s" % (record,))
+            else:
+                records[record.guid] = record                   
+        
+        self.log_debug("After filtering, %s records (limited=%s)." % (len(records), limited))
+        returnValue((records, limited, ))
+
+
+    @inlineCallbacks
+    def _queryDirectory(self, query=None, attributes=None, maxRecords=0 ):
+        
+        startTime = time.time()
+
+        
+        if not attributes:
+            attributes = self.returnedAttributes
+            
+        attributes = list(set(attributes + self.additionalAttributes)) # remove duplicates
+        
+        directoryAndRecordTypes = []
+        if self.peopleDirectory == self.userDirectory:
+            # use single ds query if possible for best performance
+            directoryAndRecordTypes.append( (self.peopleDirectory, self.peopleNode, (dsattributes.kDSStdRecordTypePeople, dsattributes.kDSStdRecordTypeUsers) ) )
+        else:
+            if self.peopleDirectory:
+                directoryAndRecordTypes.append( (self.peopleDirectory, self.peopleNode, dsattributes.kDSStdRecordTypePeople) )
+            if self.userDirectory:
+                directoryAndRecordTypes.append( (self.userDirectory, self.userNode, dsattributes.kDSStdRecordTypeUsers) )
+        
+        allResults = []
+        for directory, node, recordType in directoryAndRecordTypes:
+            try:
+                if query:
+                    if isinstance(query, match) and query.value is not "":
+                        self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r,%r)" % (
+                            node,
+                            query.attribute,
+                            query.value,
+                            query.matchType,
+                            False,
+                            recordType,
+                            attributes,
+                            maxRecords,
+                        ))
+                        results = (yield deferToThread(
+                            opendirectory.queryRecordsWithAttribute_list,
+                            directory,
+                            query.attribute,
+                            query.value,
+                            query.matchType,
+                            False,
+                            recordType,
+                            attributes,
+                            maxRecords,
+                        ))
+                    else:
+                        self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r)" % (
+                            node,
+                            query.generate(),
+                            False,
+                            recordType,
+                            attributes,
+                            maxRecords,
+                        ))
+                        results = (yield deferToThread(
+                            opendirectory.queryRecordsWithAttributes_list,
+                            directory,
+                            query.generate(),
+                            False,
+                            recordType,
+                            attributes,
+                            maxRecords,
+                        ))
+                else:
+                    self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r,%r)" % (
+                        node,
+                        recordType,
+                        attributes,
+                        maxRecords,
+                    ))
+                    results = (yield deferToThread(
+                        opendirectory.listAllRecordsWithAttributes_list,
+                        directory,
+                        recordType,
+                        attributes,
+                        maxRecords,
+                    ))
+            except opendirectory.ODError, ex:
+                self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+                raise
+            
+            allResults.extend(results)
+            
+            if maxRecords:
+                maxRecords -= len(results)
+                if maxRecords <= 0:
+                    break
+        
+
+        elaspedTime = time.time()-startTime
+        self.log_info("Timing: Directory query: %.1f ms (%d records, %.2f records/sec)" % (elaspedTime*1000, len(allResults), len(allResults)/elaspedTime))
+        returnValue( allResults )
+    
+    def _getDSFilter(self, addressBookFilter):
+        """
+        Convert the supplied addressbook-query into an expression tree.
+    
+        @param filter: the L{Filter} for the addressbook-query to convert.
+        @return: (needsAllRecords, espressionAttributes, expression) tuple
+        """
+        def propFilterListQuery(filterAllOf, propFilters):
+
+            def propFilterExpression(filterAllOf, propFilter):
+                #print("propFilterExpression")
+                """
+                Create an expression for a single prop-filter element.
+                
+                @param propFilter: the L{PropertyFilter} element.
+                @return: (needsAllRecords, espressionAttributes, expressions) tuple
+                """
+                
+                def definedExpression( defined, allOf, filterName, constant, queryAttributes, allAttrStrings):
+                    if constant or filterName in ("N" , "FN", "UID", ):
+                        return (defined, [], [])     # all records have this property so no records do not have it
+                    else:
+                        matchList = list(set([match(attrName, "", dsattributes.eDSStartsWith) for attrName in allAttrStrings]))
+                        if defined:
+                            return andOrExpression(allOf, queryAttributes, matchList)
+                        else:
+                            if len(matchList) > 1:
+                                expr = expression( expression.OR, matchList )
+                            else:
+                                expr = matchList
+                            return (False, queryAttributes, [expression( expression.NOT, expr),])
+                    #end isNotDefinedExpression()
+    
+    
+                def andOrExpression(propFilterAllOf, queryAttributes, matchList):
+                    #print("andOrExpression(propFilterAllOf=%r, queryAttributes%r, matchList%r)" % (propFilterAllOf, queryAttributes, matchList))
+                    if propFilterAllOf and len(matchList):
+                        # add OR expression because parent will AND
+                        return (False, queryAttributes, [expression( expression.OR, matchList),])
+                    else:
+                        return (False, queryAttributes, matchList)
+                    #end andOrExpression()
+                    
+    
+                # short circuit parameter filters
+                def supportedParamter( filterName, paramFilters, propFilterAllOf ):
+                    
+                    def supported( paramFilterName, paramFilterDefined, params ):
+                        paramFilterName = paramFilterName.upper()
+                        if len(params.keys()) and ((paramFilterName in params.keys()) != paramFilterDefined):
+                            return False
+                        if len(params[paramFilterName]) and str(paramFilter.qualifier).upper() not in params[paramFilterName]:
+                            return False
+                        return True 
+                        #end supported()
+                
+                    
+                    oneSupported = False
+                    for paramFilter in paramFilters:
+                        if filterName == "PHOTO":
+                            if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "ENCODING": ["B",], "TYPE": ["JPEG",], }):
+                                return not propFilterAllOf
+                            oneSupported |= propFilterAllOf
+                        elif filterName == "ADR":
+                            if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": ["WORK", "PREF", "POSTAL", "PARCEL",], }):
+                                return not propFilterAllOf
+                            oneSupported |= propFilterAllOf
+                        elif filterName == "LABEL":
+                            if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": ["POSTAL", "PARCEL",]}):
+                                return not propFilterAllOf
+                            oneSupported |= propFilterAllOf
+                        elif filterName == "TEL":
+                            if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": [], }): # has params derived from ds attributes
+                                return not propFilterAllOf
+                            oneSupported |= propFilterAllOf
+                        elif filterName == "EMAIL":
+                            if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": [], }): # has params derived from ds attributes
+                                return not propFilterAllOf
+                            oneSupported |= propFilterAllOf
+                        elif filterName == "URL":
+                            if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, {}):
+                                return not propFilterAllOf
+                            oneSupported |= propFilterAllOf
+                        elif filterName == "KEY":
+                            if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "ENCODING": ["B",], "TYPE": ["PGPPUBILICKEY", "USERCERTIFICATE", "USERPKCS12DATA", "USERSMIMECERTIFICATE",] }):
+                                return not propFilterAllOf
+                            oneSupported |= propFilterAllOf
+                        elif not filterName.startswith("X-"): #X- IMHandles X-ABRELATEDNAMES excepted, no other params are used
+                            if propFilterAllOf == paramFilter.defined:
+                                return not propFilterAllOf
+                            oneSupported |= propFilterAllOf
+                    
+                    if propFilterAllOf:
+                        return True
+                    else:
+                        return oneSupported
+                    #end supportedParamter()
+                    
+                    
+                def textMatchElementExpression( propFilterAllOf, textMatchElement ):
+    
+                    # pre process text match strings for ds query 
+                    def getMatchStrings( propFilter, matchString ):
+                                            
+                        if propFilter.filter_name in ("REV" , "BDAY", ):
+                            rawString = matchString
+                            matchString = ""
+                            for c in rawString:
+                                if not c in "TZ-:":
+                                    matchString += c
+                        elif propFilter.filter_name == "GEO":
+                            matchString = ",".join(matchString.split(";"))
+                        
+                        if propFilter.filter_name in ("N" , "ADR", "ORG", ):
+                            # for structured properties, change into multiple strings for ds query
+                            if propFilter.filter_name == "ADR":
+                                #split by newline and comma
+                                rawStrings = ",".join( matchString.split("\n") ).split(",")
+                            else:
+                                #split by space
+                                rawStrings = matchString.split(" ")
+                            
+                            # remove empty strings
+                            matchStrings = []
+                            for oneString in rawStrings:
+                                if len(oneString):
+                                    matchStrings += [oneString,]
+                            return matchStrings
+                        
+                        elif len(matchString):
+                            return [matchString,]
+                        else:
+                            return []
+                        # end getMatchStrings
+    
+                    if constant:
+                        # do the match right now!  Return either all or none.
+                        return( textMatchElement.match([constant,]), [], [] )
+                    else:
+
+                        matchStrings = getMatchStrings(propFilter, str(textMatchElement))
+
+                        if not len(matchStrings) or binaryAttrStrs:
+                            # no searching text in binary ds attributes, so change to defined/not defined case
+                            if textMatchElement.negate:
+                                return definedExpression(False, propFilterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
+                            # else fall through to attribute exists case below
+                        else:
+                            
+                            # special case UID's formed from node and record name
+                            if propFilter.filter_name == "UID":
+                                matchString = matchStrings[0]
+                                seperatorIndex = matchString.find(VCardRecord.peopleUIDSeparator)
+                                if seperatorIndex > 1:
+                                    recordNameStart = seperatorIndex + len(VCardRecord.peopleUIDSeparator)
+                                else:
+                                    seperatorIndex = matchString.find(VCardRecord.userUIDSeparator)                        
+                                    if seperatorIndex > 1:
+                                        recordNameStart = seperatorIndex + len(VCardRecord.userUIDSeparator)
+                                    else:
+                                        recordNameStart = sys.maxint
+        
+                                if recordNameStart < len(matchString)-1:
+                                    try:
+                                        recordNameQualifier = matchString[recordNameStart:].decode("base64").decode("utf8")
+                                    except Exception, e:
+                                        self.log_debug("Could not decode UID string %r in %r: %r" % (matchString[recordNameStart:], matchString, e,))
+                                    else:
+                                        if textMatchElement.negate:
+                                            return (False, queryAttributes, 
+                                                    [expression(expression.NOT, match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact)),]
+                                                    )
+                                        else:
+                                            return (False, queryAttributes, 
+                                                    [match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact),]
+                                                    )
+                            
+                            # use match_type where possible depending on property/attribute mapping
+                            # Note that case sensitive negate will not work
+                            #        Should return all records in that case
+                            matchType = dsattributes.eDSContains
+                            if propFilter.filter_name in ("NICKNAME" , "TITLE" , "NOTE" , "UID", "URL", "N", "ADR", "ORG", "REV",  "LABEL", ):
+                                if textMatchElement.match_type == "equals":
+                                        matchType = dsattributes.eDSExact
+                                elif textMatchElement.match_type == "starts-with":
+                                        matchType = dsattributes.eDSStartsWith
+                                elif textMatchElement.match_type == "ends-with":
+                                        matchType = dsattributes.eDSEndsWith
+                            
+                            matchList = []
+                            for matchString in matchStrings:
+                                matchList += [match(attrName, matchString, matchType) for attrName in stringAttrStrs]
+                            
+                            matchList = list(set(matchList))
+    
+                            if textMatchElement.negate:
+                                if len(matchList) > 1:
+                                    expr = expression( expression.OR, matchList )
+                                else:
+                                    expr = matchList
+                                return (False, queryAttributes, [expression( expression.NOT, expr),])
+                            else:
+                                return andOrExpression(propFilterAllOf, queryAttributes, matchList)
+    
+                    # attribute exists search
+                    return definedExpression(True, propFilterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
+                    #end textMatchElementExpression()
+                    
+    
+                # get attribute strings from dsqueryAttributesForProperty list 
+                queryAttributes = list(set(VCardRecord.dsqueryAttributesForProperty.get(propFilter.filter_name, [])).intersection(set(self.allowedDSQueryAttributes)))
+                
+                binaryAttrStrs = []
+                stringAttrStrs = []
+                for attr in queryAttributes:
+                    if isinstance(attr, tuple):
+                        binaryAttrStrs.append(attr[0])
+                    else:
+                        stringAttrStrs.append(attr)
+                allAttrStrings = stringAttrStrs + binaryAttrStrs
+                                        
+                constant = VCardRecord.constantProperties.get(propFilter.filter_name)
+                if not constant and not allAttrStrings: 
+                    return (False, [], [])
+                
+                if propFilter.qualifier and isinstance(propFilter.qualifier, carddavxml.IsNotDefined):
+                    return definedExpression(False, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
+                
+                paramFilterElements = [paramFilterElement for paramFilterElement in propFilter.filters if isinstance(paramFilterElement, carddavxml.ParameterFilter)]
+                textMatchElements = [textMatchElement for textMatchElement in propFilter.filters if isinstance(textMatchElement, carddavxml.TextMatch)]
+                propFilterAllOf = propFilter.propfilter_test == "allof"
+                
+                # handle parameter filter elements
+                if len(paramFilterElements) > 0:
+                    if supportedParamter(propFilter.filter_name, paramFilterElements, propFilterAllOf ):
+                        if len(textMatchElements) == 0:
+                            return definedExpression(True, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
+                    else:
+                        if propFilterAllOf:
+                            return (False, [], [])
+                
+                # handle text match elements
+                propFilterNeedsAllRecords = propFilterAllOf
+                propFilterAttributes = []
+                propFilterExpressionList = []
+                for textMatchElement in textMatchElements:
+                    
+                    textMatchNeedsAllRecords, textMatchExpressionAttributes, textMatchExpression = textMatchElementExpression(propFilterAllOf, textMatchElement)
+                    if propFilterAllOf:
+                        propFilterNeedsAllRecords &= textMatchNeedsAllRecords
+                    else:
+                        propFilterNeedsAllRecords |= textMatchNeedsAllRecords
+                    propFilterAttributes += textMatchExpressionAttributes
+                    propFilterExpressionList += textMatchExpression
+    
+
+                if (len(propFilterExpressionList) > 1) and (filterAllOf != propFilterAllOf):
+                    propFilterExpressions = [expression(expression.AND if propFilterAllOf else expression.OR , list(set(propFilterExpressionList)))] # remove duplicates
+                else:
+                    propFilterExpressions = list(set(propFilterExpressionList))
+                
+                return (propFilterNeedsAllRecords, propFilterAttributes, propFilterExpressions)
+                #end propFilterExpression
+
+            #print("propFilterListQuery: filterAllOf=%r, propFilters=%r" % (filterAllOf, propFilters,))
+            """
+            Create an expression for a list of prop-filter elements.
+            
+            @param filterAllOf: the C{True} if parent filter test is "allof"
+            @param propFilters: the C{list} of L{ComponentFilter} elements.
+            @return: (needsAllRecords, espressionAttributes, expression) tuple
+            """
+            needsAllRecords = filterAllOf
+            attributes = []
+            expressions = []
+            for propFilter in propFilters:
+                
+                propNeedsAllRecords, propExpressionAttributes, propExpression = propFilterExpression(filterAllOf, propFilter)
+                if filterAllOf:
+                    needsAllRecords &= propNeedsAllRecords
+                else:
+                    needsAllRecords |= propNeedsAllRecords
+                attributes += propExpressionAttributes
+                expressions += propExpression
+
+            if len(expressions) > 1:
+                expr = expression(expression.AND if filterAllOf else expression.OR , list(set(expressions))) # remove duplicates
+            elif len(expressions):
+                expr = expressions[0]
+            else:
+                expr = None
+            
+            return (needsAllRecords, attributes, expr)
+        
+                
+        #print("_getDSFilter")
+        # Lets assume we have a valid filter from the outset
+        
+        # Top-level filter contains zero or more prop-filters
+        if addressBookFilter:
+            filterAllOf = addressBookFilter.filter_test == "allof"
+            if len(addressBookFilter.children) > 0:
+                return propFilterListQuery(filterAllOf, addressBookFilter.children)
+            else:
+                return (filterAllOf, [], [])
+        else:
+            return (False, [], [])    
+    
+                        
+
+    def _attributesForAddressBookQuery(self, addressBookQuery ):
+                        
+        propertyNames = [] 
+        #print( "addressBookQuery.qname=%r" % addressBookQuery.qname)
+        if addressBookQuery.qname() == ("DAV:", "prop"):
+        
+            for property in addressBookQuery.children:                
+                #print("property = %r" % property )
+                if isinstance(property, carddavxml.AddressData):
+                    for addressProperty in property.children:
+                        #print("addressProperty = %r" % addressProperty )
+                        if isinstance(addressProperty, carddavxml.Property):
+                            #print("Adding property %r", addressProperty.attributes["name"])
+                            propertyNames.append(addressProperty.attributes["name"])
+                        
+                elif not self.fakeETag and property.qname() == ("DAV:", "getetag"):
+                    # for a real etag == md5(vCard), we need all attributes
+                    propertyNames = None
+                    break;
+                            
+        
+        if not len(propertyNames):
+            #print("using all attributes")
+            return self.returnedAttributes
+        
+        else:
+            propertyNames.append("X-INTERNAL-MINIMUM-VCARD-PROPERTIES") # these properties are required to make a vCard
+            queryAttributes = []
+            for prop in propertyNames:
+                if prop in VCardRecord.dsqueryAttributesForProperty:
+                    #print("adding attributes %r" % VCardRecord.dsqueryAttributesForProperty.get(prop))
+                    queryAttributes += VCardRecord.dsqueryAttributesForProperty.get(prop)
+
+            return list(set(queryAttributes).intersection(set(self.returnedAttributes)))
+
+    
+    @inlineCallbacks
+    def cacheVCardsForAddressBookQuery(self, addressBookFilter, addressBookQuery, maxResults ):
+        """
+        Cache the vCards for a given addressBookFilder and addressBookQuery
+        """
+        startTime = time.time()
+        #print("Timing: cacheVCardsForAddressBookQuery.starttime=%f" % startTime)
+        
+    
+        allRecords, filterAttributes, dsFilter  = self._getDSFilter( addressBookFilter );
+        #print("allRecords = %s, query = %s" % (allRecords, "None" if dsFilter is None else dsFilter.generate(),))
+    
+        if allRecords:
+            dsFilter = None #  None expression == all Records
+        clear = not allRecords and not dsFilter
+        
+        #get unique list of requested attributes
+        if clear:
+            attributes = None
+        else:
+            queryAttributes = self._attributesForAddressBookQuery( addressBookQuery )
+            attributes = filterAttributes + queryAttributes
+        
+        #calc maxRecords from passed in maxResults allowing extra for second stage filtering in caller
+        maxRecords = int(maxResults * 1.2)
+        if self.maxDSQueryRecords and maxRecords > self.maxDSQueryRecords:
+            maxRecords = self.maxDSQueryRecords
+            
+        updateLock, limited = (yield self._refreshCache(reschedule=False, query=dsFilter, attributes=attributes, keepLock=True, clear=clear, maxRecords=maxRecords ))
+
+        elaspedTime = time.time()-startTime
+        self.log_info("Timing: Cache fill: %.1f ms" % (elaspedTime*1000,))
+        
+
+        returnValue((updateLock, limited))
+
+
+    @inlineCallbacks
+    def vCardRecordsForAddressBookQuery(self, addressBookFilter, addressBookQuery, maxResults ):
+        """
+        Get vCards for a given addressBookFilder and addressBookQuery
+        """
+    
+        allRecords, filterAttributes, dsFilter  = self._getDSFilter( addressBookFilter );
+        #print("allRecords = %s, query = %s" % (allRecords, "None" if dsFilter is None else dsFilter.generate(),))
+        
+        # testing:
+        # allRecords = True
+        
+        if allRecords:
+            dsFilter = None #  None expression == all Records
+        clear = not allRecords and not dsFilter
+        
+        queryRecords = []
+        limited = False
+
+        if not clear:
+            queryAttributes = self._attributesForAddressBookQuery( addressBookQuery )
+            attributes = filterAttributes + queryAttributes
+            
+            #calc maxRecords from passed in maxResults allowing extra for second stage filtering in caller
+            maxRecords = int(maxResults * 1.2)
+            if self.maxDSQueryRecords and maxRecords > self.maxDSQueryRecords:
+                maxRecords = self.maxDSQueryRecords
+
+            records, limited = (yield self._getDirectoryRecords(dsFilter, attributes, maxRecords))
+            
+            #filter out bad records --- should only happen during development
+            for record in records.values():
+                try:
+                    vCardText = record.vCardText()
+                except:
+                    traceback.print_exc()
+                    self.log_info("Could not get vcard for record %s" % (record,))
+                else:
+                    if not record.firstValueForAttribute(dsattributes.kDSNAttrMetaNodeLocation).startswith("/Local"):
+                        self.log_debug("VCard text =\n%s" % (vCardText, ))
+                    queryRecords.append(record)
+                        
+        returnValue((queryRecords, limited,))        
+
+
+class VCardRecord(DirectoryRecord, DAVPropertyMixIn):
+    """
+    Open Directory implementation of L{IDirectoryRecord}.
+    """
+
+    # od attributes that may contribute to vcard properties
+    # will be used to translate vCard queries to od queries
+
+    dsqueryAttributesForProperty = {
+                             
+        "FN" : [
+               dsattributes.kDS1AttrFirstName, 
+               dsattributes.kDS1AttrLastName, 
+               dsattributes.kDS1AttrMiddleName,
+               dsattributes.kDSNAttrNamePrefix,
+               dsattributes.kDSNAttrNameSuffix,
+               dsattributes.kDS1AttrDistinguishedName,
+               dsattributes.kDSNAttrRecordName,
+               ],
+        "N" : [
+               dsattributes.kDS1AttrFirstName, 
+               dsattributes.kDS1AttrLastName, 
+               dsattributes.kDS1AttrMiddleName,
+               dsattributes.kDSNAttrNamePrefix,
+               dsattributes.kDSNAttrNameSuffix,
+               dsattributes.kDS1AttrDistinguishedName,
+               dsattributes.kDSNAttrRecordName,
+               ],
+        "NICKNAME" : [
+                dsattributes.kDSNAttrNickName,
+                ],
+        # no binary searching
+        "PHOTO" : [
+                (dsattributes.kDSNAttrJPEGPhoto, "base64"),
+                ],
+        "BDAY" : [
+                dsattributes.kDS1AttrBirthday,
+                ],
+        "ADR" : [
+                dsattributes.kDSNAttrBuilding,
+                dsattributes.kDSNAttrStreet,
+                dsattributes.kDSNAttrCity,
+                dsattributes.kDSNAttrState,
+                dsattributes.kDSNAttrPostalCode,
+                dsattributes.kDSNAttrCountry,
+                ],
+        "LABEL" : [
+                dsattributes.kDSNAttrPostalAddress,
+                dsattributes.kDSNAttrPostalAddressContacts,
+                dsattributes.kDSNAttrAddressLine1,
+                dsattributes.kDSNAttrAddressLine2,
+                dsattributes.kDSNAttrAddressLine3,
+                ],
+         "TEL" : [
+                dsattributes.kDSNAttrPhoneNumber,
+                dsattributes.kDSNAttrMobileNumber,
+                dsattributes.kDSNAttrPagerNumber,
+                dsattributes.kDSNAttrHomePhoneNumber,
+                dsattributes.kDSNAttrPhoneContacts,
+                dsattributes.kDSNAttrFaxNumber,
+                #dsattributes.kDSNAttrAreaCode,
+                ],
+         "EMAIL" : [
+                dsattributes.kDSNAttrEMailAddress,
+                dsattributes.kDSNAttrEMailContacts,
+                ],
+         "GEO" : [
+                dsattributes.kDSNAttrMapCoordinates,
+                ],
+         "TITLE" : [
+                dsattributes.kDSNAttrJobTitle,
+                ],
+         "ORG" : [
+                dsattributes.kDSNAttrCompany,
+                dsattributes.kDSNAttrOrganizationName,
+                dsattributes.kDSNAttrDepartment,
+                ],
+         "NOTE" : [
+                dsattributes.kDS1AttrComment,
+                dsattributes.kDS1AttrNote,
+                ],
+         "REV" : [
+                dsattributes.kDS1AttrModificationTimestamp,
+                ],
+         "UID" : [
+                dsattributes.kDS1AttrGeneratedUID,
+                # special cased
+                #dsattributes.kDSNAttrMetaNodeLocation,
+                #dsattributes.kDSNAttrRecordName,
+                #dsattributes.kDS1AttrDistinguishedName,
+                ],
+         "URL" : [
+                dsattributes.kDS1AttrWeblogURI,
+                dsattributes.kDSNAttrURL,
+                ],
+         "KEY" : [
+                # check on format, are these all binary?
+                (dsattributes.kDSNAttrPGPPublicKey, "base64"),
+                (dsattributes.kDS1AttrUserCertificate, "base64"),
+                (dsattributes.kDS1AttrUserPKCS12Data, "base64"),
+                (dsattributes.kDS1AttrUserSMIMECertificate, "base64"),
+                ],
+         # too bad this is not one X-Attribute with params.     Would make searching easier
+         "X-AIM" : [
+                dsattributes.kDSNAttrIMHandle,
+                ],
+         "X-JABBER" :    [
+                dsattributes.kDSNAttrIMHandle,
+                ],
+         "X-MSN" :    [
+                dsattributes.kDSNAttrIMHandle,
+                ],
+         "X-YAHOO" :  [
+                dsattributes.kDSNAttrIMHandle,
+                ],
+         "X-ICQ" :    [
+                dsattributes.kDSNAttrIMHandle,
+                ],
+         "X-ABRELATEDNAMES" :  [
+                dsattributes.kDSNAttrRelationships,
+                ],
+          "X-INTERNAL-MINIMUM-VCARD-PROPERTIES" : [
+                dsattributes.kDS1AttrGeneratedUID,
+                dsattributes.kDSNAttrMetaNodeLocation,
+                dsattributes.kDS1AttrFirstName, 
+                 dsattributes.kDS1AttrLastName, 
+                dsattributes.kDS1AttrMiddleName,
+                   dsattributes.kDSNAttrNamePrefix,
+                  dsattributes.kDSNAttrNameSuffix,
+                 dsattributes.kDS1AttrDistinguishedName,
+                dsattributes.kDSNAttrRecordName,
+                dsattributes.kDSNAttrRecordType,
+                dsattributes.kDS1AttrModificationTimestamp,
+                dsattributes.kDS1AttrCreationTimestamp,
+                ],
+          "X-INTERNAL-REQUIRED" : [
+                dsattributes.kDS1AttrGeneratedUID,
+                dsattributes.kDSNAttrMetaNodeLocation,
+                 dsattributes.kDS1AttrDistinguishedName,
+                dsattributes.kDSNAttrRecordName,
+                dsattributes.kDS1AttrFirstName, 
+                 dsattributes.kDS1AttrLastName, 
+                dsattributes.kDSNAttrRecordType,
+                ],
+  
+    }
+    
+
+    allDSQueryAttributes = sorted(list(set([attr for lookupAttributes in dsqueryAttributesForProperty.values()
+                                      for attr in lookupAttributes])))
+
+    binaryDSAttributeStrs = [attr[0] for attr in allDSQueryAttributes
+                                if isinstance(attr, tuple) ]
+
+    stringDSAttributeStrs = [attr for attr in allDSQueryAttributes
+                                if isinstance(attr, str) ]
+
+    allDSAttributeStrs = stringDSAttributeStrs + binaryDSAttributeStrs
+    
+    #peopleUIDSeparator = "-" + OpenDirectoryBackingService.baseGUID + "-"
+    userUIDSeparator = "-bf07a1a2-"
+    peopleUIDSeparator = "-cf07a1a2-"
+
+    
+    constantProperties = {
+        # 3.6.3 PRODID Type Definition
+        # should put version in but twistedcaldav.__version__, is NONE
+        # "PRODID": iCalendarProductID + "//BUILD %s" % twistedcaldav.__version__,
+        "PRODID": iCalendarProductID,
+        # 3.6.9 VERSION Type Definition
+        "VERSION": "3.0",
+        # 3.7.1 CLASS Type Definition
+        #"CLASS": "PUBLIC" if config.AnonymousDirectoryAddressBookAccess else "CONFIDENTIAL",
+        }
+
+    
+    def __init__(self, service, recordAttributes, defaultNodeName=None):
+        
+
+        self.log_debug("service=%s, attributes=%s"    % (service, recordAttributes))
+
+        #save off for debugging
+        if service.addDSAttrXProperties:
+            self.originalAttributes = recordAttributes.copy()
+
+        self.directoryBackedAddressBook = service.directoryBackedAddressBook
+        self._vCard = None
+        self._vCardText = None
+        self._uriName = None
+        self._hRef = None
+        
+        self.attributes = {}
+        for key, values in recordAttributes.items():
+            if key in VCardRecord.stringDSAttributeStrs:
+                if isinstance(values, list):
+                    self.attributes[key] = [removeControlChars(val).decode("utf8") for val in values]
+                else:
+                    self.attributes[key] = removeControlChars(values).decode("utf8")
+            else:
+                self.attributes[key] = values
+                                                        
+        # fill in  missing essential attributes used for filtering
+        fullName = self.firstValueForAttribute(dsattributes.kDS1AttrDistinguishedName)
+        if not fullName:
+            fullName = self.firstValueForAttribute(dsattributes.kDSNAttrRecordName)
+            self.attributes[dsattributes.kDS1AttrDistinguishedName] = fullName
+            
+        node = self.firstValueForAttribute(dsattributes.kDSNAttrMetaNodeLocation)
+        
+        # use a better node name -- makes better synthetic GUIDS
+        if not node or node == "/LDAPv3/127.0.0.1":
+            node = defaultNodeName if defaultNodeName else service.realmName
+            self.attributes[dsattributes.kDSNAttrMetaNodeLocation] = node
+        
+        guid = self.firstValueForAttribute(dsattributes.kDS1AttrGeneratedUID)
+        if not guid:
+            if service.standardizeSyntheticUIDs:
+                nodeUUIDStr = "00000000"
+            else:
+                nodeUUIDStr = "%x" % abs(hash(node))
+            nameUUIDStr = "".join(self.firstValueForAttribute(dsattributes.kDSNAttrRecordName).encode("utf8").encode("base64").split("\n"))
+            if self.firstValueForAttribute(dsattributes.kDSNAttrRecordType) != dsattributes.kDSStdRecordTypePeople:
+                guid =  VCardRecord.userUIDSeparator.join([nodeUUIDStr, nameUUIDStr,])
+            else:
+                guid =  VCardRecord.peopleUIDSeparator.join([nodeUUIDStr, nameUUIDStr,])
+
+            
+        # since guid is used as file name, normalize so uid uniqueness == fine name uniqueness
+        #guid = "/".join(guid.split(":")).upper()
+        self.attributes[dsattributes.kDS1AttrGeneratedUID] = guid
+        
+        if self.firstValueForAttribute(dsattributes.kDS1AttrLastName) == "99":
+            del self.attributes[dsattributes.kDS1AttrLastName]
+        
+        if self.firstValueForAttribute(dsattributes.kDSNAttrRecordType) != dsattributes.kDSStdRecordTypePeople:
+            recordType = DirectoryService.recordType_users
+        else:
+            recordType = DirectoryService.recordType_people
+                        
+        super(VCardRecord, self).__init__(
+            service                  = service,
+            recordType              = recordType,
+            guid                  = guid,
+            shortNames              = tuple(self.valuesForAttribute(dsattributes.kDSNAttrRecordName)),
+            fullName              = fullName,
+            firstName              = self.firstValueForAttribute(dsattributes.kDS1AttrFirstName, None),
+            lastName              = self.firstValueForAttribute(dsattributes.kDS1AttrLastName, None),
+            emailAddresses        = (),
+            calendarUserAddresses = (),
+            autoSchedule          = False,
+            enabledForCalendaring = False,
+        )
+        
+
+
+    def __repr__(self):
+        return "<%s[%s(%s)] %s(%s) %r>" % (
+            self.__class__.__name__,
+            self.firstValueForAttribute(dsattributes.kDSNAttrRecordType),
+            self.firstValueForAttribute(dsattributes.kDSNAttrMetaNodeLocation),
+            self.guid,
+            self.shortNames,
+            self.fullName
+        )
+    
+    def __hash__(self):
+        s = "".join([
+              "%s:%s" % (attribute, self.valuesForAttribute(attribute),)
+              for attribute in self.attributes
+              ])
+        return hash(s)
+
+    """
+    def nextFileName(self):
+        self.renameCounter += 1
+        self.fileName = self.baseFileName + "-" + str(self.renameCounter)
+        self.fileNameLower = self.fileName.lower()
+    """
+    
+    def hasAttribute(self, attributeName ):
+        return self.valuesForAttribute(attributeName, None) is not None
+
+
+    def valuesForAttribute(self, attributeName, default_values=[] ):
+        values = self.attributes.get(attributeName)
+        if (values is None):
+            return default_values
+        elif not isinstance(values, list):
+            values = [values, ] 
+        
+        # ds templates often return empty attribute values
+        #     get rid of them here
+        nonEmptyValues = [value for value in values if len(value) > 0 ]
+        
+        if len(nonEmptyValues) > 0:
+            return nonEmptyValues
+        else:
+            return default_values
+        
+
+    def firstValueForAttribute(self, attributeName, default_value="" ):
+        values = self.attributes.get(attributeName)
+        if values is None:
+            return default_value
+        elif isinstance(values, list):
+            return values[0]
+        else:
+            return values
+
+    def joinedValuesForAttribute(self, attributeName, separator=",", default_string="" ):
+        values = self.valuesForAttribute(attributeName, None)
+        if not values:
+            return default_string
+        else:
+            return separator.join(values)
+            
+
+    def isoDateStringForDateAttribute(self, attributeName, default_string="" ):
+        modDate = self.firstValueForAttribute(attributeName, default_string)
+        revDate = None
+        if modDate:
+            if len(modDate) >= len("YYYYMMDD") and modDate[:8].isdigit():
+                revDate = "%s-%s-%s" % (modDate[:4],modDate[4:6],modDate[6:8], )
+            if len(modDate) >= len("YYYYMMDDHHMMSS") and modDate[8:14].isdigit():
+                revDate += "T%s:%s:%sZ" % (modDate[8:10],modDate[10:12],modDate[12:14], )
+        return revDate
+
+    
+                
+    def vCard(self):
+        
+        
+        def generateVCard():
+            
+            def equalDictWithFilter( dict1, dict2, ignoreDict ):
+                def filteredDict(dict, ignoreDict):
+
+                    if ignoreDict:                
+                        for ignoreDictKey, ignoreDictValues in ignoreDict.items():
+                            dictKeyValues = dict[ignoreDictKey]
+                            if dictKeyValues:
+                                for ignoreDictValue in ignoreDictValues:
+                                    while ignoreDictValue in dictKeyValues:
+                                        # copy dictionary and remove value from copy
+                                        dictKeyValues = list(dictKeyValues)
+                                        dictKeyValues.remove(ignoreDictValue)
+                                        dict = dict.copy()
+                                        dict[ignoreDictKey] = dictKeyValues
+
+                    return dict
+                
+                return filteredDict(dict1, ignoreDict) == filteredDict(dict2, ignoreDict) 
+
+
+            def isUniqueProperty(vcard, newProperty, ignoreParams = None):
+                existingProperties = vcard.properties( newProperty.name() )
+                for existingProperty in existingProperties:
+                    if existingProperty.value() == newProperty.value():
+                        if equalDictWithFilter( existingProperty.params(), newProperty.params(), ignoreParams):
+                            return False
+                return True
+
+            def addUniqueProperty(vcard, newProperty, ignoreParams = None, attrType = None, attrValue = None):
+                if isUniqueProperty(vcard, newProperty, ignoreParams):
+                    vcard.addProperty(newProperty)
+                else:
+                    if attrType and attrValue:
+                        self.log_info("Ignoring attribute %r with value %r in creating property %r. A duplicate property already exists." % (attrType, attrValue, newProperty, ))
+                            
+            def addPropertyAndLabel(groupCount, label, propertyName, propertyValue, parameters = None ):
+                groupCount[0] += 1
+                groupPrefix = "item%d" % groupCount[0]
+                vcard.addProperty(Property(propertyName, propertyValue, params = parameters, group=groupPrefix))
+                vcard.addProperty(Property("X-ABLabel", label, group=groupPrefix))
+
+            # for attributes of the form  param:value
+            def addPropertiesAndLabelsForPrefixedAttribute(groupCount, propertyPrefix, propertyName, defaultLabel, nolabelParamTypes, labelMap, attrType):
+                preferred = True
+                for attrValue in self.valuesForAttribute(attrType):
+                    try:
+                        # special case for Apple
+                        if self.service.appleInternalServer and attrType == dsattributes.kDSNAttrIMHandle:
+                            splitValue = attrValue.split("|")
+                            if len (splitValue) > 1:
+                                attrValue = splitValue[0]
+
+                        colonIndex = attrValue.find(":")
+                        if (colonIndex > len(attrValue)-2):
+                            raise ValueError("Nothing after colon.")
+
+                        propertyValue = attrValue[colonIndex+1:]
+                        labelString = attrValue[:colonIndex] if colonIndex > 0 else defaultLabel
+                        paramTypeString = labelString.upper()
+                        
+                        # add PREF to first prop's parameters
+                        paramTypeStrings = [paramTypeString,]
+                        if preferred and "PREF" != paramTypeString:
+                            paramTypeStrings += ["PREF",]
+                        parameters = { "TYPE": paramTypeStrings, }
+
+                        #special case for IMHandles which the param is the last part of the property like X-AIM or X-JABBER 
+                        if propertyPrefix:
+                            propertyName = propertyPrefix + paramTypeString
+
+                        # only add label prop if needed
+                        if paramTypeString in nolabelParamTypes:
+                            addUniqueProperty(vcard, Property(propertyName, attrValue[colonIndex+1:], params = parameters), None, attrValue, attrType)
+                        else:
+                            # use special localizable addressbook labels where possible
+                            abLabelString = labelMap.get(labelString, labelString)
+                            addPropertyAndLabel( groupCount, abLabelString, propertyName, propertyValue, parameters)
+                        preferred = False
+
+                    except Exception, e:
+                        traceback.print_exc()
+                        self.log_debug("addPropertiesAndLabelsForPrefixedAttribute(): groupCount=%r, propertyPrefix=%r, propertyName=%r, nolabelParamTypes=%r, labelMap=%r, attrType=%r" % (groupCount[0], propertyPrefix, propertyName, nolabelParamTypes, labelMap, attrType,))
+                        self.log_error("addPropertiesAndLabelsForPrefixedAttribute(): Trouble parsing attribute %s, with value \"%s\".  Error = %s" % (attrType, attrValue, e,))
+
+            
+            #print("VCardRecord.vCard")
+            # create vCard
+            vcard = Component("VCARD")
+            groupCount = [0]
+            
+            # add constant properties - properties that are the same regardless of the record attributes
+            for key, value in VCardRecord.constantProperties.items():
+                vcard.addProperty(Property(key, value))
+    
+            # 3.1 IDENTIFICATION TYPES http://tools.ietf.org/html/rfc2426#section-3.1
+            # 3.1.1 FN Type Definition
+            # dsattributes.kDS1AttrDistinguishedName,      # Users distinguished or real name
+            #
+            # full name is required but this is set in OpenDiretoryBackingRecord.__init__
+            #vcard.addProperty(Property("FN", self.firstValueForAttribute(dsattributes.kDS1AttrDistinguishedName)))
+            
+            # 3.1.2 N Type Definition
+            # dsattributes.kDS1AttrFirstName,            # Used for first name of user or person record.
+            # dsattributes.kDS1AttrLastName,            # Used for the last name of user or person record.
+            # dsattributes.kDS1AttrMiddleName,            # Used for the middle name of user or person record.
+            # dsattributes.kDSNAttrNameSuffix,            # Represents the name suffix of a user or person.
+                                                        #      ie. Jr., Sr., etc.
+                                                        #      Usually found in user or people records (kDSStdRecordTypeUsers or 
+                                                        #      dsattributes.kDSStdRecordTypePeople).
+            # dsattributes.kDSNAttrNamePrefix,            # Represents the title prefix of a user or person.
+                                                        #      ie. Mr., Ms., Mrs., Dr., etc.
+                                                        #      Usually found in user or people records (kDSStdRecordTypeUsers or 
+                                                        #      dsattributes.kDSStdRecordTypePeople).
+                                                        
+            # name is required, so make sure we have one
+            # vobject.vcard says: Each name attribute can be a string or a list of strings.
+            if not self.hasAttribute(dsattributes.kDS1AttrFirstName) and not self.hasAttribute(dsattributes.kDS1AttrLastName):
+                familyName = self.firstValueForAttribute(dsattributes.kDS1AttrDistinguishedName)
+            else:
+                familyName = self.valuesForAttribute(dsattributes.kDS1AttrLastName, "")
+            
+            NameObject = Name(family = familyName, 
+                                                  given = self.valuesForAttribute(dsattributes.kDS1AttrFirstName, ""),
+                                                  additional = self.valuesForAttribute(dsattributes.kDS1AttrMiddleName, ""),
+                                                  prefix = self.valuesForAttribute(dsattributes.kDSNAttrNamePrefix, ""),
+                                                  suffix = self.valuesForAttribute(dsattributes.kDSNAttrNameSuffix, ""),
+                                                  )
+            vcard.addProperty(Property("N", NameObject ))
+            
+            # set full name to Name with contiguous spaces stripped
+            # it turns out that Address Book.app ignores FN and creates it fresh from N in ABRecord
+            # so no reason to have FN distinct from N
+            fullName = str(NameObject).strip()
+            while fullName.find("  ") > 0:
+                fullName = " ".join(fullName.split("  "))
+            vcard.addProperty(Property("FN", fullName ))
+            
+            # 3.1.3 NICKNAME Type Definition
+            # dsattributes.kDSNAttrNickName,            # Represents the nickname of a user or person.
+                                                        #    Usually found in user or people records (kDSStdRecordTypeUsers or 
+                                                        #    dsattributes.kDSStdRecordTypePeople).
+            for nickname in self.valuesForAttribute(dsattributes.kDSNAttrNickName):
+                addUniqueProperty(vcard, Property("NICKNAME", nickname), None, dsattributes.kDSNAttrNickName, nickname)
+            
+            # 3.1.4 PHOTO Type Definition
+            # dsattributes.kDSNAttrJPEGPhoto,            # Used to store binary picture data in JPEG format. 
+                                                        #      Usually found in user, people or group records (kDSStdRecordTypeUsers, 
+                                                        #      dsattributes.kDSStdRecordTypePeople,dsattributes.kDSStdRecordTypeGroups).
+            # pyOpenDirectory always returns binary-encoded string                                       
+                                                        
+            for photo in self.valuesForAttribute(dsattributes.kDSNAttrJPEGPhoto):
+                addUniqueProperty(vcard, Property("PHOTO", photo, params = { "ENCODING": ["b",], "TYPE": ["JPEG",], }, encoded = True), None, dsattributes.kDSNAttrJPEGPhoto, photo)
+    
+    
+            # 3.1.5 BDAY Type Definition
+            # dsattributes.kDS1AttrBirthday,            # Single-valued attribute that defines the user's birthday.
+                                                        #      Format is x.208 standard YYYYMMDDHHMMSSZ which we will require as GMT time.
+                                                        #                               012345678901234
+            
+            birthdate = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrBirthday)
+            if birthdate:
+                vcard.addProperty(Property("BDAY", birthdate))
+    
+    
+            # 3.2 Delivery Addressing Types http://tools.ietf.org/html/rfc2426#section-3.2
+            #
+            # 3.2.1 ADR Type Definition
+    
+            #address
+            # vobject.vcard says: Each address attribute can be a string or a list of strings.
+            extended = self.valuesForAttribute(dsattributes.kDSNAttrBuilding, "")
+            street = self.valuesForAttribute(dsattributes.kDSNAttrStreet, "")
+            city = self.valuesForAttribute(dsattributes.kDSNAttrCity, "")
+            region = self.valuesForAttribute(dsattributes.kDSNAttrState, "")
+            code = self.valuesForAttribute(dsattributes.kDSNAttrPostalCode, "")
+            country = self.valuesForAttribute(dsattributes.kDSNAttrCountry, "")
+            
+            if len(extended) > 0 or len(street) > 0 or len(city) > 0 or len(region) > 0 or len(code) > 0 or len(country) > 0:
+                vcard.addProperty(Property("ADR", Address(
+                                                           #box = box,
+                                                           extended = extended,
+                                                           street = street,
+                                                           city = city,
+                                                           region = region,
+                                                           code = code,
+                                                           country = country,
+                                                           ),
+                                                           params = { "TYPE": ["WORK", "PREF", "POSTAL", "PARCEL",], }))
+    
+    
+            # 3.2.2 LABEL Type Definition
+            
+            # dsattributes.kDSNAttrPostalAddress,            # The postal address usually excluding postal code.
+            # dsattributes.kDSNAttrPostalAddressContacts,    # multi-valued attribute that defines a record's alternate postal addresses .
+                                                            #      found in user records (kDSStdRecordTypeUsers) and resource records (kDSStdRecordTypeResources).
+            # dsattributes.kDSNAttrAddressLine1,            # Line one of multiple lines of address data for a user.
+            # dsattributes.kDSNAttrAddressLine2,            # Line two of multiple lines of address data for a user.
+            # dsattributes.kDSNAttrAddressLine3,            # Line three of multiple lines of address data for a user.
+            
+            for label in self.valuesForAttribute(dsattributes.kDSNAttrPostalAddress):
+                addUniqueProperty(vcard, Property("LABEL", label, params = { "TYPE": ["POSTAL", "PARCEL",]}), None, dsattributes.kDSNAttrPostalAddress, label)
+                
+            for label in self.valuesForAttribute(dsattributes.kDSNAttrPostalAddressContacts):
+                addUniqueProperty(vcard, Property("LABEL", label, params = { "TYPE": ["POSTAL", "PARCEL",]}), None, dsattributes.kDSNAttrPostalAddressContacts, label)
+                
+            address = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine1)
+            addressLine2 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine2)
+            if len(addressLine2) > 0:
+                address += "\n" + addressLine2
+            addressLine3 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine3)
+            if len(addressLine3) > 0:
+                address += "\n" + addressLine3
+            
+            if len(address) > 0:
+                vcard.addProperty(Property("LABEL", address, params = { "TYPE": ["POSTAL", "PARCEL",]}))
+    
+            # 3.3 TELECOMMUNICATIONS ADDRESSING TYPES http://tools.ietf.org/html/rfc2426#section-3.3
+            # 3.3.1 TEL Type Definition
+            #          TEL;TYPE=work,voice,pref,msg:+1-213-555-1234
+    
+            # dsattributes.kDSNAttrPhoneNumber,            # Telephone number of a user.
+            # dsattributes.kDSNAttrMobileNumber,        # Represents the mobile numbers of a user or person.
+                                                        #      Usually found in user or people records (kDSStdRecordTypeUsers or 
+                                                        #      dsattributes.kDSStdRecordTypePeople).
+            # dsattributes.kDSNAttrFaxNumber,            # Represents the FAX numbers of a user or person.
+                                                        # Usually found in user or people records (kDSStdRecordTypeUsers or 
+                                                        # kDSStdRecordTypePeople).
+            # dsattributes.kDSNAttrPagerNumber,            # Represents the pager numbers of a user or person.
+                                                        #      Usually found in user or people records (kDSStdRecordTypeUsers or 
+                                                        #      dsattributes.kDSStdRecordTypePeople).
+            # dsattributes.kDSNAttrHomePhoneNumber,        # Home telephone number of a user or person.
+            # dsattributes.kDSNAttrPhoneContacts,        # multi-valued attribute that defines a record's custom phone numbers .
+                                                        #      found in user records (kDSStdRecordTypeUsers). 
+                                                        #      Example: home fax:408-555-4444
+            
+            params = { "TYPE": ["WORK", "PREF", "VOICE",], }
+            for phone in self.valuesForAttribute(dsattributes.kDSNAttrPhoneNumber):
+                addUniqueProperty(vcard, Property("TEL", phone, params=params), {"TYPE": ["PREF"]}, phone, dsattributes.kDSNAttrPhoneNumber)
+                params = { "TYPE": ["WORK", "VOICE",], }
+    
+            params = { "TYPE": ["WORK", "PREF", "CELL",], }
+            for phone in self.valuesForAttribute(dsattributes.kDSNAttrMobileNumber):
+                addUniqueProperty(vcard, Property("TEL", phone, params=params), {"TYPE": ["PREF"]}, phone, dsattributes.kDSNAttrMobileNumber)
+                params = { "TYPE": ["WORK", "CELL",], }
+    
+            params = { "TYPE": ["WORK", "PREF", "FAX",], }
+            for phone in self.valuesForAttribute(dsattributes.kDSNAttrFaxNumber):
+                addUniqueProperty(vcard, Property("TEL", phone, params=params), {"TYPE": ["PREF"]}, phone, dsattributes.kDSNAttrFaxNumber)
+                params = { "TYPE": ["WORK", "FAX",], }
+    
+            params = { "TYPE": ["WORK", "PREF", "PAGER",], }
+            for phone in self.valuesForAttribute(dsattributes.kDSNAttrPagerNumber):
+                addUniqueProperty(vcard, Property("TEL", phone, params=params), {"TYPE": ["PREF"]}, phone, dsattributes.kDSNAttrPagerNumber)
+                params = { "TYPE": ["WORK", "PAGER",], }
+    
+            params = { "TYPE": ["HOME", "PREF", "VOICE",], }
+            for phone in self.valuesForAttribute(dsattributes.kDSNAttrHomePhoneNumber):
+                addUniqueProperty(vcard, Property("TEL", phone, params=params), {"TYPE": ["PREF"]}, phone, dsattributes.kDSNAttrHomePhoneNumber)
+                params = { "TYPE": ["HOME", "VOICE",], }
+                    
+            addPropertiesAndLabelsForPrefixedAttribute(groupCount, None, "TEL", "work",
+                                                        ["VOICE", "CELL", "FAX", "PAGER",], {},
+                                                        dsattributes.kDSNAttrPhoneContacts, )
+
+            """
+            # EXTEND:  Use this attribute
+            # dsattributes.kDSNAttrAreaCode,            # Area code of a user's phone number.
+            """
+    
+            # 3.3.2 EMAIL Type Definition
+            # dsattributes.kDSNAttrEMailAddress,        # Email address of usually a user record.
+    
+            # setup some params
+            preferredWorkParams = { "TYPE": ["WORK", "PREF", "INTERNET",], }
+            workParams = { "TYPE": ["WORK", "INTERNET",], }
+            params = preferredWorkParams
+            for emailAddress in self.valuesForAttribute(dsattributes.kDSNAttrEMailAddress):
+                addUniqueProperty(vcard, Property("EMAIL", emailAddress, params=params), {"TYPE": ["PREF"]}, emailAddress, dsattributes.kDSNAttrEMailAddress)
+                params = workParams
+                
+            # dsattributes.kDSNAttrEMailContacts,        # multi-valued attribute that defines a record's custom email addresses .
+                                                        #    found in user records (kDSStdRecordTypeUsers). 
+                                                        #      Example: home:johndoe at mymail.com
+    
+            # check to see if parameters type are open ended. Could be any string
+            addPropertiesAndLabelsForPrefixedAttribute(groupCount, None, "EMAIL", "work",
+                                                        ["WORK", "HOME",], {}, 
+                                                        dsattributes.kDSNAttrEMailContacts, )
+    
+            """
+            # UNIMPLEMENTED:
+            # 3.3.3 MAILER Type Definition
+            """
+            # 3.4 GEOGRAPHICAL TYPES http://tools.ietf.org/html/rfc2426#section-3.4
+            """
+            # UNIMPLEMENTED:
+            # 3.4.1 TZ Type Definition
+            """
+            # 3.4.2 GEO Type Definition
+            #dsattributes.kDSNAttrMapCoordinates,        # attribute that defines coordinates for a user's location .
+                                                        #      Found in user records (kDSStdRecordTypeUsers) and resource records (kDSStdRecordTypeResources).
+                                                        #      Example: 7.7,10.6
+            for coordinate in self.valuesForAttribute(dsattributes.kDSNAttrMapCoordinates):
+                parts = coordinate.split(",")
+                if (len(parts) == 2):
+                    vcard.addProperty(Property("GEO", parts ))
+                else:
+                    self.log_info("Ignoring malformed attribute %r with value %r. Well-formed example: 7.7,10.6." % (dsattributes.kDSNAttrMapCoordinates, coordinate))
+            #
+            # 3.5 ORGANIZATIONAL TYPES http://tools.ietf.org/html/rfc2426#section-3.5
+            #
+            # 3.5.1 TITLE Type Definition
+            for jobTitle in self.valuesForAttribute(dsattributes.kDSNAttrJobTitle):
+                addUniqueProperty(vcard, Property("TITLE", jobTitle), None, dsattributes.kDSNAttrJobTitle, jobTitle)
+    
+            """
+            # UNIMPLEMENTED:
+            # 3.5.2 ROLE Type Definition
+            # 3.5.3 LOGO Type Definition
+            # 3.5.4 AGENT Type Definition
+            """
+            # 3.5.5 ORG Type Definition
+            company = self.joinedValuesForAttribute(dsattributes.kDSNAttrCompany)
+            if len(company) == 0:
+                company = self.joinedValuesForAttribute(dsattributes.kDSNAttrOrganizationName)
+            department = self.joinedValuesForAttribute(dsattributes.kDSNAttrDepartment)
+            extra = self.joinedValuesForAttribute(dsattributes.kDSNAttrOrganizationInfo)
+            if len(company) > 0 or len(department) > 0:
+                vcard.addProperty(Property("ORG", (company, department, extra, ),))
+            
+            # 3.6 EXPLANATORY TYPES http://tools.ietf.org/html/rfc2426#section-3.6
+            """
+            # UNIMPLEMENTED:
+            # 3.6.1 CATEGORIES Type Definition
+            """
+            # 3.6.2 NOTE Type Definition
+            # dsattributes.kDS1AttrComment,                  # Attribute used for unformatted comment.
+            # dsattributes.kDS1AttrNote,                  # Note attribute. Commonly used in printer records.
+            for comment in self.valuesForAttribute(dsattributes.kDS1AttrComment):
+                addUniqueProperty(vcard, Property("NOTE", comment), None, dsattributes.kDS1AttrComment, comment)
+    
+            for note in self.valuesForAttribute(dsattributes.kDS1AttrNote):
+                addUniqueProperty(vcard, Property("NOTE", note), None, dsattributes.kDS1AttrNote, note)
+            
+            # 3.6.3 PRODID Type Definition
+            #vcard.addProperty(Property("PRODID", iCalendarProductID + "//BUILD %s" % twistedcaldav.__version__))
+            #vcard.addProperty(Property("PRODID", iCalendarProductID))
+            # ADDED WITH CONTSTANT PROPERTIES
+            
+            # 3.6.4 REV Type Definition
+            revDate = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrModificationTimestamp)
+            if revDate:
+                vcard.addProperty(Property("REV", revDate))
+            
+            """
+            # UNIMPLEMENTED:
+            # 3.6.5 SORT-STRING Type Definition
+            # 3.6.6 SOUND Type Definition
+            """
+            # 3.6.7 UID Type Definition
+            # dsattributes.kDS1AttrGeneratedUID,        # Used for 36 character (128 bit) unique ID. Usually found in user, 
+                                                        #      group, and computer records. An example value is "A579E95E-CDFE-4EBC-B7E7-F2158562170F".
+                                                        #      The standard format contains 32 hex characters and four hyphen characters.
+            # !! don't use self.guid which is URL encoded
+            vcard.addProperty(Property("UID", self.firstValueForAttribute(dsattributes.kDS1AttrGeneratedUID)))
+    
+            # 3.6.8 URL Type Definition 
+            # dsattributes.kDSNAttrURL,                    # List of URLs.
+            # dsattributes.kDS1AttrWeblogURI,            # Single-valued attribute that defines the URI of a user's weblog.
+                                                        #     Usually found in user records (kDSStdRecordTypeUsers). 
+                                                        #      Example: http://example.com/blog/jsmith
+            for url in self.valuesForAttribute(dsattributes.kDS1AttrWeblogURI):
+                addPropertyAndLabel( groupCount, "weblog", "URL", url, parameters = { "TYPE": ["Weblog",] } )
+    
+            for url in self.valuesForAttribute(dsattributes.kDSNAttrURL):
+                addPropertyAndLabel( groupCount, "_$!<HomePage>!$_", "URL", url, parameters = { "TYPE": ["Homepage",] } )
+    
+    
+            # 3.6.9 VERSION Type Definition
+            # ALREADY ADDED
+    
+            # 3.7 SECURITY TYPES http://tools.ietf.org/html/rfc2426#section-3.7
+            # 3.7.1 CLASS Type Definition
+            # ALREADY ADDED
+            
+            # 3.7.2 KEY Type Definition
+            
+            # dsattributes.kDSNAttrPGPPublicKey,        # Pretty Good Privacy public encryption key.
+            # dsattributes.kDS1AttrUserCertificate,        # Attribute containing the binary of the user's certificate.
+                                                        #       Usually found in user records. The certificate is data which identifies a user.
+                                                        #       This data is attested to by a known party, and can be independently verified 
+                                                        #       by a third party.
+            # dsattributes.kDS1AttrUserPKCS12Data,        # Attribute containing binary data in PKCS #12 format. 
+                                                        #       Usually found in user records. The value can contain keys, certificates,
+                                                        #      and other related information and is encrypted with a passphrase.
+            # dsattributes.kDS1AttrUserSMIMECertificate,# Attribute containing the binary of the user's SMIME certificate.
+                                                        #       Usually found in user records. The certificate is data which identifies a user.
+                                                        #       This data is attested to by a known party, and can be independently verified 
+                                                        #       by a third party. SMIME certificates are often used for signed or encrypted
+                                                        #       emails.
+    
+            for key in self.valuesForAttribute(dsattributes.kDSNAttrPGPPublicKey):
+                addUniqueProperty(vcard, Property("KEY", key, params = { "ENCODING": ["b",], "TYPE": ["PGPPublicKey",] }, encoded=True), None, dsattributes.kDSNAttrPGPPublicKey, key)
+    
+            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserCertificate):
+                addUniqueProperty(vcard, Property("KEY", key, params = { "ENCODING": ["b",], "TYPE": ["UserCertificate",] }, encoded=True), None, dsattributes.kDS1AttrUserCertificate, key)
+    
+            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserPKCS12Data):
+                addUniqueProperty(vcard, Property("KEY", key, params = { "ENCODING": ["b",], "TYPE": ["UserPKCS12Data",] }, encoded=True), None, dsattributes.kDS1AttrUserPKCS12Data, key)
+    
+            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserSMIMECertificate):
+                addUniqueProperty(vcard, Property("KEY", key, params = { "ENCODING": ["b",], "TYPE": ["UserSMIMECertificate",] }), None, dsattributes.kDS1AttrUserSMIMECertificate, key)
+    
+            """
+            X- attributes, Address Book support
+            """
+            # X-AIM, X-JABBER, X-MSN, X-YAHOO, X-ICQ
+            # instant messaging
+            # dsattributes.kDSNAttrIMHandle,            # Represents the Instant Messaging handles of a user.
+                                                        #      Values should be prefixed with the appropriate IM type
+                                                        #       ie. AIM:, Jabber:, MSN:, Yahoo:, or ICQ:
+                                                        #       Usually found in user records (kDSStdRecordTypeUsers).
+    
+            addPropertiesAndLabelsForPrefixedAttribute(groupCount, "X-", None, "aim",
+                                                        ["AIM", "JABBER", "MSN", "YAHOO", "ICQ"], 
+                                                        {}, 
+                                                        dsattributes.kDSNAttrIMHandle,)
+                    
+            # X-ABRELATEDNAMES
+            # dsattributes.kDSNAttrRelationships,        #      multi-valued attribute that defines the relationship to the record type .
+                                                        #      found in user records (kDSStdRecordTypeUsers). 
+                                                        #      Example: brother:John
+            addPropertiesAndLabelsForPrefixedAttribute(groupCount, None, "X-ABRELATEDNAMES", "friend",
+                                                        [],  
+                                                        {   "FATHER":"_$!<Father>!$_",
+                                                            "MOTHER":"_$!<Mother>!$_",
+                                                            "PARENT":"_$!<Parent>!$_",
+                                                            "BROTHER":"_$!<Brother>!$_",
+                                                            "SISTER":"_$!<Sister>!$_",
+                                                            "CHILD":"_$!<Child>!$_",
+                                                            "FRIEND":"_$!<Friend>!$_",
+                                                            "SPOUSE":"_$!<Spouse>!$_",
+                                                            "PARTNER":"_$!<Partner>!$_",
+                                                            "ASSISTANT":"_$!<Assistant>!$_",
+                                                            "MANAGER":"_$!<Manager>!$_", },
+                                                        dsattributes.kDSNAttrRelationships, )
+            
+            
+            # special case for Apple
+            if self.service.appleInternalServer:
+                for manager in self.valuesForAttribute("dsAttrTypeNative:appleManager"):
+                    splitManager = manager.split("|")
+                    if len(splitManager) >= 4:
+                        managerValue = "%s %s, %s" % (splitManager[0], splitManager[1], splitManager[3],)
+                    elif len(splitManager) >= 2:
+                        managerValue = "%s %s" % (splitManager[0], splitManager[1])
+                    else:
+                        managerValue = manager
+                    addPropertyAndLabel( groupCount, "_$!<Manager>!$_", "X-ABRELATEDNAMES", managerValue, parameters = { "TYPE": ["Manager",] } )
+            
+            """
+            # UNIMPLEMENTED: X- attributes
+            
+            X-MAIDENNAME
+            X-PHONETIC-FIRST-NAME
+            X-PHONETIC-MIDDLE-NAME
+            X-PHONETIC-LAST-NAME
+        
+            sattributes.kDS1AttrPicture,                # Represents the path of the picture for each user displayed in the login window.
+                                                        #      Found in user records (kDSStdRecordTypeUsers).
+           
+            dsattributes.kDS1AttrMapGUID,                # Represents the GUID for a record's map.
+            dsattributes.kDSNAttrMapURI,                # attribute that defines the URI of a user's location.
+    
+            dsattributes.kDSNAttrOrganizationInfo,        # Usually the organization info of a user.
+            dsattributes.kDSNAttrAreaCode,                # Area code of a user's phone number.
+    
+            dsattributes.kDSNAttrMIME,                    # Data contained in this attribute type is a fully qualified MIME Type. 
+            
+            """
+            
+            # debug, create x attributes for all ds attributes
+            if self.service.addDSAttrXProperties:
+                for attribute in self.originalAttributes:
+                    for value in self.valuesForAttribute(attribute):
+                        vcard.addProperty(Property("X-"+"-".join(attribute.split(":")), removeControlChars(value)))
+    
+            return vcard
+
+        
+        if not self._vCard:
+            self._vCard = generateVCard()
+        
+        return self._vCard
+    
+    def vCardText(self):
+        if not self._vCardText:
+            self._vCardText = str(self.vCard())
+        
+        return self._vCardText
+
+    def uriName(self):
+        if not self._uriName:
+            self._uriName = self.vCard().getProperty("UID").value() + ".vcf"
+        #print("uriName():self._uriName=%s" % self._uriName)
+        return self._uriName
+        
+    
+    def hRef(self, parentURI="/directory/"):
+        if not self._hRef:
+            self._hRef = davxml.HRef.fromString(joinURL(parentURI, self.uriName()))
+            
+        return self._hRef
+
+
+    def readProperty(self, property, request):
+        
+        if type(property) is tuple:
+            qname = property
+        else:
+            qname = property.qname()
+
+        namespace, name = qname
+        
+        #print("VCardResource.readProperty: qname = %s" % (qname, ))
+        
+        if namespace == dav_namespace:
+            if name == "resourcetype":
+                result = davxml.ResourceType.empty #@UndefinedVariable
+                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
+                return result
+            elif name == "getetag":
+                result = davxml.GETETag( hashlib.md5(self.vCardText()).hexdigest() )
+                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
+                return result
+            elif name == "getcontenttype":
+                mimeType = MimeType('text', 'vcard', {})
+                result = davxml.GETContentType(generateContentType(mimeType))
+                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
+                return result
+            elif name == "getcontentlength":
+                result = davxml.GETContentLength.fromString(str(len(self.vCardText())))
+                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
+                return result
+            elif name == "getlastmodified":
+                if self.vCard().hasProperty("REV"):
+                    modDatetime = parse_date(self.vCard().propertyValue("REV"))
+                else:
+                    # use creation date attribute if it exists
+                    creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
+                    if creationDateString:
+                        modDatetime = parse_date(creationDateString)
+                    else:
+                        modDatetime = datetime.datetime.utcnow()
+
+                #strip time zone because time zones are unimplemented in davxml.GETLastModified.fromDate
+                d = modDatetime.date()
+                t = modDatetime.time()
+                modDatetimeNoTZ = datetime.datetime(d.year, d.month, d.day, t.hour, t.minute, t.second, t.microsecond, None)
+                result = davxml.GETLastModified.fromDate(modDatetimeNoTZ)
+                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
+                return result
+            elif name == "creationdate":
+                creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
+                if creationDateString:
+                    creationDatetime = parse_date(creationDateString)
+                elif self.vCard().hasProperty("REV"):    # use modification date property if it exists
+                    creationDatetime = parse_date(self.vCard().propertyValue("REV"))
+                else:
+                    creationDatetime = datetime.datetime.utcnow()
+                result = davxml.CreationDate.fromDate(creationDatetime)
+                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
+                return result
+            elif name == "displayname":
+                # AddressBook.app uses N. Use FN or UID instead?
+                result = davxml.DisplayName.fromString(self.vCard().propertyValue("N"))
+                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
+                return result
+
+        elif namespace == twisted_dav_namespace:
+            return super(VCardRecord, self).readProperty(property, request)
+            #return DAVPropertyMixIn.readProperty(self, property, request)
+
+        return self.directoryBackedAddressBook.readProperty(property, request)
+
+    def listProperties(self, request):
+        #print("VCardResource.listProperties()")
+        qnames = set(self.liveProperties)
+
+        # Add dynamic live properties that exist
+        dynamicLiveProperties = (
+            (dav_namespace, "quota-available-bytes"     ),
+            (dav_namespace, "quota-used-bytes"          ),
+        )
+        for dqname in dynamicLiveProperties:
+            #print("VCardResource.listProperties: removing dqname=%s" % (dqname,))
+            qnames.remove(dqname)
+
+        for qname in self.deadProperties().list():
+            if (qname not in qnames) and (qname[0] != twisted_private_namespace):
+                #print("listProperties: adding qname=%s" % (qname,))
+                qnames.add(qname)
+
+        #for qn in qnames: print("VCardResource.listProperties: qn=%s" % (qn,))
+
+        yield qnames
+        
+    listProperties = deferredGenerator(listProperties)
+    
+# utility
+#remove control characters because vCard does not support them
+def removeControlChars( utf8String ):
+    result = utf8String
+    for a in utf8String:
+        if '\x00' <= a <= '\x1F':
+            result = ""
+            for c in utf8String:
+                if '\x00' <= c <= '\x1F':
+                    pass 
+                else:
+                    result += c
+    #if utf8String != result: print ("changed %r to %r" % (utf8String, result))
+    return result
+
+

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -48,6 +48,8 @@
 from twisted.web2.dav.util import joinURL
 from twisted.web2.dav.noneprops import NonePropertyStore
 
+from twistedcaldav import carddavxml
+
 from twext.log import Logger
 
 from twistedcaldav.authkerb import NegotiateCredentials
@@ -457,7 +459,9 @@
 
         if record in self._principalResourceCache:
             return self._principalResourceCache[record]
-        if record.enabledForCalendaring:
+        if record.enabledForCalendaring or record.enabledForAddressBooks:
+            # XXX these are different features and one should not automatically
+            # imply the other...
             principal = DirectoryCalendarPrincipalResource(self, record)
         else:
             principal = DirectoryPrincipalResource(self, record)
@@ -839,6 +843,7 @@
         return "".join((
             """\nCalendar homes:\n"""          , format_list(format_link(u) for u in self.calendarHomeURLs()),
             """\nCalendar user addresses:\n""" , format_list(format_link(a) for a in self.calendarUserAddresses()),
+            """\nAddress Book homes:\n"""       , format_list(format_link(u) for u in self.addressBookHomeURLs()),
         ))
 
     ##
@@ -898,6 +903,13 @@
         else:
             return None
 
+    def addressBookHomeURLs(self):
+        home = self._addressBookHome()
+        if home is None:
+            return ()
+        else:
+            return (home.url(),)
+
     def _homeChildURL(self, name):
         if not hasattr(self, "calendarHomeURL"):
             home = self.calendarHome()
@@ -922,7 +934,22 @@
         else:
             return None
 
+    def _addressBookHome(self):
+        # FIXME: self.record.service.addressBookHomesCollection smells like a hack
+        # See AddressBookHomeProvisioningFile.__init__()
+        service = self.record.service
+        if hasattr(service, "addressBookHomesCollection"):
+            return service.addressBookHomesCollection.homeForDirectoryRecord(self.record)
+        else:
+            return None
 
+    def supportedReports(self):            # Method added for AddressBook find-shared report support
+        result = super(DirectoryCalendarPrincipalResource, self).supportedReports()
+        if config.EnableCardDAV and config.EnableFindSharedReport:
+            result.append(davxml.Report(carddavxml.AddressBookFindShared(),)) 
+        return result
+
+
     ##
     # Static
     ##

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -201,7 +201,9 @@
         """
         DirectoryPrincipalProvisioningResource.principalForCalendarUserAddress()
         """
-        for provisioningResource, recordType, recordResource, record in self._allRecords():
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in self._allRecords():
             principalURL = recordResource.principalURL()
             if principalURL.endswith("/"):
                 alternateURL = principalURL[:-1]
@@ -218,11 +220,33 @@
 
         # Explicitly check the disabled record
         provisioningResource = self.principalRootResources['XMLDirectoryService']
-        self.failIf(provisioningResource.principalForCalendarUserAddress("mailto:nocalendar at example.com") is not None)
-        self.failIf(provisioningResource.principalForCalendarUserAddress("urn:uuid:543D28BA-F74F-4D5F-9243-B3E3A61171E5") is not None)
-        self.failIf(provisioningResource.principalForCalendarUserAddress("/principals/users/nocalendar/") is not None)
-        self.failIf(provisioningResource.principalForCalendarUserAddress("/principals/__uids__/543D28BA-F74F-4D5F-9243-B3E3A61171E5/") is not None)
 
+        self.failUnlessIdentical(
+            provisioningResource.principalForCalendarUserAddress(
+                "mailto:nocalendar at example.com"
+            ),
+            None
+        )
+        self.failUnlessIdentical(
+            provisioningResource.principalForCalendarUserAddress(
+                "urn:uuid:543D28BA-F74F-4D5F-9243-B3E3A61171E5"
+            ),
+            None
+        )
+        self.failUnlessIdentical(
+            provisioningResource.principalForCalendarUserAddress(
+                "/principals/users/nocalendar/"
+            ),
+            None
+        )
+        self.failUnlessIdentical(
+            provisioningResource.principalForCalendarUserAddress(
+                "/principals/__uids__/543D28BA-F74F-4D5F-9243-B3E3A61171E5/"
+            ),
+            None
+        )
+
+
     def test_enabledForCalendaring(self):
         """
         DirectoryPrincipalProvisioningResource.principalForCalendarUserAddress()
@@ -460,7 +484,9 @@
             for each record in each directory in C{directoryServices}.
         """
         for directory in self.directoryServices:
-            provisioningResource = self.principalRootResources[directory.__class__.__name__]
+            provisioningResource = self.principalRootResources[
+                directory.__class__.__name__
+            ]
             for recordType in directory.recordTypes():
                 for record in directory.listRecords(recordType):
                     recordResource = provisioningResource.principalForRecord(record)

Modified: CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -36,6 +36,7 @@
 ELEMENT_GUID              = "guid"
 ELEMENT_ENABLE            = "enable"
 ELEMENT_HOSTEDAT          = "hosted-at"
+ELEMENT_ENABLEADDRESSBOOK = "enable-addressbook"
 ELEMENT_ENABLECALENDAR    = "enable-calendar"
 ELEMENT_AUTOSCHEDULE      = "auto-schedule"
 
@@ -45,11 +46,12 @@
 VALUE_FALSE               = "false"
 
 ELEMENT_AUGMENTRECORD_MAP = {
-    ELEMENT_GUID:           "guid",
-    ELEMENT_ENABLE:         "enabled",
-    ELEMENT_HOSTEDAT:       "hostedAt",
-    ELEMENT_ENABLECALENDAR: "enabledForCalendaring",
-    ELEMENT_AUTOSCHEDULE:   "autoSchedule",
+    ELEMENT_GUID:              "guid",
+    ELEMENT_ENABLE:            "enabled",
+    ELEMENT_HOSTEDAT:          "hostedAt",
+    ELEMENT_ENABLECALENDAR:    "enabledForCalendaring",
+    ELEMENT_ENABLEADDRESSBOOK: "enabledForAddressBooks",
+    ELEMENT_AUTOSCHEDULE:      "autoSchedule",
 }
 
 class XMLAugmentsParser(object):
@@ -101,6 +103,7 @@
                     ELEMENT_ENABLE,
                     ELEMENT_ENABLECALENDAR,
                     ELEMENT_AUTOSCHEDULE,
+                    ELEMENT_ENABLEADDRESSBOOK
                 ):
                     fields[node.tag] = node.text == VALUE_TRUE
                 else:

Modified: CalendarServer/trunk/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/xmlfile.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/directory/xmlfile.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -55,6 +55,7 @@
 
         defaults = {
             'xmlFile' : None,
+            'directoryBackedAddressBook': None,
             'recordTypes' : (
                 self.recordType_users,
                 self.recordType_groups,
@@ -105,8 +106,17 @@
         self._fileInfo = None
         self._lastCheck = 0
         self._alwaysStat = alwaysStat
+        self.directoryBackedAddressBook = params.get('directoryBackedAddressBook')
+
         self._accounts()
 
+
+    def createCache(self):
+        """
+        No-op to pacify addressbook backing.
+        """
+        
+
     def recordTypes(self):
         return self._recordTypes
 

Copied: CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/directorybackedaddressbook.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,136 @@
+##
+# Copyright (c) 2008-2009 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.
+##
+
+"""
+Directory-backed address book service resource and operations.
+"""
+
+__all__ = [
+    "DirectoryBackedAddressBookResource",
+]
+
+
+
+from twext.log import Logger
+from twisted.internet.defer import succeed, inlineCallbacks, maybeDeferred, returnValue
+from twisted.python.reflect import namedClass
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.resource import TwistedACLInheritable
+from twisted.web2.http import HTTPError, StatusResponse
+
+from twistedcaldav.config import config
+from twistedcaldav.resource import CalDAVResource
+
+
+
+
+log = Logger()
+
+
+
+class DirectoryBackedAddressBookResource (CalDAVResource):
+    """
+    Directory-backed address book
+    """
+
+    def __init__(self):
+
+        CalDAVResource.__init__(self)
+
+        self.directory = None       # creates directory attribute
+
+        
+    def provisionDirectory(self):
+        if self.directory is None:
+            directoryClass = namedClass(config.DirectoryAddressBook["type"])
+        
+            log.info("Configuring: %s:%r"
+                 % (config.DirectoryAddressBook["type"], config.DirectoryAddressBook["params"]))
+        
+            #add self as "directoryBackedAddressBook" parameter
+            params = config.DirectoryAddressBook["params"].copy()
+            params["directoryBackedAddressBook"] = self
+
+            self.directory = directoryClass(params)
+            return self.directory.createCache()
+            
+            #print ("DirectoryBackedAddressBookResource.provisionDirectory: provisioned")
+        
+        return succeed(None)
+
+
+    def defaultAccessControlList(self):
+        #print( "DirectoryBackedAddressBookResource.defaultAccessControlList" )
+        if config.AnonymousDirectoryAddressBookAccess:
+            # DAV:Read for all principals (includes anonymous)
+            accessPrincipal = davxml.All()
+        else:
+            # DAV:Read for all authenticated principals (does not include anonymous)
+            accessPrincipal = davxml.Authenticated()
+
+        return davxml.ACL(
+            davxml.ACE(
+                davxml.Principal(accessPrincipal),
+                davxml.Grant(
+                    davxml.Privilege(davxml.Read()),
+                    davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet())
+                                ),
+                davxml.Protected(),
+                TwistedACLInheritable(),
+           ),
+        )
+
+    def resourceType(self):
+        return davxml.ResourceType.addressbook #@UndefinedVariable
+
+    def isDirectoryBackedAddressBookCollection(self):
+        return True
+
+    def isAddressBookCollection(self):
+        #print( "DirectoryBackedAddressBookResource.isAddressBookCollection: return True" )
+        return True
+
+    def isCollection(self):
+        return True
+
+    def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
+        # Permissions here are fixed, and are not subject to inheritance rules, etc.
+        return succeed(self.defaultAccessControlList())
+    
+    @inlineCallbacks
+    def renderHTTP(self, request):
+        if not self.directory:
+            raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE,"Service is starting up" ))
+        elif self.directory.liveQuery:
+            response = (yield maybeDeferred(super(DirectoryBackedAddressBookResource, self).renderHTTP, request))
+            returnValue(response)
+        else:
+            available = (yield maybeDeferred(self.directory.available, ))
+        
+            if not available:
+                raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE,"Service is starting up" ))
+            else:
+                updateLock = self.directory.updateLock()
+                yield updateLock.acquire()
+                try:
+                    response = (yield maybeDeferred(super(DirectoryBackedAddressBookResource, self).renderHTTP, request))
+    
+                finally:
+                    yield updateLock.release()
+                
+                returnValue(response)
+

Modified: CalendarServer/trunk/twistedcaldav/method/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/__init__.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/method/__init__.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -33,5 +33,7 @@
     "report_calquery",
     "report_freebusy",
     "report_multiget",
+    "report_addressbook_multiget",
+    "report_addressbook_query",
     "report_sync_collection",
 ]

Modified: CalendarServer/trunk/twistedcaldav/method/copymove.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/copymove.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/method/copymove.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -34,9 +34,19 @@
 
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.method.put_common import StoreCalendarObjectResource
+from twistedcaldav.method.copymove_contact import (
+    maybeCOPYContact, maybeMOVEContact, KEEP_GOING
+)
+
 from twistedcaldav.resource import isCalendarCollectionResource,\
     isPseudoCalendarCollectionResource
 
+CalDAVFile = None               # Pacify PyFlakes; this *should* be fixed, but
+                                # it's not actually an undefined name, as the
+                                # bottom of twistedcaldav.static fixes it up
+                                # for us before any functions in this module
+                                # are invoked.
+
 log = Logger()
 
 @inlineCallbacks
@@ -54,8 +64,10 @@
 
     result, sourcecal, sourceparent, destination_uri, destination, destinationcal, destinationparent = (yield checkForCalendarAction(self, request))
     if not result or not destinationcal:
-        # Do default WebDAV action
-        result = (yield super(CalDAVFile, self).http_COPY(request))
+        # Check with CardDAV first (XXX might want to check EnableCardDAV switch?)
+        result = yield maybeCOPYContact(self, request)
+        if result is KEEP_GOING:
+            result = yield super(CalDAVFile, self).http_COPY(request)
         returnValue(result)
 
     #
@@ -122,6 +134,11 @@
         is_calendar_collection = isPseudoCalendarCollectionResource(self)
         defaultCalendar = (yield self.isDefaultCalendar(request)) if is_calendar_collection else False
 
+        if not is_calendar_collection:
+            result = yield maybeMOVEContact(self, request)
+            if result is not KEEP_GOING:
+                returnValue(result)
+
         # Do default WebDAV action
         result = (yield super(CalDAVFile, self).http_MOVE(request))
         

Copied: CalendarServer/trunk/twistedcaldav/method/copymove_contact.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/method/copymove_contact.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/copymove_contact.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/method/copymove_contact.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,241 @@
+##
+# Copyright (c) 2006-2009 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.
+##
+
+"""
+CalDAV COPY and MOVE methods.
+"""
+
+__all__ = ["maybeCOPYContact", "maybeMOVEContact"]
+
+from urlparse import urlsplit
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.web2 import responsecode
+from twisted.web2.filter.location import addLocation
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.dav.util import parentForURL
+from twisted.web2.http import StatusResponse, HTTPError
+
+from twistedcaldav.carddavxml import carddav_namespace
+from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
+from twistedcaldav.resource import isAddressBookCollectionResource
+from twext.log import Logger
+
+log = Logger()
+
+KEEP_GOING = object()
+
+ at inlineCallbacks
+def maybeCOPYContact(self, request):
+    """
+    Special handling of COPY request if parents are addressbook collections.
+    When copying we do not have to worry about the source resource as it
+    is not being changed in any way. We do need to do an index update for
+    the destination if its an addressbook collection.
+    """
+    # Copy of addressbook collections isn't allowed.
+    if isAddressBookCollectionResource(self):
+        returnValue(responsecode.FORBIDDEN)
+
+    result, sourceadbk, sourceparent, destination_uri, destination, destinationadbk, destinationparent = (yield checkForAddressBookAction(self, request))
+    if not result or not destinationadbk:
+        # Give up, do default action.
+        
+        returnValue(KEEP_GOING)
+
+    #
+    # Check authentication and access controls
+    #
+    yield self.authorize(request, (davxml.Read(),), recurse=True)
+
+    if destination.exists():
+        yield destination.authorize(request, (davxml.WriteContent(), davxml.WriteProperties()), recurse=True)
+    else:
+        destparent = (yield request.locateResource(parentForURL(destination_uri)))
+        yield destparent.authorize(request, (davxml.Bind(),))
+
+    # Check for existing destination resource
+    overwrite = request.headers.getHeader("overwrite", True)
+    if destination.exists() and not overwrite:
+        log.err("Attempt to copy onto existing file without overwrite flag enabled: %s"
+                % (destination.fp.path,))
+        raise HTTPError(StatusResponse(
+            responsecode.PRECONDITION_FAILED,
+            "Destination %s already exists." % (destination_uri,))
+        )
+
+    # Checks for copying an addressbook collection
+    if self.isAddressBookCollection():
+        log.err("Attempt to copy an addressbook collection into another addressbook collection %s" % destination)
+        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "addressbook-collection-location-ok")))
+
+    # We also do not allow regular collections in addressbook collections
+    if self.isCollection():
+        log.err("Attempt to copy a collection into an addressbook collection")
+        raise HTTPError(StatusResponse(
+            responsecode.FORBIDDEN,
+            "Cannot create collection within special collection %s" % (destination,))
+        )
+
+    # May need to add a location header
+    addLocation(request, destination_uri)
+
+    storer = StoreAddressObjectResource(
+        request = request,
+        source = self,
+        source_uri = request.uri,
+        sourceparent = sourceparent,
+        sourceadbk = sourceadbk,
+        destination = destination,
+        destination_uri = destination_uri,
+        destinationparent = destinationparent,
+        destinationadbk = destinationadbk,
+    )
+    result = (yield storer.run())
+    returnValue(result)
+
+ at inlineCallbacks
+def maybeMOVEContact(self, request):
+    """
+    Special handling of MOVE request if parent is an addressbook collection.
+    When moving we may need to remove the index entry for the source resource
+    since its effectively being deleted. We do need to do an index update for
+    the destination if its an addressbook collection
+    """
+    result, sourceadbk, sourceparent, destination_uri, destination, destinationadbk, destinationparent = (yield checkForAddressBookAction(self, request))
+    if not result or not destinationadbk:
+
+        # assume it will work and dirty caches
+        if isAddressBookCollectionResource(self):
+            yield self.updateCTag()
+            
+        # Do default WebDAV action
+        returnValue(KEEP_GOING)
+        
+    #
+    # Check authentication and access controls
+    #
+    parent = (yield request.locateResource(parentForURL(request.uri)))
+    yield parent.authorize(request, (davxml.Unbind(),))
+
+    if destination.exists():
+        yield destination.authorize(request, (davxml.Bind(), davxml.Unbind()), recurse=True)
+    else:
+        destparent = (yield request.locateResource(parentForURL(destination_uri)))
+        yield destparent.authorize(request, (davxml.Bind(),))
+
+    # Check for existing destination resource
+    overwrite = request.headers.getHeader("overwrite", True)
+    if destination.exists() and not overwrite:
+        log.err("Attempt to copy onto existing file without overwrite flag enabled: %s"
+                % (destination.fp.path,))
+        raise HTTPError(StatusResponse(
+            responsecode.PRECONDITION_FAILED,
+            "Destination %s already exists." % (destination_uri,)
+        ))
+
+    if destinationadbk:
+        # Checks for copying an addressbook collection
+        if self.isAddressBookCollection():
+            log.err("Attempt to move an addressbook collection into another addressbook collection %s" % destination)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "addressbook-collection-location-ok")))
+    
+        # We also do not allow regular collections in addressbook collections
+        if self.isCollection():
+            log.err("Attempt to move a collection into an addressbook collection")
+            raise HTTPError(StatusResponse(
+                responsecode.FORBIDDEN,
+                "Cannot create collection within special collection %s" % (destination,)
+            ))
+
+    # May need to add a location header
+    addLocation(request, destination_uri)
+
+    storer = StoreAddressObjectResource(
+        request = request,
+        source = self,
+        source_uri = request.uri,
+        sourceparent = sourceparent,
+        sourceadbk = sourceadbk,
+        deletesource = True,
+        destination = destination,
+        destination_uri = destination_uri,
+        destinationparent = destinationparent,
+        destinationadbk = destinationadbk,
+    )
+    result = (yield storer.run())
+    returnValue(result)
+
+ at inlineCallbacks
+def checkForAddressBookAction(self, request):
+    """
+    Check to see whether the source or destination of the copy/move
+    is an addressbook collection, since we need to do special processing
+    if that is the case.
+    @return: tuple::
+        result:           True if special CalDAV processing required, False otherwise
+            NB If there is any type of error with the request, return False
+            and allow normal COPY/MOVE processing to return the error.
+        sourceadbk:        True if source is in an addressbook collection, False otherwise
+        sourceparent:     The parent resource for the source
+        destination_uri:  The URI of the destination resource
+        destination:      CalDAVFile of destination if special processing required,
+        None otherwise
+        destinationadbk:   True if the destination is in an addressbook collection,
+            False otherwise
+        destinationparent:The parent resource for the destination
+        
+    """
+    
+    result = False
+    sourceadbk = False
+    destinationadbk = False
+    
+    # Check the source path first
+    if not self.fp.exists():
+        log.err("File not found: %s" % (self.fp.path,))
+        raise HTTPError(StatusResponse(
+            responsecode.NOT_FOUND,
+            "Source resource %s not found." % (request.uri,)
+        ))
+
+    # Check for parent addressbook collection
+    sourceparent = (yield request.locateResource(parentForURL(request.uri)))
+    if isAddressBookCollectionResource(sourceparent):
+        result = True
+        sourceadbk = True
+    
+    #
+    # Find the destination resource
+    #
+    destination_uri = request.headers.getHeader("destination")
+
+    if not destination_uri:
+        msg = "No destination header in %s request." % (request.method,)
+        log.err(msg)
+        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
+    
+    destination = (yield request.locateResource(destination_uri))
+
+    # Check for parent addressbook collection
+    destination_uri = urlsplit(destination_uri)[2]
+    destinationparent = (yield request.locateResource(parentForURL(destination_uri)))
+    if isAddressBookCollectionResource(destinationparent):
+        result = True
+        destinationadbk = True
+
+    returnValue((result, sourceadbk, sourceparent, destination_uri, destination, destinationadbk, destinationparent))

Modified: CalendarServer/trunk/twistedcaldav/method/delete_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/delete_common.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/method/delete_common.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -13,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from twistedcaldav.method.report_common import applyToAddressBookCollections
+from twistedcaldav.method.report_common import applyToCalendarCollections
 
 
 """
@@ -36,7 +38,7 @@
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 from twistedcaldav.method.report_common import applyToCalendarCollections
 from twistedcaldav.resource import isCalendarCollectionResource,\
-    isPseudoCalendarCollectionResource
+    isPseudoCalendarCollectionResource, isAddressBookCollectionResource
 from twistedcaldav.scheduling.implicit import ImplicitScheduler
 
 log = Logger()
@@ -277,7 +279,12 @@
             
         elif isCalendarCollectionResource(self.resource):
             response = (yield self.deleteCalendar(self.resource, self.resource_uri, self.parent))
-        
+        elif isAddressBookCollectionResource(self.parent):
+            response = (yield self.deleteAddressBookResource(self.resource, self.resource_uri, self.parent))
+
+        elif isAddressBookCollectionResource(self.resource):
+            response = (yield self.deleteAddressBook(self.resource, self.resource_uri, self.parent))
+
         elif self.resource.isCollection():
             response = (yield self.deleteCollection())
 
@@ -285,3 +292,134 @@
             response = (yield self.deleteResource(self.resource, self.resource_uri, self.parent))
 
         returnValue(response)
+
+    @inlineCallbacks
+    def deleteAddressBookResource(self, delresource, deluri, parent):
+        """
+        Delete a single addressbook resource and do implicit scheduling actions if required.
+
+        @param delresource:
+        @type delresource:
+        @param deluri:
+        @type deluri:
+        @param parent:
+        @type parent:
+        """
+
+        # TODO: need to use transaction based delete on live scheduling object resources
+        # as the iTIP operation may fail and may need to prevent the delete from happening.
+    
+        # Do quota checks before we start deleting things
+        myquota = (yield delresource.quota(self.request))
+        if myquota is not None:
+            old_size = (yield delresource.quotaSize(self.request))
+        else:
+            old_size = 0
+        
+        try:
+    
+            # Do delete
+            response = (yield delete(deluri, delresource.fp, self.depth))
+
+            # Adjust quota
+            if myquota is not None:
+                yield delresource.quotaSizeAdjust(self.request, -old_size)
+    
+            if response == responsecode.NO_CONTENT:
+                index = parent.index()
+                index.deleteResource(delresource.fp.basename())
+    
+                # Change CTag on the parent addressbook collection
+                yield parent.updateCTag()
+    
+    
+        except MemcacheLockTimeoutError:
+            raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use on the server." % (deluri,)))
+    
+                
+        returnValue(response)
+
+    @inlineCallbacks
+    def deleteAddressBook(self, delresource, deluri, parent):
+        """
+        Delete an entire addressbook collection by deleting each child resource in turn to
+        ensure that proper implicit scheduling actions occur.
+        
+        This has to emulate the behavior in fileop.delete in that any errors need to be
+        reported back in a multistatus response.
+        """
+
+
+        if self.depth != "infinity":
+            msg = "Client sent illegal depth header value for DELETE: %s" % (self.depth,)
+            log.err(msg)
+            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
+
+        log.debug("Deleting addressbook %s" % (delresource.fp.path,))
+
+        errors = ResponseQueue(deluri, "DELETE", responsecode.NO_CONTENT)
+
+        for childname in delresource.listChildren():
+
+            childurl = joinURL(deluri, childname)
+            child = (yield self.request.locateChildResource(delresource, childname))
+
+            try:
+                yield self.deleteAddressBookResource(child, childurl, delresource)
+            except:
+                errors.add(childurl, responsecode.BAD_REQUEST)
+
+        # Now do normal delete
+        yield delresource.updateCTag()
+        more_responses = (yield self.deleteResource(delresource, deluri, parent))
+        
+        if isinstance(more_responses, MultiStatusResponse):
+            # Merge errors
+            errors.responses.update(more_responses.children)                
+
+        response = errors.response()
+        
+        returnValue(response)
+
+    @inlineCallbacks
+    def deleteCollectionAB(self):
+        # XXX CSCS-MERGE this needs to be merged into deleteCollection
+        """
+        Delete a regular collection with special processing for any addressbook collections
+        contained within it.
+        """
+        if self.depth != "infinity":
+            msg = "Client sent illegal depth header value for DELETE: %s" % (self.depth,)
+            log.err(msg)
+            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
+
+        log.debug("Deleting collection %s" % (self.resource.fp.path,))
+
+        errors = ResponseQueue(self.resource_uri, "DELETE", responsecode.NO_CONTENT)
+ 
+        @inlineCallbacks
+        def doDeleteAddressBook(delresource, deluri):
+            
+            delparent = (yield delresource.locateParent(self.request, deluri))
+
+            response = (yield self.deleteAddressBook(delresource, deluri, delparent))
+
+            if isinstance(response, MultiStatusResponse):
+                # Merge errors
+                errors.responses.update(response.children)                
+
+            returnValue(True)
+
+        yield applyToAddressBookCollections(self.resource, self.request, self.resource_uri, self.depth, doDeleteAddressBook, None)
+
+        # Now do normal delete
+        more_responses = (yield self.deleteResource(self.resource, self.resource_uri, self.parent))
+        
+        if isinstance(more_responses, MultiStatusResponse):
+            # Merge errors
+            errors.responses.update(more_responses.children)                
+
+        response = errors.response()
+
+        returnValue(response)
+        

Modified: CalendarServer/trunk/twistedcaldav/method/mkcol.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/mkcol.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/method/mkcol.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -1,5 +1,6 @@
+# -*- test-case-name: twistedcaldav.test.test_DAV.MKCOL -*-
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -15,29 +16,175 @@
 ##
 
 """
-CalDAV MKCOL method.
+Extended MKCOL method.
 """
 
 __all__ = ["http_MKCOL"]
 
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from twisted.python import log
+from twisted.python.failure import Failure
+
 from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse, MultiStatusResponse, PropertyStatusResponseQueue
+from twisted.web2.dav.util import davXMLFromStream
+from twisted.web2.dav.util import parentForURL
+from twisted.web2.http import HTTPError
 from twisted.web2.http import StatusResponse
 
+from twistedcaldav import caldavxml, carddavxml, mkcolxml
+from twistedcaldav.config import config
+from twistedcaldav.resource import isAddressBookCollectionResource
 from twistedcaldav.resource import isPseudoCalendarCollectionResource
+from twistedcaldav.static import CalDAVFile
 
+ at inlineCallbacks
 def http_MKCOL(self, request):
+
     #
-    # Don't allow DAV collections in a calendar collection
+    # Check authentication and access controls
     #
-    def gotParent(parent):
+    parent = (yield request.locateResource(parentForURL(request.uri)))
+
+    yield parent.authorize(request, (davxml.Bind(),))
+
+    if self.exists():
+        log.err("Attempt to create collection where file exists: %s"
+                % (self.fp.path,))
+        raise HTTPError(ErrorResponse(
+            responsecode.FORBIDDEN,
+            (davxml.dav_namespace, "resource-must-be-null"))
+        )
+
+    if not parent.isCollection():
+        log.err("Attempt to create collection with non-collection parent: %s"
+                % (self.fp.path,))
+        raise HTTPError(ErrorResponse(
+            responsecode.CONFLICT,
+            (davxml.dav_namespace, "collection-location-ok"))
+        )
+
+    #
+    # Don't allow DAV collections in a calendar or address book collection
+    #
+
+    if config.EnableCalDAV:
+        parent = (yield self._checkParents(request, isPseudoCalendarCollectionResource))
+    
         if parent is not None:
-            return StatusResponse(
+            raise HTTPError(StatusResponse(
                 responsecode.FORBIDDEN,
                 "Cannot create collection within calendar collection %s" % (parent,)
-            )
+            ))
 
-        return super(CalDAVFile, self).http_MKCOL(request)
+    if config.EnableCardDAV:
+        parent = (yield self._checkParents(request, isAddressBookCollectionResource))
+    
+        if parent is not None:
+            raise HTTPError(StatusResponse(
+                responsecode.FORBIDDEN,
+                "Cannot create collection within address book collection %s" % (parent,)
+            ))
 
-    d = self._checkParents(request, isPseudoCalendarCollectionResource)
-    d.addCallback(gotParent)
-    return d
+    #
+    # Read request body
+    #
+    try:
+        doc = (yield davXMLFromStream(request.stream))
+    except ValueError, e:
+        log.err("Error while handling MKCOL: %s" % (e,))
+        # TODO: twisted.web2.dav 'MKCOL' tests demand this particular response
+        # code, but should we really be looking at the XML content or the
+        # content-type header?  It seems to me like this ought to be considered
+        # a BAD_REQUEST if it claims to be XML but isn't, but an
+        # UNSUPPORTED_MEDIA_TYPE if it claims to be something else. -glyph
+        raise HTTPError(
+            StatusResponse(responsecode.UNSUPPORTED_MEDIA_TYPE, str(e))
+        )
+
+    if doc is not None:
+
+        # Parse response body
+        mkcol = doc.root_element
+        if not isinstance(mkcol, mkcolxml.MakeCollection):
+            # Clean up
+            if self.fp.exists(): self.fp.remove()
+
+            error = ("Non-%s element in MKCOL request body: %s"
+                     % (mkcolxml.MakeCollection.name, mkcol))
+            log.err(error)
+            raise HTTPError(StatusResponse(responsecode.UNSUPPORTED_MEDIA_TYPE, error))
+
+        errors = PropertyStatusResponseQueue("PROPPATCH", request.uri, responsecode.NO_CONTENT)
+        got_an_error = False
+    
+        if mkcol.children:
+            # mkcol -> set -> prop -> property*
+            properties = mkcol.children[0].children[0].children
+
+            # First determine the resource type
+            rtype = None
+            for property in properties:
+                if isinstance(property, davxml.ResourceType):
+                    if rtype:
+                        error = "Multiple {DAV:}resource-type properties in MKCOL request body: %s" % (mkcol,)
+                        log.err(error)
+                        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error))
+                    else:
+                        if property.childrenOfType(davxml.Collection):
+                            if property.childrenOfType(caldavxml.Calendar):
+                                rtype = "calendar"
+                            elif property.childrenOfType(carddavxml.AddressBook):
+                                rtype = "addressbook"
+            if not rtype:
+                error = "No {DAV:}resource-type property in MKCOL request body: %s" % (mkcol,)
+                log.err(error)
+                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error))
+            elif rtype not in ("calendar", "addressbook"):
+                error = "{DAV:}resource-type property in MKCOL request body not supported: %s" % (mkcol,)
+                log.err(error)
+                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error))
+                
+            # Make sure feature is enabled
+            if (rtype == "calendar" and not config.EnableCalDAV or
+                rtype == "addressbook" and not config.EnableCardDAV):
+                error = "{DAV:}resource-type property in MKCOL request body not supported: %s" % (mkcol,)
+                log.err(error)
+                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error))
+            
+            # Now create the special collection
+            if rtype == "calendar":
+                yield self.createCalendar(request)
+            elif rtype == "addressbook":
+                yield self.createAddressBook(request)
+
+            # Now handle other properties
+            for property in mkcol.children[0].children[0].children:
+                try:
+                    if rtype == "calendar":
+                        if property.qname() == (caldavxml.caldav_namespace, "supported-calendar-component-set"):
+                            self.writeDeadProperty(property)
+                        elif not isinstance(property, davxml.ResourceType):
+                            yield self.writeProperty(property, request)
+                except HTTPError:
+                    errors.add(Failure(), property)
+                    got_an_error = True
+                else:
+                    errors.add(responsecode.OK, property)
+    
+        if got_an_error:
+            # Clean up
+            if self.fp.exists(): self.fp.remove()
+
+            errors.error()
+            raise HTTPError(MultiStatusResponse([errors.response()]))
+
+        yield returnValue(responsecode.CREATED)
+    
+    else:
+        # No request body so it is a standard MKCOL
+        result = yield super(CalDAVFile, self).http_MKCOL(request)
+        returnValue(result)
+

Modified: CalendarServer/trunk/twistedcaldav/method/put.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/method/put.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 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.
@@ -29,11 +29,17 @@
 from twext.web2.dav.davxml import ErrorResponse
 
 from twistedcaldav.caldavxml import caldav_namespace
+
 from twistedcaldav.method.put_common import StoreCalendarObjectResource
 from twistedcaldav.resource import isPseudoCalendarCollectionResource
+from twistedcaldav.static import CalDAVFile
 
 log = Logger()
 
+from twistedcaldav.carddavxml import carddav_namespace
+from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
+from twistedcaldav.resource import isAddressBookCollectionResource
+
 @inlineCallbacks
 def http_PUT(self, request):
 
@@ -75,6 +81,40 @@
             log.err("Error while handling (calendar) PUT: %s" % (e,))
             raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
 
+    elif isAddressBookCollectionResource(parent):
+        self.fp.restat(False)
+
+        # Content-type check
+        content_type = request.headers.getHeader("content-type")
+        if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "vcard"):
+            log.err("MIME type %s not allowed in address book collection" % (content_type,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "supported-address-data")))
+            
+        # Read the vcard component from the stream
+        try:
+            vcarddata = (yield allDataFromStream(request.stream))
+
+            # We must have some data at this point
+            if vcarddata is None:
+                # Use correct DAV:error response
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "valid-address-data")))
+
+            storer = StoreAddressObjectResource(
+                request = request,
+                sourceadbk = False,
+                vcard = vcarddata,
+                destination = self,
+                destination_uri = request.uri,
+                destinationadbk = True,
+                destinationparent = parent,
+            )
+            result = (yield storer.run())
+            returnValue(result)
+
+        except ValueError, e:
+            log.err("Error while handling (address book) PUT: %s" % (e,))
+            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
+
     else:
         result = (yield super(CalDAVFile, self).http_PUT(request))
 

Copied: CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/method/put_addressbook_common.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,682 @@
+##
+# Copyright (c) 2005-2009 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.
+##
+
+"""
+PUT/COPY/MOVE common behavior.
+"""
+
+__all__ = ["StoreAddressObjectResource"]
+
+import types
+
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred, inlineCallbacks, succeed
+from twisted.internet.defer import maybeDeferred, returnValue
+from twisted.python import failure
+from twisted.python.filepath import FilePath
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.element.base import dav_namespace
+from twisted.web2.dav.element.base import PCDATAElement
+from twisted.web2.dav.fileop import delete
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.dav.resource import TwistedGETContentMD5
+from twisted.web2.dav.stream import MD5StreamWrapper
+from twisted.web2.dav.util import joinURL, parentForURL
+from twisted.web2.http import HTTPError
+from twisted.web2.http import StatusResponse
+from twisted.web2.iweb import IResponse
+from twisted.web2.stream import MemoryStream
+
+from twistedcaldav.config import config
+from twistedcaldav.caldavxml import NumberOfRecurrencesWithinLimits
+from twistedcaldav.carddavxml import NoUIDConflict, carddav_namespace
+from twistedcaldav.fileops import copyToWithXAttrs
+from twistedcaldav.fileops import putWithXAttrs
+from twistedcaldav.fileops import copyWithXAttrs
+from twistedcaldav.instance import TooManyInstancesError
+from twistedcaldav.vcard import Component
+from twistedcaldav.vcardindex import ReservationError
+from twext.log import Logger
+#from twistedcaldav.scheduling.implicit import ImplicitScheduler
+
+log = Logger()
+
+class StoreAddressObjectResource(object):
+    
+    class RollbackState(object):
+        """
+        This class encapsulates the state needed to rollback the entire PUT/COPY/MOVE
+        transaction, leaving the server state the same as it was before the request was
+        processed. The DoRollback method will actually execute the rollback operations.
+        """
+        
+        def __init__(self, storer):
+            self.storer = storer
+            self.active = True
+            self.source_copy = None
+            self.destination_copy = None
+            self.destination_created = False
+            self.source_deleted = False
+            self.source_index_deleted = False
+            self.destination_index_deleted = False
+        
+        def Rollback(self):
+            """
+            Rollback the server state. Do not allow this to raise another exception. If
+            rollback fails then we are going to be left in an awkward state that will need
+            to be cleaned up eventually.
+            """
+            if self.active:
+                self.active = False
+                log.debug("Rollback: rollback")
+                try:
+                    if self.source_copy and self.source_deleted:
+                        self.source_copy.moveTo(self.storer.source.fp)
+                        log.debug("Rollback: source restored %s to %s" % (self.source_copy.path, self.storer.source.fp.path))
+                        self.source_copy = None
+                        self.source_deleted = False
+                    if self.destination_copy:
+                        self.storer.destination.fp.remove()
+                        log.debug("Rollback: destination restored %s to %s" % (self.destination_copy.path, self.storer.destination.fp.path))
+                        self.destination_copy.moveTo(self.storer.destination.fp)
+                        self.destination_copy = None
+                    elif self.destination_created:
+                        if self.storer.destinationadbk and self.storer.indexdestination:
+                            self.storer.doRemoveDestinationIndex()
+                            log.debug("Rollback: destination index removed %s" % (self.storer.destination.fp.path,))
+                            self.destination_index_deleted = False
+                        self.storer.destination.fp.remove()
+                        log.debug("Rollback: destination removed %s" % (self.storer.destination.fp.path,))
+                        self.destination_created = False
+                    if self.destination_index_deleted:
+                        # Must read in vcard for destination being re-indexed
+                        self.storer.doDestinationIndex(self.storer.destination.vCard())
+                        self.destination_index_deleted = False
+                        log.debug("Rollback: destination re-indexed %s" % (self.storer.destination.fp.path,))
+                    if self.source_index_deleted:
+                        self.storer.doSourceIndexRecover()
+                        self.destination_index_deleted = False
+                        log.debug("Rollback: source re-indexed %s" % (self.storer.source.fp.path,))
+                except:
+                    log.err("Rollback: exception caught and not handled: %s" % failure.Failure())
+
+        def Commit(self):
+            """
+            Commit the resource changes by wiping the rollback state.
+            """
+            if self.active:
+                log.debug("Rollback: commit")
+                self.active = False
+                if self.source_copy:
+                    self.source_copy.remove()
+                    log.debug("Rollback: removed source backup %s" % (self.source_copy.path,))
+                    self.source_copy = None
+                if self.destination_copy:
+                    self.destination_copy.remove()
+                    log.debug("Rollback: removed destination backup %s" % (self.destination_copy.path,))
+                    self.destination_copy = None
+                self.destination_created = False
+                self.source_deleted = False
+                self.source_index_deleted = False
+                self.destination_index_deleted = False
+
+    class UIDReservation(object):
+        
+        def __init__(self, index, uid, uri):
+            self.reserved = False
+            self.index = index
+            self.uid = uid
+            self.uri = uri
+            
+        @inlineCallbacks
+        def reserve(self):
+            
+            # Lets use a deferred for this and loop a few times if we cannot reserve so that we give
+            # time to whoever has the reservation to finish and release it.
+            failure_count = 0
+            while(failure_count < 10):
+                try:
+                    yield self.index.reserveUID(self.uid)
+                    self.reserved = True
+                    break
+                except ReservationError:
+                    self.reserved = False
+                failure_count += 1
+                
+                pause = Deferred()
+                def _timedDeferred():
+                    pause.callback(True)
+                reactor.callLater(0.5, _timedDeferred) #@UndefinedVariable
+                yield pause
+            
+            if self.uri and not self.reserved:
+                raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use." % (self.uri,)))
+        
+        @inlineCallbacks
+        def unreserve(self):
+            if self.reserved:
+                yield self.index.unreserveUID(self.uid)
+                self.reserved = False
+
+    def __init__(
+        self,
+        request,
+        source=None, source_uri=None, sourceparent=None, sourceadbk=False, deletesource=False,
+        destination=None, destination_uri=None, destinationparent=None, destinationadbk=True,
+        vcard=None,
+        indexdestination = True,
+        updatedestinationctag = True,
+   ):
+        """
+        Function that does common PUT/COPY/MOVE behavior.
+        
+        @param request:           the L{twisted.web2.server.Request} for the current HTTP request.
+        @param source:            the L{CalDAVFile} for the source resource to copy from, or None if source data
+            is to be read from the request.
+        @param source_uri:        the URI for the source resource.
+        @param destination:       the L{CalDAVFile} for the destination resource to copy into.
+        @param destination_uri:   the URI for the destination resource.
+        @param vcard:          the C{str} or L{Component} vcard data if there is no source, None otherwise.
+        @param sourceadbk:         True if the source resource is in a vcard collection, False otherwise.
+        @param destinationadbk:    True if the destination resource is in a vcard collection, False otherwise
+        @param sourceparent:      the L{CalDAVFile} for the source resource's parent collection, or None if source is None.
+        @param destinationparent: the L{CalDAVFile} for the destination resource's parent collection.
+        @param deletesource:      True if the source resource is to be deleted on successful completion, False otherwise.
+        """
+        
+        # Check that all arguments are valid
+        try:
+            assert destination is not None and destinationparent is not None and destination_uri is not None
+            assert (source is None and sourceparent is None) or (source is not None and sourceparent is not None)
+            assert (vcard is None and source is not None) or (vcard is not None and source is None)
+            assert not deletesource or (deletesource and source is not None)
+        except AssertionError:
+            log.err("Invalid arguments to StoreAddressObjectResource.__init__():")
+            log.err("request=%s\n" % (request,))
+            log.err("sourceadbk=%s\n" % (sourceadbk,))
+            log.err("destinationadbk=%s\n" % (destinationadbk,))
+            log.err("source=%s\n" % (source,))
+            log.err("source_uri=%s\n" % (source_uri,))
+            log.err("sourceparent=%s\n" % (sourceparent,))
+            log.err("destination=%s\n" % (destination,))
+            log.err("destination_uri=%s\n" % (destination_uri,))
+            log.err("destinationparent=%s\n" % (destinationparent,))
+            log.err("vcard=%s\n" % (vcard,))
+            log.err("deletesource=%s\n" % (deletesource,))
+            raise
+    
+        self.request = request
+        self.sourceadbk = sourceadbk
+        self.destinationadbk = destinationadbk
+        self.source = source
+        self.source_uri = source_uri
+        self.sourceparent = sourceparent
+        self.destination = destination
+        self.destination_uri = destination_uri
+        self.destinationparent = destinationparent
+        self.vcard = vcard
+        self.vcarddata = None
+        self.deletesource = deletesource
+        self.indexdestination = indexdestination
+        self.updatedestinationctag = updatedestinationctag
+        #self.isiTIP = isiTIP
+        #self.allowImplicitSchedule = allowImplicitSchedule
+        #self.internal_request = internal_request
+        
+        self.rollback = None
+        self.access = None
+
+    def fullValidation(self):
+        """
+        Do full validation of source and destination vcard data.
+        """
+
+        if self.destinationadbk:
+            # Valid resource name check
+            result, message = self.validResourceName()
+            if not result:
+                log.err(message)
+                raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
+
+            if not self.sourceadbk:
+                # Valid content type check on the source resource if its not in a vcard collection
+                if self.source is not None:
+                    result, message = self.validContentType()
+                    if not result:
+                        log.err(message)
+                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "supported-address-data")))
+                
+                    # At this point we need the calendar data to do more tests
+                    self.vcard = self.source.vCard()
+                else:
+                    try:
+                        if type(self.vcard) in (types.StringType, types.UnicodeType,):
+                            self.vcarddata = self.vcard
+                            self.vcard = Component.fromString(self.vcard)
+                    except ValueError, e:
+                        log.err(str(e))
+                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "valid-address-data")))
+                        
+                # Valid vcard data for CalDAV check
+                result, message = self.validCardDAVDataCheck()
+                if not result:
+                    log.err(message)
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "valid-addressbook-object-resource")))
+
+                # Must have a valid UID at this point
+                self.uid = self.vcard.resourceUID()
+            else:
+                # Get UID from original resource
+                self.source_index = self.sourceparent.index()
+                self.uid = self.source_index.resourceUIDForName(self.source.fp.basename())
+                if self.uid is None:
+                    log.err("Source vcard does not have a UID: %s" % self.source.fp.basename())
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "valid-addressbook-object-resource")))
+
+                # FIXME: We need this here because we have to re-index the destination. Ideally it
+                # would be better to copy the index entries from the source and add to the destination.
+                self.vcard = self.source.vCard()
+
+            # Valid vcard data size check
+            result, message = self.validSizeCheck()
+            if not result:
+                log.err(message)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "max-resource-size")))
+
+            # Check access
+            return succeed(None)
+    
+    def validResourceName(self):
+        """
+        Make sure that the resource name for the new resource is valid.
+        """
+        result = True
+        message = ""
+        filename = self.destination.fp.basename()
+        if filename.startswith("."):
+            result = False
+            message = "File name %s not allowed in vcard collection" % (filename,)
+
+        return result, message
+        
+    def validContentType(self):
+        """
+        Make sure that the content-type of the source resource is text/vcard.
+        This test is only needed when the source is not in a vcard collection.
+        """
+        result = True
+        message = ""
+        content_type = self.source.contentType()
+        if not ((content_type.mediaType == "text") and (content_type.mediaSubtype == "vcard")):
+            result = False
+            message = "MIME type %s not allowed in vcard collection" % (content_type,)
+
+        return result, message
+        
+    def validCardDAVDataCheck(self):
+        """
+        Check that the vcard data is valid vCard.
+        @return:         tuple: (True/False if the vcard data is valid,
+                                 log message string).
+        """
+        result = True
+        message = ""
+        if self.vcard is None:
+            result = False
+            message = "Empty resource not allowed in vcard collection"
+        else:
+            try:
+                self.vcard.validForCardDAV()
+            except ValueError, e:
+                result = False
+                message = "Invalid vcard data: %s" % (e,)
+        
+        return result, message
+    
+    def validCalDAVDataCheck(self):
+        """
+        Check that the vcard data is valid as a CalDAV vcard object resource.
+        @return:         tuple: (True/False if the vcard data is valid,
+                                 log message string).
+        """
+        result = True
+        message = ""
+        try:
+            self.vcard.validateForCardDAV()
+        except ValueError, e:
+            result = False
+            message = "Calendar data does not conform to CalDAV requirements: %s" % (e,)
+        
+        return result, message
+    
+    def validSizeCheck(self):
+        """
+        Make sure that the content-type of the source resource is text/vcard.
+        This test is only needed when the source is not in a vcard collection.
+        """
+        result = True
+        message = ""
+        if config.MaximumAttachmentSize:
+            vcardsize = len(str(self.vcard))
+            if vcardsize > config.MaximumAttachmentSize:
+                result = False
+                message = "Data size %d bytes is larger than allowed limit %d bytes" % (vcardsize, config.MaximumAttachmentSize)
+
+        return result, message
+
+    def noUIDConflict(self, uid):
+        """
+        Check that the UID of the new vcard object conforms to the requirements of
+        CardDAV, i.e. it must be unique in the collection and we must not overwrite a
+        different UID.
+        @param uid: the UID for the resource being stored.
+        @return: tuple: (True/False if the UID is valid, log message string,
+            name of conflicted resource).
+        """
+
+        result = True
+        message = ""
+        rname = ""
+
+        # Adjust for a move into same vcard collection
+        oldname = None
+        if self.sourceparent and (self.sourceparent.fp.path == self.destinationparent.fp.path) and self.deletesource:
+            oldname = self.source.fp.basename()
+
+        # UID must be unique
+        index = self.destinationparent.index()
+        if not index.isAllowedUID(uid, oldname, self.destination.fp.basename()):
+            rname = index.resourceNameForUID(uid)
+            # This can happen if two simultaneous PUTs occur with the same UID.
+            # i.e. one PUT has reserved the UID but has not yet written the resource,
+            # the other PUT tries to reserve and fails but no index entry exists yet.
+            if rname is None:
+                rname = "<<Unknown Resource>>"
+            
+            result = False
+            message = "Address book resource %s already exists with same UID %s" % (rname, uid)
+        else:
+            # Cannot overwrite a resource with different UID
+            if self.destination.fp.exists():
+                olduid = index.resourceUIDForName(self.destination.fp.basename())
+                if olduid != uid:
+                    rname = self.destination.fp.basename()
+                    result = False
+                    message = "Cannot overwrite vcard resource %s with different UID %s" % (rname, olduid)
+        
+        return result, message, rname
+
+    @inlineCallbacks
+    def checkQuota(self):
+        """
+        Get quota details for destination and source before we start messing with adding other files.
+        """
+
+        if self.request is None:
+            self.destquota = None
+        else:
+            self.destquota = (yield self.destination.quota(self.request))
+            if self.destquota is not None and self.destination.exists():
+                self.old_dest_size = (yield self.destination.quotaSize(self.request))
+            else:
+                self.old_dest_size = 0
+            
+        if self.request is None:
+            self.sourcequota = None
+        elif self.source is not None:
+            self.sourcequota = (yield self.source.quota(self.request))
+            if self.sourcequota is not None and self.source.exists():
+                self.old_source_size = (yield self.source.quotaSize(self.request))
+            else:
+                self.old_source_size = 0
+        else:
+            self.sourcequota = None
+            self.old_source_size = 0
+
+        returnValue(None)
+
+    def setupRollback(self):
+        """
+        We may need to restore the original resource data if the PUT/COPY/MOVE fails,
+        so rename the original file in case we need to rollback.
+        """
+
+        self.rollback = StoreAddressObjectResource.RollbackState(self)
+        self.overwrite = self.destination.exists()
+        if self.overwrite:
+            self.rollback.destination_copy = FilePath(self.destination.fp.path)
+            self.rollback.destination_copy.path += ".rollback"
+            copyToWithXAttrs(self.destination.fp, self.rollback.destination_copy)
+            log.debug("Rollback: backing up destination %s to %s" % (self.destination.fp.path, self.rollback.destination_copy.path))
+        else:
+            self.rollback.destination_created = True
+            log.debug("Rollback: will create new destination %s" % (self.destination.fp.path,))
+
+        if self.deletesource:
+            self.rollback.source_copy = FilePath(self.source.fp.path)
+            self.rollback.source_copy.path += ".rollback"
+            copyToWithXAttrs(self.source.fp, self.rollback.source_copy)
+            log.debug("Rollback: backing up source %s to %s" % (self.source.fp.path, self.rollback.source_copy.path))
+
+    @inlineCallbacks
+    def doStore(self):
+        # Do put or copy based on whether source exists
+        if self.source is not None:
+            response = maybeDeferred(copyWithXAttrs, self.source.fp, self.destination.fp, self.destination_uri)
+        else:
+            if self.vcarddata is None:
+                self.vcarddata = str(self.vcard)
+            md5 = MD5StreamWrapper(MemoryStream(self.vcarddata))
+            response = maybeDeferred(putWithXAttrs, md5, self.destination.fp)
+        response = (yield response)
+
+        # Update the MD5 value on the resource
+        if self.source is not None:
+            # Copy MD5 value from source to destination
+            if self.source.hasDeadProperty(TwistedGETContentMD5):
+                md5 = self.source.readDeadProperty(TwistedGETContentMD5)
+                self.destination.writeDeadProperty(md5)
+        else:
+            # Finish MD5 calculation and write dead property
+            md5.close()
+            md5 = md5.getMD5()
+            self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+    
+        returnValue(IResponse(response))
+
+    @inlineCallbacks
+    def doSourceDelete(self):
+        # Delete index for original item
+        if self.sourceadbk:
+            self.source_index.deleteResource(self.source.fp.basename())
+            self.rollback.source_index_deleted = True
+            log.debug("Source index removed %s" % (self.source.fp.path,))
+
+        # Delete the source resource
+        delete(self.source_uri, self.source.fp, "0")
+        self.rollback.source_deleted = True
+        log.debug("Source removed %s" % (self.source.fp.path,))
+
+        # Update quota
+        if self.sourcequota is not None:
+            delete_size = 0 - self.old_source_size
+            yield self.source.quotaSizeAdjust(self.request, delete_size)
+
+        # Change CTag on the parent vcard collection
+        if self.sourceadbk:
+            yield self.sourceparent.updateCTag()
+  
+        returnValue(None)
+
+    @inlineCallbacks
+    def doDestinationQuotaCheck(self):
+        # Get size of new/old resources
+        new_dest_size = (yield self.destination.quotaSize(self.request))
+
+        diff_size = new_dest_size - self.old_dest_size
+
+        if diff_size >= self.destquota[0]:
+            log.err("Over quota: available %d, need %d" % (self.destquota[0], diff_size))
+            raise HTTPError(ErrorResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded")))
+        yield self.destination.quotaSizeAdjust(self.request, diff_size)
+
+        returnValue(None)
+
+    def doSourceIndexRecover(self):
+        """
+        Do source resource indexing. This only gets called when restoring
+        the source after its index has been deleted.
+        
+        @return: None if successful, ErrorResponse on failure
+        """
+        
+        # Add or update the index for this resource.
+        try:
+            self.source_index.addResource(self.source.fp.basename(), self.vcard)
+        except TooManyInstancesError, ex:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                    NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
+                ))
+
+            self.source.writeDeadProperty(davxml.GETContentType.fromString("text/vcard"))
+            return None
+
+    def doDestinationIndex(self, vcardtoindex):
+        """
+        Do destination resource indexing, replacing any index previous stored.
+        
+        @return: None if successful, ErrorResponse on failure
+        """
+        
+        # Delete index for original item
+        if self.overwrite:
+            self.doRemoveDestinationIndex()
+        
+        # Add or update the index for this resource.
+        try:
+            self.destination_index.addResource(self.destination.fp.basename(), vcardtoindex)
+            log.debug("Destination indexed %s" % (self.destination.fp.path,))
+        except (ValueError, TypeError), ex:
+            log.err("Cannot index vcard resource: %s" % (ex,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "valid-address-data")))
+
+        self.destination.writeDeadProperty(davxml.GETContentType.fromString("text/vcard"))
+        return None
+
+    def doRemoveDestinationIndex(self):
+        """
+        Remove any existing destination index.
+        """
+        
+        # Delete index for original item
+        if self.destinationadbk:
+            self.destination_index.deleteResource(self.destination.fp.basename())
+            self.rollback.destination_index_deleted = True
+            log.debug("Destination index removed %s" % (self.destination.fp.path,))
+
+    @inlineCallbacks
+    def run(self):
+        """
+        Function that does common PUT/COPY/MOVE behavior.
+
+        @return: a Deferred with a status response result.
+        """
+
+        try:
+            reservation = None
+            
+            # Handle all validation operations here.
+            yield self.fullValidation()
+
+            # Reservation and UID conflict checking is next.
+            if self.destinationadbk:    
+                # Reserve UID
+                self.destination_index = self.destinationparent.index()
+                reservation = StoreAddressObjectResource.UIDReservation(self.destination_index, self.uid, self.destination_uri)
+                if self.indexdestination:
+                    yield reservation.reserve()
+            
+                # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
+                # try to write the same vcard data to two different resource URIs.
+                result, message, rname = self.noUIDConflict(self.uid)
+                if not result:
+                    log.err(message)
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN,
+                        NoUIDConflict(davxml.HRef.fromString(joinURL(parentForURL(self.destination_uri), rname.encode("utf-8"))))
+                    ))
+            
+            # Get current quota state.
+            yield self.checkQuota()
+
+            # Initialize the rollback system
+            self.setupRollback()
+
+            """
+            Handle actual store operations here.
+            
+            The order in which this is done is import:
+                
+                1. Do store operation for new data
+                2. Delete source and source index if needed
+                3. Do new indexing if needed
+                
+            Note that we need to remove the source index BEFORE doing the destination index to cover the
+            case of a resource being 'renamed', i.e. moved within the same collection. Since the index UID
+            column must be unique in SQL, we cannot add the new index before remove the old one.
+            """
+    
+            # Do the actual put or copy
+            response = (yield self.doStore())
+            
+            # Delete the original source if needed.
+            if self.deletesource:
+                yield self.doSourceDelete()
+    
+            # Index the new resource if storing to a vcard.
+            if self.destinationadbk:
+                result = self.doDestinationIndex(self.vcard)
+                if result is not None:
+                    self.rollback.Rollback()
+                    returnValue(result)
+    
+            # Do quota check on destination
+            if self.destquota is not None:
+                yield self.doDestinationQuotaCheck()
+    
+            if self.destinationadbk and self.updatedestinationctag:
+                # Change CTag on the parent vcard collection
+                yield self.destinationparent.updateCTag()
+    
+            # Can now commit changes and forget the rollback details
+            self.rollback.Commit()
+    
+            if reservation:
+                yield reservation.unreserve()
+    
+            returnValue(response)
+    
+        except Exception, err:
+            if reservation:
+                yield reservation.unreserve()
+    
+            # Roll back changes to original server state. Note this may do nothing
+            # if the rollback has already occurred or changes already committed.
+            if self.rollback:
+                self.rollback.Rollback()
+
+            raise err

Copied: CalendarServer/trunk/twistedcaldav/method/report_addressbook_multiget.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/method/report_addressbook_multiget.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_addressbook_multiget.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/method/report_addressbook_multiget.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,316 @@
+##
+# Copyright (c) 2006-2009 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.
+##
+
+"""
+CardDAV multiget report
+"""
+
+__all__ = ["report_urn_ietf_params_xml_ns_carddav_addressbook_multiget"]
+
+import time
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python import log
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.element.base import dav_namespace
+from twisted.web2.dav.http import ErrorResponse, MultiStatusResponse
+from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError, StatusResponse
+
+from twistedcaldav import carddavxml
+from twistedcaldav.config import config
+from twistedcaldav.carddavxml import carddav_namespace
+from twistedcaldav.method import report_common
+
+from urllib import unquote
+
+
+ at inlineCallbacks
+def report_urn_ietf_params_xml_ns_carddav_addressbook_multiget(self, request, multiget):
+    """
+    Generate a multiget REPORT.
+    (CardDAV, section 8.7)
+    """
+
+    startTime = time.time()
+
+    # Verify root element
+    if multiget.qname() != (carddav_namespace, "addressbook-multiget"):
+        raise ValueError("{CardDAV:}addressbook-multiget expected as root element, not %s." % (multiget.sname(),))
+
+    # Make sure target resource is of the right type
+    if not self.isCollection():
+        parent = (yield self.locateParent(request, request.uri))
+        if not parent.isAddressBookCollection():
+            log.err("addressbook-multiget report is not allowed on a resource outside of an address book collection %s" % (self,))
+            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Must be address book resource"))
+
+    responses = []
+
+    propertyreq = multiget.property
+    resources  = multiget.resources
+    
+    if propertyreq.qname() == ("DAV:", "allprop"):
+        propertiesForResource = report_common.allPropertiesForResource
+
+    elif propertyreq.qname() == ("DAV:", "propname"):
+        propertiesForResource = report_common.propertyNamesForResource
+
+    elif propertyreq.qname() == ("DAV:", "prop"):
+        propertiesForResource = report_common.propertyListForResource
+        
+        # Verify that any address-data element matches what we can handle
+        result, message, _ignore = report_common.validPropertyListAddressDataTypeVersion(propertyreq)
+        if not result:
+            log.err(message)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "supported-address-data")))
+    else:
+        raise AssertionError("We shouldn't be here")
+
+    # Check size of results is within limit
+    if len(resources) > config.MaxAddressBookMultigetHrefs:
+        log.err("Too many results in multiget report: %d" % len(resources))
+        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (dav_namespace, "number-of-matches-within-limits")))
+
+    """
+    Three possibilities exist:
+        
+        1. The request-uri is an address book collection, in which case all the hrefs
+        MUST be one-level below that collection and must be address book object resources.
+        
+        2. The request-uri is a regular collection, in which case all the hrefs
+        MUST be children of that (at any depth) but MUST also be address book object
+        resources (i.e. immediate child of an address book collection).
+        
+        3. The request-uri is a resource, in which case there MUST be
+        a single href equal to the request-uri, and MUST be an address book
+        object resource.
+    """
+
+    disabled = False
+    if self.isAddressBookCollection():
+        requestURIis = "addressbook"
+
+        # Do some optimisation of access control calculation by determining any inherited ACLs outside of
+        # the child resource loop and supply those to the checkPrivileges on each child.
+        filteredaces = (yield self.inheritedACEsforChildren(request))
+    
+        # Check for disabled access
+        if filteredaces is None:
+            disabled = True
+
+    elif self.isCollection():
+        requestURIis = "collection"
+        filteredaces = None
+        lastParent = None
+    else:
+        requestURIis = "resource"
+        filteredaces = None
+
+    if not disabled:
+        
+        @inlineCallbacks
+        def doAddressBookResponse():
+            
+            directoryAddressBookLock = None
+            try: 
+                # for directory address book, get requested resources
+                done = False
+                if self.isDirectoryBackedAddressBookCollection() and self.directory.liveQuery:
+                    
+                    # Verify that requested resources are immediate children of the request-URI
+                    # and get vCardFilters ;similar to "normal" case below but do not call getChild()
+                    vCardFilters = []
+                    valid_hrefs = []
+                    for href in resources:
+                        resource_uri = str(href)
+                        resource_name = unquote(resource_uri[resource_uri.rfind("/") + 1:])
+                        if self._isChildURI(request, resource_uri) and resource_name.endswith(".vcf") and len(resource_name) > 4:
+                            valid_hrefs.append(href)
+                            vCardFilters.append(carddavxml.PropertyFilter(
+                                                    carddavxml.TextMatch.fromString(resource_name[:-4]), 
+                                                    name="UID", # attributes
+                                                ))
+                        elif not self.directory.cacheQuery:
+                            responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
+                           
+                    # exit if not valid           
+                    if not vCardFilters or not valid_hrefs:
+                        returnValue( None )
+                         
+                    addressBookFilter = carddavxml.Filter( *vCardFilters )
+                    if self.directory.cacheQuery:
+                        # add vcards to directory address book and run "normal case" below
+                        limit = 0 #config.MaxAddressBookMultigetHrefs
+                        directoryAddressBookLock, limited = (yield  self.directory.cacheVCardsForAddressBookQuery(addressBookFilter, propertyreq, limit) )
+                        if limited:
+                            log.err("Too many results in multiget report: %d" % len(resources))
+                            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (dav_namespace, "number-of-matches-within-limits")))
+                    else:
+                        #get vCards and filter
+                        limit = 0 #config.MaxAddressBookMultigetHrefs
+                        vCardRecords, limited = (yield self.directory.vCardRecordsForAddressBookQuery( addressBookFilter, propertyreq, limit ))
+                        if limited:
+                            log.err("Too many results in multiget report: %d" % len(resources))
+                            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (dav_namespace, "number-of-matches-within-limits")))
+                       
+                        for href in valid_hrefs:
+                            matchingRecord = None
+                            for vCardRecord in vCardRecords:
+                                if href == vCardRecord.hRef(): # might need to compare urls instead - also case sens ok?
+                                    matchingRecord = vCardRecord
+                                    break;
+
+                            if matchingRecord:
+                                yield report_common.responseForHrefAB(request, responses, href, matchingRecord, propertiesForResource, propertyreq, vcard=matchingRecord.vCard())
+                            else:
+                                responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
+                        # done with live, noncaching directoryBackedAddressBook query
+                        done = True
+                        
+                if not done: 
+                    # "normal" case
+                    
+                    # Verify that requested resources are immediate children of the request-URI
+                    valid_names = []
+                    for href in resources:
+                        resource_uri = str(href)
+                        name = unquote(resource_uri[resource_uri.rfind("/") + 1:])
+                        if not self._isChildURI(request, resource_uri) or self.getChild(name) is None:
+                            responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
+                        else:
+                            valid_names.append(name)
+                    if not valid_names:
+                        returnValue( None )
+                
+                    # Verify that valid requested resources are address book objects
+                    exists_names = tuple(self.index().resourcesExist(valid_names))
+                    checked_names = []
+                    for name in valid_names:
+                        if name not in exists_names:
+                            href = davxml.HRef.fromString(joinURL(request.uri, name))
+                            responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
+                        else:
+                            checked_names.append(name)
+                    if not checked_names:
+                        returnValue( None )
+                    
+                    # Now determine which valid resources are readable and which are not
+                    ok_resources = []
+                    bad_resources = []
+                    yield self.findChildrenFaster(
+                        "1",
+                        request,
+                        lambda x, y: ok_resources.append((x, y)),
+                        lambda x, y: bad_resources.append((x, y)),
+                        checked_names,
+                        (davxml.Read(),),
+                        inherited_aces=filteredaces
+                    )
+        
+                    # Get properties for all valid readable resources
+                    for resource, href in ok_resources:
+                        yield report_common.responseForHrefAB(request, responses, davxml.HRef.fromString(href), resource, propertiesForResource, propertyreq)
+                                
+                    # Indicate error for all valid non-readable resources
+                    for ignore_resource, href in bad_resources: #@UnusedVariable
+                        responses.append(davxml.StatusResponse(davxml.HRef.fromString(href), davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
+            
+            finally:
+                if directoryAddressBookLock:
+                    yield directoryAddressBookLock.release()
+    
+
+        if requestURIis == "addressbook":
+            yield doAddressBookResponse()
+        else:
+            for href in resources:
+    
+                resource_uri = str(href)
+    
+                # Do href checks
+                if requestURIis == "addressbook":
+                    pass
+        
+                # TODO: we can optimize this one in a similar manner to the address book case
+                elif requestURIis == "collection":
+                    name = unquote(resource_uri[resource_uri.rfind("/") + 1:])
+                    if not self._isChildURI(request, resource_uri, False):
+                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
+                        continue
+     
+                    child = (yield request.locateResource(resource_uri))
+    
+                    if not child or not child.exists():
+                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
+                        continue
+    
+                    parent = (yield child.locateParent(request, resource_uri))
+    
+                    if not parent.isAddressBookCollection() or not parent.index().resourceExists(name):
+                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
+                        continue
+                    
+                    # Check privileges on parent - must have at least DAV:read
+                    try:
+                        yield parent.checkPrivileges(request, (davxml.Read(),))
+                    except AccessDeniedError:
+                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
+                        continue
+                    
+                    # Cache the last parent's inherited aces for checkPrivileges optimization
+                    if lastParent != parent:
+                        lastParent = parent
+                
+                        # Do some optimisation of access control calculation by determining any inherited ACLs outside of
+                        # the child resource loop and supply those to the checkPrivileges on each child.
+                        filteredaces = (yield parent.inheritedACEsforChildren(request))
+        
+                else:
+                    name = unquote(resource_uri[resource_uri.rfind("/") + 1:])
+                    if (resource_uri != request.uri) or not self.exists():
+                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
+                        continue
+    
+                    parent = (yield self.locateParent(request, resource_uri))
+    
+                    if not parent.isAddressBookCollection() or not parent.index().resourceExists(name):
+                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
+                        continue
+                    child = self
+            
+                    # Do some optimisation of access control calculation by determining any inherited ACLs outside of
+                    # the child resource loop and supply those to the checkPrivileges on each child.
+                    filteredaces = (yield parent.inheritedACEsforChildren(request))
+        
+                # Check privileges - must have at least DAV:read
+                try:
+                    yield child.checkPrivileges(request, (davxml.Read(),), inherited_aces=filteredaces)
+                except AccessDeniedError:
+                    responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
+                    continue
+        
+                yield report_common.responseForHrefAB(request, responses, href, child, propertiesForResource, propertyreq)
+
+    retValue = MultiStatusResponse(responses)
+    
+    elaspedTime = time.time() - startTime
+    self.log_info("Timing: CARDDAV:addressbook-multiget Report total: %.1f ms" % (elaspedTime*1000,))
+
+    returnValue(retValue)
+

Copied: CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/method/report_addressbook_query.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,295 @@
+##
+# Copyright (c) 2006-2009 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.
+##
+
+"""
+CardDAV addressbook-query report
+"""
+
+__all__ = ["report_urn_ietf_params_xml_ns_carddav_addressbook_query"]
+
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue, maybeDeferred
+from twisted.python import log
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse, MultiStatusResponse
+from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
+from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError, StatusResponse
+
+from twistedcaldav import carddavxml
+from twistedcaldav.config import config
+from twistedcaldav.carddavxml import carddav_namespace, NResults
+from twistedcaldav.method import report_common
+
+import urllib
+import time
+
+ at inlineCallbacks
+def report_urn_ietf_params_xml_ns_carddav_addressbook_query(self, request, addressbook_query):
+    """
+    Generate an addressbook-query REPORT.
+    (CardDAV, section 8.6)
+    """
+    startTime = time.time()
+    #print("Timing: report_urn_ietf_params_xml_ns_carddav_addressbook_query.starttime=%f" % startTime)
+    
+    # Verify root element
+    if addressbook_query.qname() != (carddav_namespace, "addressbook-query"):
+        raise ValueError("{CardDAV:}addressbook-query expected as root element, not %s." % (addressbook_query.sname(),))
+
+    if not self.isCollection():
+        parent = (yield self.locateParent(request, request.uri))
+        if not parent.isAddressBookCollection():
+            log.err("addressbook-query report is not allowed on a resource outside of an address book collection %s" % (self,))
+            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Must be address book collection or address book resource"))
+
+    responses = []
+
+    filter = addressbook_query.filter
+    query  = addressbook_query.query
+    limit = addressbook_query.limit
+
+    assert query is not None
+    
+    if query.qname() == ("DAV:", "allprop"):
+        propertiesForResource = report_common.allPropertiesForResource
+        generate_address_data = False
+
+    elif query.qname() == ("DAV:", "propname"):
+        propertiesForResource = report_common.propertyNamesForResource
+        generate_address_data = False
+
+    elif query.qname() == ("DAV:", "prop"):
+        propertiesForResource = report_common.propertyListForResource
+       
+        # Verify that any address-data element matches what we can handle
+        result, message, generate_address_data = report_common.validPropertyListAddressDataTypeVersion(query)
+        if not result:
+            log.err(message)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "supported-address-data")))
+        
+    else:
+        raise AssertionError("We shouldn't be here")
+
+    # Verify that the filter element is valid
+    if (filter is None) or not filter.valid():
+        log.err("Invalid filter element: %r" % (filter,))
+        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "valid-filter")))
+
+    matchcount = [0,]
+    max_number_of_results = [config.MaxAddressBookQueryResults,]
+    limited = [False,]
+    
+    if limit:
+        clientLimit = int(str(limit.childOfType(NResults)))
+        if clientLimit < max_number_of_results[0]:
+            max_number_of_results[0] = clientLimit
+
+    @inlineCallbacks
+    def doQuery(addrresource, uri):
+        """
+        Run a query on the specified address book collection
+        accumulating the query responses.
+        @param addrresource: the L{CalDAVFile} for an address book collection.
+        @param uri: the uri for the address book collecton resource.
+        """
+        
+        def checkMaxResults():
+            matchcount[0] += 1
+            if matchcount[0] > max_number_of_results[0]:
+                raise NumberOfMatchesWithinLimits(max_number_of_results[0])
+           
+        
+        def queryAddressBookObjectResource(resource, uri, name, vcard, query_ok = False):
+            """
+            Run a query on the specified vcard.
+            @param resource: the L{CalDAVFile} for the vcard.
+            @param uri: the uri of the resource.
+            @param name: the name of the resource.
+            @param vcard: the L{Component} vcard read from the resource.
+            """
+            
+            if query_ok or filter.match(vcard):
+                # Check size of results is within limit
+                checkMaxResults()
+
+                if name:
+                    href = davxml.HRef.fromString(joinURL(uri, name))
+                else:
+                    href = davxml.HRef.fromString(uri)
+            
+                return report_common.responseForHrefAB(request, responses, href, resource, propertiesForResource, query, vcard=vcard)
+            else:
+                return succeed(None)
+            
+                                
+            
+        @inlineCallbacks
+        def queryDirectoryBackedAddressBook(directoryBackedAddressBook, addressBookFilter):
+            """
+            """
+            records, limited[0] = (yield directoryBackedAddressBook.directory.vCardRecordsForAddressBookQuery( addressBookFilter, query, max_number_of_results[0] ))
+            for vCardRecord in records:
+                
+                # match against original filter
+                if filter.match(vCardRecord.vCard()):
+ 
+                    # Check size of results is within limit
+                    checkMaxResults()
+                   
+                    yield report_common.responseForHrefAB(request, responses, vCardRecord.hRef(), vCardRecord, propertiesForResource, query, vcard=vCardRecord.vCard())
+ 
+ 
+            
+        directoryAddressBookLock = None
+        try:                
+
+        #self.maxDSQueryRecords and len(queryResults) >= self.maxDSQueryRecords
+ 
+            if addrresource.isDirectoryBackedAddressBookCollection() and addrresource.directory.cacheQuery:
+                
+                directory = addrresource.directory
+                if directory.liveQuery:
+                    # if liveQuery and cacheQuery, get vCards into the directory address book on disk
+                    directoryAddressBookLock, limited[0] = (yield  directory.cacheVCardsForAddressBookQuery( filter, query, max_number_of_results[0] ) )
+ 
+                elif directory.maxDSQueryRecords and directory.maxDSQueryRecords < max_number_of_results[0]:
+                    max_number_of_results[0] = directory.maxDSQueryRecords
+                   
+                
+            elif not addrresource.isAddressBookCollection():
+ 
+                #do UID lookup on last part of uri
+                resource_name = urllib.unquote(uri[uri.rfind("/") + 1:])
+                if resource_name.endswith(".vcf") and len(resource_name) > 4:
+    
+                    # see if parent is directory backed address book
+                    parent = (yield  addrresource.locateParent( request, uri ) )
+    
+                    if parent.isDirectoryBackedAddressBookCollection() and parent.directory.cacheQuery:
+                        
+                        directory = parent.directory
+                        if directory.liveQuery:
+                            vCardFilter = carddavxml.Filter( *[carddavxml.PropertyFilter(
+                                                        carddavxml.TextMatch.fromString(resource_name[:-4]), 
+                                                        name="UID", # attributes
+                                                        ),] )
+                            
+                            directoryAddressBookLock, limited[0] = (yield  directory.cacheVCardsForAddressBookQuery( vCardFilter, query, max_number_of_results[0] ) )
+
+                        elif directory.maxDSQueryRecords and directory.maxDSQueryRecords < max_number_of_results[0]:
+                            max_number_of_results[0] = directory.maxDSQueryRecords
+   
+    
+        
+            # Check whether supplied resource is an address book or an address book object resource
+            if addrresource.isAddressBookCollection():
+    
+                if addrresource.isDirectoryBackedAddressBookCollection() and addrresource.directory.liveQuery and not addrresource.directory.cacheQuery:
+                    yield  maybeDeferred( queryDirectoryBackedAddressBook, addrresource, filter )
+                
+                else:
+
+                    # Do some optimisation of access control calculation by determining any inherited ACLs outside of
+                    # the child resource loop and supply those to the checkPrivileges on each child.
+                    filteredaces = (yield addrresource.inheritedACEsforChildren(request))
+                
+                    # Check for disabled access
+                    if filteredaces is not None:
+                        # See whether the filter is valid for an index only query
+                        index_query_ok = addrresource.index().searchValid(filter)
+                    
+                        # Get list of children that match the search and have read access
+                        names = [name for name, ignore_uid in addrresource.index().search(filter)] #@UnusedVariable
+                        if not names:
+                            return
+                          
+                        # Now determine which valid resources are readable and which are not
+                        ok_resources = []
+                        yield addrresource.findChildrenFaster(
+                            "1",
+                            request,
+                            lambda x, y: ok_resources.append((x, y)),
+                            None,
+                            names,
+                            (davxml.Read(),),
+                            inherited_aces=filteredaces
+                        )
+                        
+                        for child, child_uri in ok_resources:
+                            child_uri_name = child_uri[child_uri.rfind("/") + 1:]
+                            child_path_name = urllib.unquote(child_uri_name)
+                            
+                            if generate_address_data or not index_query_ok:
+                                vcard = addrresource.vCard(child_path_name)
+                                assert vcard is not None, "vCard %s is missing from address book collection %r" % (child_uri_name, self)
+                            else:
+                                vcard = None
+                            
+                            yield queryAddressBookObjectResource(child, uri, child_uri_name, vcard, query_ok = index_query_ok)
+                        
+            else:
+                
+                handled = False;
+                resource_name = urllib.unquote(uri[uri.rfind("/") + 1:])
+                if resource_name.endswith(".vcf") and len(resource_name) > 4:
+                    
+                    # see if parent is directory backed address book
+                    parent = (yield  addrresource.locateParent( request, uri ) )
+    
+                    if parent.isDirectoryBackedAddressBookCollection() and parent.directory.liveQuery and not parent.directory.cacheQuery:
+ 
+                        vCardFilter = carddavxml.Filter( *[carddavxml.PropertyFilter(
+                                                    carddavxml.TextMatch.fromString(resource_name[:-4]), 
+                                                    name="UID", # attributes
+                                                    ),] )
+                        
+                        yield  maybeDeferred( queryDirectoryBackedAddressBook, parent, vCardFilter )
+                        handled = True
+
+                if not handled:
+                    vcard = addrresource.vCard()
+                    yield queryAddressBookObjectResource(addrresource, uri, None, vcard)
+        
+            if limited[0]:
+                raise NumberOfMatchesWithinLimits(matchcount[0])
+
+        finally:
+            if directoryAddressBookLock:
+                yield directoryAddressBookLock.release()
+                
+    
+    # Run report taking depth into account
+    try:
+        depth = request.headers.getHeader("depth", "0")
+        yield report_common.applyToAddressBookCollections(self, request, request.uri, depth, doQuery, (davxml.Read(),))
+    except NumberOfMatchesWithinLimits, e:
+        self.log_info("Too many matching components in addressbook-query report. Limited to %d items" % e.maxLimit())
+        #raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits()))
+        responses.append(davxml.StatusResponse(
+                        davxml.HRef.fromString(request.uri),
+                        davxml.Status.fromResponseCode(responsecode.INSUFFICIENT_STORAGE_SPACE),
+                        davxml.Error(davxml.NumberOfMatchesWithinLimits()),
+                        #davxml.ResponseDescription("Results limited by %s at %d" % resultsWereLimited),
+                        davxml.ResponseDescription("Results limited to %d items" % e.maxLimit()),
+                    ))
+           
+    retValue = MultiStatusResponse(responses)
+    
+    elaspedTime = time.time() - startTime
+    self.log_info("Timing: CARDDAV:addressbook-query Report total: %.1f ms" % (elaspedTime*1000,))
+
+    returnValue(retValue)

Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -16,6 +16,7 @@
 
 __all__ = [
     "applyToCalendarCollections",
+    "applyToAddressBookCollections",
     "responseForHref",
     "allPropertiesForResource",
     "propertyNamesForResource",
@@ -53,6 +54,7 @@
 from twext.log import Logger
 
 from twistedcaldav import caldavxml
+from twistedcaldav import carddavxml
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
 from twistedcaldav.dateops import clipPeriod, normalizePeriodList, timeRangesOverlap
@@ -99,8 +101,45 @@
         if not result:
             break
 
-def responseForHref(request, responses, href, resource, calendar, timezone, propertiesForResource, propertyreq, isowner=True):
+ at inlineCallbacks
+def applyToAddressBookCollections(resource, request, request_uri, depth, apply, privileges):
     """
+    Run an operation on all address book collections, starting at the specified
+    root, to the specified depth. This involves scanning the URI hierarchy
+    down from the root. Return a MultiStatus element of all responses.
+    
+    @param request: the L{IRequest} for the current request.
+    @param resource: the L{CalDAVFile} representing the root to start scanning
+        for address book collections.
+    @param depth: the depth to do the scan.
+    @param apply: the function to apply to each address book collection located
+        during the scan.
+    @param privileges: the privileges that must exist on the address book collection.
+    """
+
+    # First check the privilege on this resource
+    if privileges:
+        try:
+            print ("DeleteResource.applyToAddressBookCollections(1.5)")
+            yield resource.checkPrivileges(request, privileges)
+        except AccessDeniedError:
+            returnValue( None )
+
+    # When scanning we only go down as far as an address book collection - not into one
+    if resource.isAddressBookCollection():
+        resources = [(resource, request_uri)]
+    elif not resource.isCollection():
+        resources = [(resource, request_uri)]
+    else:
+        resources = []
+        yield resource.findCalendarCollections(depth, request, lambda x, y: resources.append((x, y)), privileges = privileges)
+         
+    for addrresource, uri in resources:
+        yield apply(addrresource, uri)
+
+
+def responseForHref(request, responses, href, resource, calendar, timezone, propertiesForResource, propertyreq, isowner=True, vcard=None):
+    """
     Create an appropriate property status response for the given resource.
 
     @param request: the L{IRequest} for the current request.
@@ -110,7 +149,14 @@
     @param calendar: the L{Component} for the calendar for the resource. This may be None
         if the calendar has not already been read in, in which case the resource
         will be used to get the calendar if needed.
-    @param propertiesForResource: the method to use to get the list of properties to return.
+    @param vcard: the L{Component} for the vcard for the resource. This may be None
+        if the vcard has not already been read in, in which case the resource
+        will be used to get the vcard if needed.
+
+    @param propertiesForResource: the method to use to get the list of
+        properties to return.  This is a callable object with a signature
+        matching that of L{allPropertiesForResource}.
+
     @param propertyreq: the L{PropertyContainer} element for the properties of interest.
     @param isowner: C{True} if the authorized principal making the request is the DAV:owner,
         C{False} otherwise.
@@ -131,13 +177,28 @@
         if propstats:
             responses.append(davxml.PropertyStatusResponse(href, *propstats))
 
-    d = propertiesForResource(request, propertyreq, resource, calendar, timezone, isowner)
+    d = propertiesForResource(request, propertyreq, resource, calendar, timezone, vcard, isowner)
     d.addCallback(_defer)
     return d
 
-def allPropertiesForResource(request, prop, resource, calendar=None, timezone=None, isowner=True):
+
+
+def responseForHrefAB(request, responses, href, resource, propertiesForResource,
+                      propertyreq, calendar=None, timezone=None, vcard=None,
+                      isowner=True):
     """
+    Legacy wrapper for compatibility of signature of L{responseForHref} in
+    Contacts Server.
+    """
+    return responseForHref(request, responses, href, resource, calendar,
+                           timezone, propertiesForResource, propertyreq, isowner, vcard)
+
+
+
+def allPropertiesForResource(request, prop, resource, calendar=None, timezone=None, vcard=None, isowner=True):
+    """
     Return all (non-hidden) properties for the specified resource.
+
     @param request: the L{IRequest} for the current request.
     @param prop: the L{PropertyContainer} element for the properties of interest.
     @param resource: the L{CalDAVFile} for the targeted resource.
@@ -145,19 +206,22 @@
         if the calendar has not already been read in, in which case the resource
         will be used to get the calendar if needed.
     @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
+    @param vcard: the L{Component} for the vcard for the resource. This may be None
+        if the vcard has not already been read in, in which case the resource
+        will be used to get the vcard if needed.
     @param isowner: C{True} if the authorized principal making the request is the DAV:owner,
         C{False} otherwise.
     @return: a map of OK and NOT FOUND property values.
     """
 
     def _defer(props):
-        return _namedPropertiesForResource(request, props, resource, calendar, timezone, isowner)
+        return _namedPropertiesForResource(request, props, resource, calendar, timezone, vcard, isowner)
 
     d = resource.listAllprop(request)
     d.addCallback(_defer)
     return d
 
-def propertyNamesForResource(request, prop, resource, calendar=None, timezone=None, isowner=True): #@UnusedVariable
+def propertyNamesForResource(request, prop, resource, calendar=None, timezone=None, vcard=None, isowner=True): #@UnusedVariable
     """
     Return property names for all properties on the specified resource.
     @param request: the L{IRequest} for the current request.
@@ -182,7 +246,7 @@
     d.addCallback(_defer)
     return d
 
-def propertyListForResource(request, prop, resource, calendar=None, timezone=None, isowner=True):
+def propertyListForResource(request, prop, resource, calendar=None, timezone=None, vcard=None, isowner=True):
     """
     Return the specified properties on the specified resource.
     @param request: the L{IRequest} for the current request.
@@ -197,7 +261,7 @@
     @return: a map of OK and NOT FOUND property values.
     """
     
-    return _namedPropertiesForResource(request, prop.children, resource, calendar, timezone, isowner)
+    return _namedPropertiesForResource(request, prop.children, resource, calendar, timezone, vcard, isowner)
 
 def validPropertyListCalendarDataTypeVersion(prop):
     """
@@ -222,8 +286,31 @@
 
     return result, message, generate_calendar_data
 
+def validPropertyListAddressDataTypeVersion(prop):
+    """
+    If the supplied prop element includes an address-data element, verify that
+    the type/version on that matches what we can handle..
+
+    @param prop: the L{PropertyContainer} element for the properties of interest.
+    @return:     a tuple: (True/False if the address-data element is one we can handle or not present,
+                           error message).
+    """
+    
+    result = True
+    message = ""
+    generate_address_data = False
+    for property in prop.children:
+        if isinstance(property, carddavxml.AddressData):
+            if not property.verifyTypeVersion([("text/vcard", "3.0")]):
+                result = False
+                message = "Address-data element type/version not supported: content-type: %s, version: %s" % (property.content_type,property.version)
+            generate_address_data = True
+            break
+
+    return result, message, generate_address_data
+
 @inlineCallbacks
-def _namedPropertiesForResource(request, props, resource, calendar=None, timezone=None, isowner=True):
+def _namedPropertiesForResource(request, props, resource, calendar=None, timezone=None, vcard=None, isowner=True):
     """
     Return the specified properties on the specified resource.
     @param request: the L{IRequest} for the current request.
@@ -233,6 +320,9 @@
         if the calendar has not already been read in, in which case the resource
         will be used to get the calendar if needed.
     @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
+    @param vcard: the L{Component} for the vcard for the resource. This may be None
+        if the vcard has not already been read in, in which case the resource
+        will be used to get the vcard if needed.
     @param isowner: C{True} if the authorized principal making the request is the DAV:owner,
         C{False} otherwise.
     @return: a map of OK and NOT FOUND property values.
@@ -262,6 +352,16 @@
             properties_by_status[responsecode.OK].append(propvalue)
             continue
     
+        if isinstance(property, carddavxml.AddressData):
+            if vcard:
+                propvalue = property.elementFromAddress(vcard)
+            else:
+                propvalue = property.elementFromResource(resource)
+            if propvalue is None:
+                raise ValueError("Invalid CardDAV:address-data for request: %r" % (property,))
+            properties_by_status[responsecode.OK].append(propvalue)
+            continue
+    
         if isinstance(property, WebDAVElement):
             qname = property.qname()
         else:

Copied: CalendarServer/trunk/twistedcaldav/mkcolxml.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/mkcolxml.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/mkcolxml.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/mkcolxml.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,56 @@
+##
+# Copyright (c) 2005-2009 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.
+##
+
+"""
+CalDAV XML Support.
+
+This module provides XML utilities for use with CalDAV.
+
+This API is considered private to static.py and is therefore subject to
+change.
+
+See draft spec: http://ietf.webdav.org/caldav/draft-dusseault-caldav.txt
+"""
+
+from twisted.web2.dav import davxml
+
+##
+# Extended MKCOL objects
+##
+
+mkcol_compliance = (
+    "extended-mkcol",
+)
+
+class MakeCollection (davxml.WebDAVElement):
+    """
+    Top-level element for request body in MKCOL.
+    (Extended-MKCOL, section 5.1)
+    """
+    name = "mkcol"
+
+    allowed_children = { (davxml.dav_namespace, "set"): (0, 1) }
+
+    child_types = { "WebDAVUnknownElement": (0, None) }
+
+class MakeCollectionResponse (davxml.WebDAVElement):
+    """
+    Top-level element for response body in MKCOL.
+    (Extended-MKCOL, section 5.2)
+    """
+    name = "mkcol-response"
+
+    allowed_children = { davxml.WebDAVElement: (0, None) }

Modified: CalendarServer/trunk/twistedcaldav/query/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/__init__.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/query/__init__.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -15,5 +15,5 @@
 ##
 
 """
-CalDAV queries.
+CalDAV and CardDAV queries.
 """

Copied: CalendarServer/trunk/twistedcaldav/query/addressbookquery.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/query/addressbookquery.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/addressbookquery.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/query/addressbookquery.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,126 @@
+##
+# Copyright (c) 2006-2009 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.
+##
+
+"""
+Convert a addressbook-query into an expression tree.
+Convert a addressbook-query into a partial SQL statement.
+"""
+
+__version__ = "0.0"
+
+__all__ = [
+    "addressbookquery",
+    "sqladdressbookquery",
+]
+
+from twistedcaldav.query import sqlgenerator
+from twistedcaldav.query import expression
+from twistedcaldav import carddavxml
+
+# SQL Index column (field) names
+
+FIELD_TYPE      = "RESOURCE.TYPE"
+FIELD_UID       = "RESOURCE.UID"
+
+def addressbookquery(filter):
+    """
+    Convert the supplied addressbook-query into an expression tree.
+
+    @param filter: the L{Filter} for the addressbook-query to convert.
+    @return: a L{baseExpression} for the expression tree.
+    """
+    # Lets assume we have a valid filter from the outset.
+    
+    # Top-level filter contains zero or more prop-filter element
+    if len(filter.children) > 0:
+        return propfilterListExpression(filter.children)
+    else:
+        return expression.allExpression()
+
+def propfilterListExpression(propfilters):
+    """
+    Create an expression for a list of prop-filter elements.
+    
+    @param propfilters: the C{list} of L{ComponentFilter} elements.
+    @return: a L{baseExpression} for the expression tree.
+    """
+    
+    if len(propfilters) == 1:
+        return propfilterExpression(propfilters[0])
+    else:
+        return expression.orExpression([propfilterExpression(c) for c in propfilters])
+
+def propfilterExpression(propfilter):
+    """
+    Create an expression for a single prop-filter element.
+    
+    @param propfilter: the L{PropertyFilter} element.
+    @return: a L{baseExpression} for the expression tree.
+    """
+    
+    # Only handle UID right now
+    if propfilter.filter_name != "UID":
+        raise ValueError
+
+    # Handle is-not-defined case
+    if not propfilter.defined:
+        # Test for <<field>> != "*"
+        return expression.isExpression(FIELD_UID, "", True)
+    
+    # Handle text-match
+    tm = None
+    if propfilter.qualifier and isinstance(propfilter.qualifier, carddavxml.TextMatch):
+        if propfilter.qualifier.negate:
+            tm = expression.notcontainsExpression(propfilter.filter_name, str(propfilter.qualifier), propfilter.qualifier)
+        else:
+            tm = expression.containsExpression(propfilter.filter_name, str(propfilter.qualifier), propfilter.qualifier)
+    
+    # Handle embedded parameters - we do not right now as our Index does not handle them
+    params = []
+    if len(propfilter.filters) > 0:
+        raise ValueError
+    if len(params) > 1:
+        paramsExpression = expression.orExpression[params]
+    elif len(params) == 1:
+        paramsExpression = params[0]
+    else:
+        paramsExpression = None
+
+    # Now build return expression
+    if (tm is not None) and (paramsExpression is not None):
+        return expression.andExpression([tm, paramsExpression])
+    elif tm is not None:
+        return tm
+    elif paramsExpression is not None:
+        return paramsExpression
+    else:
+        return None
+
+def sqladdressbookquery(filter):
+    """
+    Convert the supplied addressbook-query into a partial SQL statement.
+
+    @param filter: the L{Filter} for the addressbook-query to convert.
+    @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+            and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+            Or return C{None} if it is not possible to create an SQL query to fully match the addressbook-query.
+    """
+    try:
+        expression = addressbookquery(filter)
+        sql = sqlgenerator.sqlgenerator(expression)
+        return sql.generate()
+    except ValueError:
+        return None

Copied: CalendarServer/trunk/twistedcaldav/report_addressbook_findshared.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/report_addressbook_findshared.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/report_addressbook_findshared.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/report_addressbook_findshared.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,486 @@
+##
+# Copyright (c) 2006-2009 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.
+##
+
+"""
+AddressBook Server "Find Shared" Address Books report
+Based on addressbook-query report in report_addressbook_query.py
+"""
+
+__all__ = [
+    "http___addressbookserver_org_ns__addressbook_findshared",
+    "getReadWriteSharedAddressBookGroups",
+    "getReadOnlySharedAddressBookGroups",
+    "getWritersGroupForSharedAddressBookGroup",
+]
+
+#import traceback
+import opendirectory
+import dsattributes
+
+from plistlib import readPlist
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python import log
+from twisted.python.filepath import FilePath
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import MultiStatusResponse
+from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError, StatusResponse
+
+from twistedcaldav import customxml
+from twistedcaldav.carddavxml import addressbookserver_namespace, carddav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE as defaultConfigFile
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.directory.appleopendirectory import OpenDirectoryRecord
+from twistedcaldav.resource import isAddressBookCollectionResource
+
+from twistedcaldav.directory.directory import DirectoryService
+
+gLogLocal = 0       # Poor mans logging control for this file only
+
+class AddressBookAccessMode (davxml.WebDAVTextElement):
+    """
+    Access Mode XML element for Address Book "Find Shared" report
+    """
+    name = "current-addressbook-access-mode"
+    namespace = addressbookserver_namespace
+    protected = True
+    
+
+class AddressBookGroupAddressBookInfo (davxml.WebDAVElement):
+    name = "addressbook-info"
+    namespace = addressbookserver_namespace
+    protected = True
+
+    allowed_children = { (davxml.dav_namespace, "href"): (0, None),
+                         (calendarserver_namespace, "getctag"): (0, None),
+                         (davxml.dav_namespace, "displayname"): (0, None),
+                       }
+    
+class AddressBookGroupAddressBooks (davxml.WebDAVElement):
+    """
+    The list (hrefs) of address books contained within a group principal
+    """
+    # Code based on CalendarHomeSet()
+    
+    name = "current-addressbooks-set"
+    namespace = addressbookserver_namespace
+    protected = True
+    
+    allowed_children = { (AddressBookGroupAddressBookInfo.namespace, AddressBookGroupAddressBookInfo.name): (0, None),
+                       }
+    
+    _sharedABFileInfo = None
+    _sharedABDict = None
+    
+
+def getABSharedFileAsDictionary():
+
+    try:
+    # get file path
+        sharedABFilePath = "SharedAddressBooks.plist"
+        if config._configFile:
+            configFilePath = config._configFile
+        else:
+            configFilePath = defaultConfigFile
+        sharedABFilePath = configFilePath[:configFilePath.rfind("/")+1] + sharedABFilePath
+    
+        sharedABFile = FilePath(sharedABFilePath)
+        sharedABFile.restat()
+        fileInfo  = (sharedABFile.getmtime(), sharedABFile.getsize())
+        if fileInfo != AddressBookGroupAddressBooks._sharedABFileInfo:
+            AddressBookGroupAddressBooks._sharedABFileInfo = fileInfo
+            AddressBookGroupAddressBooks._sharedABDict = readPlist(sharedABFilePath)
+            
+            
+    except Exception, e:
+        log.msg("getABSharedFileAsDictionary(): could not read or decode %s: %r" % (sharedABFilePath, e,))
+        AddressBookGroupAddressBooks._sharedABDict = None
+    
+    return AddressBookGroupAddressBooks._sharedABDict
+        
+
+def reloadRecordFromDS(record):
+    # Cause the record to be re-read from DS by forcing a cache reload on it
+    if record == None:
+        return
+        
+    if gLogLocal:
+        log.msg("(Shared Address Book) Reloading record from DS: %s (%s)" % (record.shortNames[0], record.guid));
+    guid = record.guid
+    service = record.service
+    service.reloadCache(record.recordType, lookup=["guid", guid], logIt=False)
+    record = service.recordWithUID(guid)               # reacquire record after cache reload
+    return record
+
+
+def reloadGroupMembersFromDS(groupRecord):
+    # This routine is mainly for purposes of adding the "-writers" group to an ACL.  If the -writers group contains any nested groups, then
+    # make sure that the memberships of those nested groups is up to date
+    # Assumes that groupRecord itself is already current
+    if groupRecord == None:
+        return
+    
+    if gLogLocal:
+        log.msg("(Shared Address Book) Reloading members from DS for record: %s (%s)" % (groupRecord.shortNames[0], groupRecord.guid));
+    
+    visitedGroups = []
+    for m in groupRecord.members():
+        if m.recordType == DirectoryService.recordType_groups:      # only care about refreshing group members - I hope
+            if m.guid in visitedGroups:
+                continue
+            visitedGroups.append(m.guid)
+            m = reloadRecordFromDS(m)                   # refresh the member group
+            reloadGroupMembersFromDS(m)                 # and any of it's children
+
+    if gLogLocal:
+        log.msg("(Shared Address Book) Completed reload of members from DS for record: %s (%s)" % (groupRecord.shortNames[0], groupRecord.guid));
+    
+    
+def getSharedAddressBookSpecialGroup(service, wantGroupName):
+    # Used to find the "ab_readwrite", "ab_readonly" or "xx-writers" groups in the /Local/ node
+ 
+    # Read these directory from DS because DSLocal recrods are not in principals
+ 
+    # We now intentionally force the read to go to DS to make sure we don't have stale data (esp. between processes)
+    if gLogLocal:
+        log.msg("(Shared Address Book) Querying DS for provisioning group: %s" % wantGroupName);
+
+    def _uniqueTupleFromAttribute(attribute):
+        if attribute:
+            if isinstance(attribute, str):
+                return (attribute,)
+            else:
+                s = set()
+                return tuple([(s.add(x), x)[1] for x in attribute if x not in s])
+        else:
+            return ()
+
+    record = None
+    attrs = [
+        dsattributes.kDS1AttrGeneratedUID,
+        dsattributes.kDSNAttrRecordName,
+        dsattributes.kDS1AttrDistinguishedName,
+        dsattributes.kDSNAttrGroupMembers,
+        dsattributes.kDSNAttrNestedGroups,
+        dsattributes.kDSNAttrMetaNodeLocation,
+    ]
+
+    try:
+        localNodeDirectory = opendirectory.odInit("/Local/Default")
+        
+        if gLogLocal:
+            log.msg("(Shared Address Book) opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+            "/Local/Default",
+            dsattributes.kDSNAttrRecordName,
+            wantGroupName,
+            dsattributes.eDSExact,
+            False,
+            dsattributes.kDSStdRecordTypeGroups,
+            attrs,
+        ))
+        results = opendirectory.queryRecordsWithAttribute_list(
+            localNodeDirectory,
+            dsattributes.kDSNAttrRecordName,
+            wantGroupName,
+            dsattributes.eDSExact,
+            False,
+            dsattributes.kDSStdRecordTypeGroups,
+            attrs,
+        )
+    
+        if gLogLocal:
+            log.msg("(Shared Address Book) results= %r" % (results,))
+
+        if len(results) > 0:
+
+            recordShortName, value = results[0] #@UnusedVariable
+
+            memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
+            if memberGUIDs is None:
+                memberGUIDs = ()
+            elif type(memberGUIDs) is str:
+                memberGUIDs = (memberGUIDs,)
+            nestedGUIDs = value.get(dsattributes.kDSNAttrNestedGroups)
+            if nestedGUIDs:
+                if type(nestedGUIDs) is str:
+                    nestedGUIDs = (nestedGUIDs,)
+                memberGUIDs += tuple(nestedGUIDs)
+
+            record = OpenDirectoryRecord(
+                service               = service,
+                recordType            = DirectoryService.recordType_groups,
+                guid                  = value.get(dsattributes.kDS1AttrGeneratedUID),
+                nodeName              = value.get(dsattributes.kDSNAttrMetaNodeLocation),
+                shortNames            = _uniqueTupleFromAttribute(value.get(dsattributes.kDSNAttrRecordName)),
+                authIDs               = (),
+                fullName              = value.get(dsattributes.kDS1AttrDistinguishedName),
+                firstName             = None,
+                lastName              = None,
+                emailAddresses        = (),
+                calendarUserAddresses = (),
+                autoSchedule          = False,
+                enabledForCalendaring = False,
+                memberGUIDs           = memberGUIDs,
+                proxyGUIDs            = (),
+                readOnlyProxyGUIDs    = (),
+            )
+            
+    except opendirectory.ODError, e:
+        #traceback.print_exc()
+        log.err("Open Directory (node=%s) error: %s" % ("/Local/Default", str(e,)))
+    except Exception, e:
+        #traceback.print_exc()
+        log.err("Exception while qerying DS for provisioning group: %s: r" % (wantGroupName, e,))
+    
+    if gLogLocal:
+        log.msg("(Shared Address Book) record= %r" % (record,))
+    return record
+    
+                
+    
+def getSharedAddressBookGroups(service, masterGroupName):
+    record = getSharedAddressBookSpecialGroup(service, masterGroupName)
+    if record == None:
+        return []  # don't return None since callers expect to be able to iterate the results
+    
+    return record.members()
+    
+
+def getReadWriteSharedAddressBookGroups(service):
+    return getSharedAddressBookGroups(service, "com.apple.addressbookserver.sharedABs.readwrite")
+
+
+def getReadOnlySharedAddressBookGroups(service):
+    return getSharedAddressBookGroups(service, "com.apple.addressbookserver.sharedABs.readonly")
+
+
+def getWritersGroupForSharedAddressBookGroup(groupRecord):
+    # Find the "-writers" record object for a given group record
+    # Do not just call:
+    #       writerRecord = self.record.service.recordWithShortName(DirectoryService.recordType_groups, writerRecName)
+    # because that will cause the DS cache to fault looking for the record name if it doesn't exist
+    writerRecName = "com.apple.addressbookserver.sharedABs.writers." + groupRecord.shortNames[0]
+    
+    writersRec = getSharedAddressBookSpecialGroup(groupRecord.service, writerRecName)
+    reloadGroupMembersFromDS(writersRec)        # Make sure all group memberships are up to date
+    
+    return writersRec
+
+    
+def groupRecordContainsMember(aGroup, wantMember):
+    # Does recursive search of aGroup's members, looking for wantMember
+    # Caller is responsible for insuring that "aGroup" is current before calling this routine; we'll make sure to re-read any nested groups
+    
+    if aGroup == None or wantMember == None:
+        return False
+        
+    visitedGroups = []
+    for m in aGroup.members():
+        isGroup = m.recordType == DirectoryService.recordType_groups
+        if isGroup:
+            if m.guid in visitedGroups:
+                continue
+            visitedGroups.append(m.guid)
+        
+        if m.guid == wantMember.guid:
+            return True
+        
+        if isGroup:
+            # Reread the nested group information to make sure it's membership is current
+            m = reloadRecordFromDS(m)
+            if groupRecordContainsMember(m, wantMember):
+                return True
+        
+    return False
+
+    
+def findPrincipalForRecord(rec, principalCollections):
+    for pc in principalCollections:
+        recs = pc.getChild(rec.recordType)
+        if recs:
+            p = recs.principalForRecord(rec)
+            if p:
+                return p
+  
+    return None
+    
+    
+def userIsAddressBookGroupWriter(userRecord, groupRecord):
+    # Check to see if the user is a member of the "-writers" record (if one exists)
+            
+    # Now go after the actual -writer record and insure that one exists in the local node
+    writerRecord = getWritersGroupForSharedAddressBookGroup(groupRecord)
+    if not writerRecord:
+        return False
+        
+    # Check to see if the user is a member of the "-writers" record  
+    return groupRecordContainsMember(writerRecord, userRecord)      # RECURSIVE search!
+    
+
+def filterGroupsForMember(groupList, wantMember):
+    # Check to see which groups in "groupList" wantMember is a member of (recursively)
+    # Will only return the top level groups from groupList, not the actual group that the user is a member of
+    
+    list = []
+    for g in groupList:
+        # "g" could conceivably be a user record but then g.members() will return [] so it shouldn't be necessary to preflight
+        if groupRecordContainsMember(g, wantMember):
+            list.append(g)
+
+    return list
+
+ at inlineCallbacks
+def http___addressbookserver_org_ns__addressbook_findshared(self, request, findshared):
+    """
+    Generate a findshared REPORT.
+    """
+        
+    # Verify root element
+    if findshared.qname() != (addressbookserver_namespace, "addressbook-findshared"):
+        raise ValueError("addressbook-findshared expected as root element, not %s." % (findshared.sname(),))
+
+    # Make sure target resource is of the right type
+    uriResource = yield request.locateResource(request.uri)
+    if uriResource == None:
+        log.err("addressbook-findshared unable to convert request URI to resource: %s" % request.uri)
+        raise HTTPError(StatusResponse(responsecode.NOT_FOUND, "Unable to convert request URI to resource: %s" % request.uri))
+    
+    if uriResource.record.recordType != DirectoryService.recordType_users:
+        log.err("addressbook-findshared request URI is not a user principal: %s" % request.uri)
+        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Request URI is not a user principal: %s" % request.uri))
+
+    #
+    # Reacquire the user record from DS to make sure it's information is up-to-date
+    #
+    userRecord = uriResource.record
+    userRecord = reloadRecordFromDS(userRecord)
+    if userRecord == None:
+        raise HTTPError(StatusResponse(responsecode.NOT_FOUND, "Unable to reload user record from DS cache: %s" % request.uri))
+    
+    uriResource = None      # Invalidate since it contains a reference to a stale "record" instance
+    
+    #
+    # Run the report
+    #
+    responses = []
+    
+    
+    # 
+    # Get the master lists of Address Book-enabled groups  (No group expansion necessary - each group must contain leaf groups that are enabled)
+    #
+    readOnlyGroups = getReadOnlySharedAddressBookGroups(userRecord.service)        # wrap in tuple() if we're going to use more than once since this returns a generator
+    readOnlyGroups = filterGroupsForMember(readOnlyGroups, userRecord)             # keep only those groups that "user" is a member of (checking nested membership)
+    
+    readWriteGroups = getReadWriteSharedAddressBookGroups(userRecord.service)
+    readWriteGroups = filterGroupsForMember(readWriteGroups, userRecord)
+
+    
+    #
+    # Determine which of the groups have address books enabled and what the user's access is to them
+    #
+    processedGroups = []
+    for memberList in (readWriteGroups, readOnlyGroups):        # Make sure to process R/W group access before R/O access
+        isReadWriteGroup = memberList == readWriteGroups
+        for g in memberList:
+            if g.guid in processedGroups:                            # Just in case we have multiple references to the same group, process only once
+                continue
+            processedGroups.append(g.guid)
+ 
+            # Reload the group from DS to make sure it's information is up-to-date
+            g = reloadRecordFromDS(g)
+            if g == None:                   # group disappeared on cache reload
+                continue
+            
+            mode = None
+            if isReadWriteGroup:
+                mode = "ReadWrite"
+            else:
+                mode = "ReadOnly"
+                if userIsAddressBookGroupWriter(userRecord, g):
+                    mode = "ReadWrite"
+    
+            if mode == None:
+                continue
+                    
+            groupPrincipalURL = None
+            groupPrincipal = findPrincipalForRecord(g, self.principalCollections()) 
+            if groupPrincipal:
+                groupPrincipalURL = groupPrincipal.principalURL()
+            
+            
+            abHome = yield groupPrincipal.readProperty((carddav_namespace, "addressbook-home-set"), request)
+            
+            groupDisplayName = yield groupPrincipal.readProperty((davxml.dav_namespace, "displayname"), request)
+            
+            groupUUID = customxml.ResourceID(g.guid)
+            
+            abMode = AddressBookAccessMode(mode)
+    
+            abInfos = []
+            for home in groupPrincipal.addressBookHomeURLs():
+                homeResource = yield request.locateResource(home)
+                for child in homeResource.listChildren():
+                    props = []
+                    childPath = joinURL(homeResource.url(), child)
+                    childResource = yield request.locateResource(childPath)
+                    if childResource and isAddressBookCollectionResource(childResource):
+                        childPath = childPath + "/"                     # Now that we know it's a directory, append the trailing slash
+                        props.append(davxml.HRef(childPath))
+                        
+                        cTag = None
+                        try:
+                            cTag = yield childResource.readProperty((calendarserver_namespace, "getctag"), request)
+                        except:
+                            cTag = None
+                        
+                        if cTag is not None:
+                            props.append(cTag)
+                        
+                        if str(child) == "addressbook":
+                            sharedABFileDictionary = getABSharedFileAsDictionary()
+                            if sharedABFileDictionary:
+                                sharedABDict = sharedABFileDictionary.get("SharedAddressBooks")
+                                if sharedABDict:
+                                    thisGroupsDict = sharedABDict.get(g.guid)
+                                    if thisGroupsDict:
+                                        displayNameString = thisGroupsDict.get("AddressBookName")
+                                        if displayNameString:                        
+                                            displayName = davxml.DisplayName.fromString(displayNameString)
+                                            props.append(displayName)
+
+                        
+                        thisInfo = AddressBookGroupAddressBookInfo(*props)
+                        abInfos.append(thisInfo)
+                            
+            groupAddressBooksProp = AddressBookGroupAddressBooks(*abInfos)
+            #groupAddressBooksProp = AddressBookGroupAddressBooks(*[davxml.HRef(url) for url in groupAddressBooks])                            
+            
+            xml_status      = davxml.Status.fromResponseCode(responsecode.OK)
+            xml_container   = davxml.PropertyContainer(groupDisplayName, groupUUID, abHome, abMode, groupAddressBooksProp)
+            xml_propstat    = davxml.PropertyStatus(xml_container, xml_status)
+            
+            propstats = []
+            propstats.append(xml_propstat)
+    
+            xml_resource = davxml.HRef.fromString(groupPrincipalURL)
+            xml_response = davxml.PropertyStatusResponse(xml_resource, *propstats)
+        
+            responses.append(xml_response)
+
+    returnValue(MultiStatusResponse(responses))
+

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -25,6 +25,9 @@
     "CalendarPrincipalResource",
     "isCalendarCollectionResource",
     "isPseudoCalendarCollectionResource",
+    "isAddressBookCollectionResource",
+    "SearchAddressBookResource",
+    "SearchAllAddressBookResource",
 ]
 
 import urllib
@@ -54,6 +57,8 @@
 
 import twistedcaldav
 from twistedcaldav import caldavxml, customxml
+from twistedcaldav import carddavxml
+from twistedcaldav.carddavxml import carddav_namespace
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
@@ -63,11 +68,13 @@
 from twistedcaldav.ical import Component as iComponent
 from twistedcaldav.ical import allowedComponents
 from twistedcaldav.icaldav import ICalDAVResource, ICalendarPrincipalResource
+from twistedcaldav.vcard import Component as vComponent
 
+
 if twistedcaldav.__version__:
-    serverVersion = twisted.web2.server.VERSION + " TwistedCalDAV/" + twistedcaldav.__version__
+    serverVersion = twisted.web2.server.VERSION + " TwistedCardDAV/" + twistedcaldav.__version__
 else:
-    serverVersion = twisted.web2.server.VERSION + " TwistedCalDAV/?"
+    serverVersion = twisted.web2.server.VERSION + " TwistedCardDAV/?"
 
 class CalDAVComplianceMixIn(object):
 
@@ -83,6 +90,8 @@
         if config.Scheduling.CalDAV.get("EnablePrivateComments", True):
             extra_compliance += customxml.calendarserver_private_comments_compliance
         extra_compliance += customxml.calendarserver_principal_property_search
+        if config.EnableCardDAV:
+            extra_compliance += carddavxml.carddav_compliance
         return tuple(super(CalDAVComplianceMixIn, self).davComplianceClasses()) + extra_compliance
 
 
@@ -440,6 +449,9 @@
     ##
 
     def displayName(self):
+        if self.isAddressBookCollection() and not self.hasDeadProperty((davxml.dav_namespace, "displayname")):
+            return None
+        
         if 'record' in dir(self):
             if self.record.fullName:
                 return self.record.fullName
@@ -460,6 +472,15 @@
         """
         return self.isSpecialCollection(caldavxml.Calendar)
 
+    def isAddressBookCollection(self):
+        """
+        See L{ICalDAVResource.isAddressBookCollection}.
+        """
+        return self.isSpecialCollection(carddavxml.AddressBook)
+
+    def isDirectoryBackedAddressBookCollection(self):       # ATM - temporary fix? (this one worked)
+        return False
+
     def isSpecialCollection(self, collectiontype):
         """
         See L{ICalDAVResource.isSpecialCollection}.
@@ -685,6 +706,65 @@
                 return principal
         return None
 
+    def createAddressBook(self, request):
+        """
+        See L{ICalDAVResource.createAddressBook}.
+        This implementation raises L{NotImplementedError}; a subclass must
+        override it.
+        """
+        unimplemented(self)
+
+    def vCard(self, name=None):
+        """
+        See L{ICalDAVResource.vCard}.
+
+        This implementation returns the an object created from the data returned
+        by L{vCardText} when given the same arguments.
+
+        Note that L{vCardText} by default calls this method, which creates
+        an infinite loop.  A subclass must override one of both of these
+        methods.
+        """
+        vcard_data = self.vCardText(name)
+
+        if vcard_data is None: return None
+
+        try:
+            return vComponent.fromString(vcard_data)
+        except ValueError:
+            return None
+
+    def vCardRolledup(self, request):
+        """
+        See L{ICalDAVResource.vCardRolledup}.
+
+        This implementation raises L{NotImplementedError}; a subclass must
+        override it.
+        """
+        unimplemented(self)
+
+    def vCardText(self, name=None):
+        """
+        See L{ICalDAVResource.vCardText}.
+
+        This implementation returns the string representation (according to
+        L{str}) of the object returned by L{vCard} when given the same
+        arguments.
+
+        Note that L{vCard} by default calls this method, which creates
+        an infinite loop.  A subclass must override one of both of these
+        methods.
+        """
+        return str(self.vCard(name))
+
+    def vCardXML(self, name=None):
+        """
+        See L{ICalDAVResource.vCardXML}.
+        This implementation returns an XML element constructed from the object
+        returned by L{vCard} when given the same arguments.
+        """
+        return carddavxml.AddressData.fromAddress(self.vCard(name))
+
     def supportedReports(self):
         result = super(CalDAVResource, self).supportedReports()
         result.append(davxml.Report(caldavxml.CalendarQuery(),))
@@ -692,6 +772,9 @@
         if self.isCollection():
             # Only allowed on collections
             result.append(davxml.Report(caldavxml.FreeBusyQuery(),))
+        if config.EnableCardDAV:
+            result.append(davxml.Report(carddavxml.AddressBookQuery(),))
+            result.append(davxml.Report(carddavxml.AddressBookMultiGet(),))
         if self.isPseudoCalendarCollection() and config.EnableSyncReport:
             # Only allowed on calendar/inbox collections
             result.append(davxml.Report(SyncCollection(),))
@@ -709,7 +792,7 @@
         """
 
         # Do this only for regular calendar collections and Inbox/Outbox
-        if self.isPseudoCalendarCollection():
+        if self.isPseudoCalendarCollection() or self.isAddressBookCollection():
             edited_aces = []
             for ace in newaces:
                 if TwistedACLInheritable() not in ace.children:
@@ -782,6 +865,12 @@
     def isPseudoCalendarCollection(self):
         return False
 
+    def isAddressBookCollection(self):
+        return False
+
+    def isDirectoryBackedAddressBookCollection(self):
+        return False
+
     def principalForCalendarUserAddress(self, address):
         return None
 
@@ -843,6 +932,14 @@
         elif not enable and qname in clz.liveProperties:
             clz.liveProperties = tuple([p for p in clz.liveProperties if p != qname])
 
+    @classmethod
+    def enableAddressBooks(clz, enable):
+        qname = (carddav_namespace, "addressbook-home-set" )
+        if enable and qname not in clz.liveProperties:
+            clz.liveProperties += (qname,)
+        elif not enable and qname in clz.liveProperties:
+            clz.liveProperties = tuple([p for p in clz.liveProperties if p != qname])
+
     def isCollection(self):
         return True
 
@@ -902,6 +999,11 @@
                 returnValue(customxml.CalendarProxyWriteFor(
                     *[davxml.HRef(principal.principalURL()) for principal in results]
                 ))
+        elif config.EnableCardDAV and namespace == carddav_namespace:
+            if name == "addressbook-home-set":
+                returnValue(carddavxml.AddressBookHomeSet(
+                    *[davxml.HRef(url) for url in self.addressBookHomeURLs()]
+                 ))
 
             elif name == "auto-schedule":
                 autoSchedule = self.getAutoSchedule()
@@ -982,6 +1084,13 @@
         else:
             return None
 
+    def addressBookHomeURLs(self):
+        if self.hasDeadProperty((carddav_namespace, "addressbook-home-set")):
+            home_set = self.readDeadProperty((carddav_namespace, "addressbook-home-set"))
+            return [str(h) for h in home_set.children]
+        else:
+            return ()
+
     ##
     # Quota
     ##
@@ -999,6 +1108,64 @@
         return None
 
 
+
+
+
+class SearchAddressBookResource (CalDAVResource):
+    """
+    Search collection resource.
+    """
+    def __init__(self, parent):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
+
+        CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+        self.parent = parent
+
+    def resourceType(self):
+        return davxml.ResourceType.searchaddressbook #@UndefinedVariable
+
+    def renderHTTP(self, request):
+        return RedirectResponse(request.unparseURL(path="/directory/"))
+
+
+class SearchAllAddressBookResource (CalDAVResource):
+    """
+    Search collection resource.
+    """
+    def __init__(self, parent):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
+
+        CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+        self.parent = parent
+
+    def resourceType(self):
+        return davxml.ResourceType.searchalladdressbook #@UndefinedVariable
+
+    def renderHTTP(self, request):
+        
+        # if requested path ends with "searchall", redirect it for now, redirect to "addressbook"
+        #
+        # in future, should combine all accessible address books
+        
+        matchString = "/searchall"
+        if request.path[-1] == "/":
+            matchString += "/"
+            
+        if request.path.endswith( matchString ):
+            return RedirectResponse(request.unparseURL(path=request.path[:-len(matchString)] + "/addressbook/"))
+        else:
+            return CalDAVResource.renderHTTP(self, request)
+
+
+
 class AuthenticationWrapper(SuperAuthenticationWrapper):
 
     """ AuthenticationWrapper implementation which allows overriding
@@ -1047,4 +1214,11 @@
     else:
         return resource.isPseudoCalendarCollection()
 
+def isAddressBookCollectionResource(resource):
+    try:
+        resource = ICalDAVResource(resource)
+    except TypeError:
+        return False
+    else:
+        return resource.isAddressBookCollection()
 

Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/static.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -32,6 +32,10 @@
     "DropBoxCollectionFile",
     "DropBoxChildFile",
     "TimezoneServiceFile",
+    "AddressBookHomeProvisioningFile",
+    "AddressBookHomeUIDProvisioningFile",
+    "AddressBookHomeFile",
+    "DirectoryBackedAddressBookFile",
 ]
 
 import datetime
@@ -58,6 +62,7 @@
 from twisted.web2.dav.util import parentForURL, bindMethods, joinURL
 
 from twistedcaldav import caldavxml
+from twistedcaldav import carddavxml
 from twistedcaldav import customxml
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.client.reverseproxy import ReverseProxyResource
@@ -71,8 +76,15 @@
 from twistedcaldav.ical import Property as iProperty
 from twistedcaldav.index import Index, IndexSchedule, SyncTokenValidException
 from twistedcaldav.resource import CalDAVResource, isCalendarCollectionResource, isPseudoCalendarCollectionResource
+from twistedcaldav.resource import isAddressBookCollectionResource, SearchAddressBookResource, SearchAllAddressBookResource
 from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource, IScheduleInboxResource
 from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource
+from twistedcaldav.directorybackedaddressbook import DirectoryBackedAddressBookResource
+from twistedcaldav.directory.addressbook import uidsResourceName as uidsResourceNameAddressBook
+from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
+from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeTypeProvisioningResource
+from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeUIDProvisioningResource
+from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeResource
 from twistedcaldav.directory.calendar import uidsResourceName
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeTypeProvisioningResource
@@ -80,6 +92,7 @@
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeResource
 from twistedcaldav.directory.resource import AutoProvisioningResourceMixIn
 from twistedcaldav.timezoneservice import TimezoneServiceResource
+from twistedcaldav.vcardindex import AddressBookIndex
 from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
 from twistedcaldav.notify import getPubSubConfiguration, getPubSubXMPPURI
 from twistedcaldav.notify import getPubSubHeartbeatURI, getPubSubPath
@@ -347,6 +360,95 @@
     def iCalendarXML(self, name=None):
         return caldavxml.CalendarData.fromCalendar(self.iCalendarText(name))
 
+    def createAddressBook(self, request):
+        #
+        # request object is required because we need to validate against parent
+        # resources, and we need the request in order to locate the parents.
+        #
+
+        if self.fp.exists():
+            log.err("Attempt to create collection where file exists: %s" % (self.fp.path,))
+            raise HTTPError(StatusResponse(responsecode.NOT_ALLOWED, "File exists"))
+
+        if not os.path.isdir(os.path.dirname(self.fp.path)):
+            log.err("Attempt to create collection with no parent: %s" % (self.fp.path,))
+            raise HTTPError(StatusResponse(responsecode.CONFLICT, "No parent collection"))
+
+        #
+        # Verify that no parent collection is a calendar also
+        #
+        log.msg("Creating address book collection %s" % (self,))
+
+        def _defer(parent):
+            if parent is not None:
+                log.err("Cannot create an address book collection within an address book collection %s" % (parent,))
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (carddavxml.carddav_namespace, "addressbook-collection-location-ok")
+                ))
+
+            return self.createAddressBookCollection()
+
+        parent = self._checkParents(request, isAddressBookCollectionResource)
+        parent.addCallback(_defer)
+        return parent
+
+    def createAddressBookCollection(self):
+        #
+        # Create the collection once we know it is safe to do so
+        #
+        def onAddressBookCollection(status):
+            if status != responsecode.CREATED:
+                raise HTTPError(status)
+
+            # Initialize CTag on the address book collection
+            d1 = self.updateCTag()
+
+            # Create the index so its ready when the first PUTs come in
+            d1.addCallback(lambda _: self.index().create())
+            d1.addCallback(lambda _: status)
+            return d1
+
+        d = self.createSpecialCollection(davxml.ResourceType.addressbook) #@UndefinedVariable
+        d.addCallback(onAddressBookCollection)
+        return d
+
+    @inlineCallbacks
+    def vCardRolledup(self, request):
+        # TODO: just catenate all the vCards together 
+        yield fail(HTTPError((ErrorResponse(responsecode.BAD_REQUEST))))
+
+    def vCardText(self, name=None):
+        if self.isAddressBookCollection():
+            if name is None:
+                return str(self.vCard())
+
+            try:
+                vcard_file = self.fp.child(name).open()
+            except IOError, e:
+                if e[0] == errno.ENOENT: return None
+                raise
+
+        elif self.isCollection():
+            return None
+
+        else:
+            if name is not None:
+                raise AssertionError("name must be None for non-collection vcard resource")
+
+            vcard_file = self.fp.open()
+
+        # FIXME: This is blocking I/O
+        try:
+            vcard_data = vcard_file.read()
+        finally:
+            vcard_file.close()
+
+        return vcard_data
+
+    def vCardXML(self, name=None):
+        return carddavxml.AddressData.fromAddressData(self.vCardText(name))
+
     def supportedPrivileges(self, request):
         # read-free-busy support on calendar collection and calendar object resources
         if self.isCollection():
@@ -375,7 +477,10 @@
         @raise AssertionError: if this resource is not a calendar collection
             resource.
         """
-        return Index(self)
+        if self.isAddressBookCollection():
+            return AddressBookIndex(self)
+        else:
+            return Index(self)
 
     def whatchanged(self, client_token):
         
@@ -1182,6 +1287,283 @@
     def checkPrivileges(self, request, privileges, recurse=False, principal=None, inherited_aces=None):
         return succeed(None)
 
+class AddressBookHomeProvisioningFile (DirectoryAddressBookHomeProvisioningResource, DAVFile):
+    """
+    Resource which provisions address book home collections as needed.
+    """
+    def __init__(self, path, directory, url):
+        """
+        @param path: the path to the file which will back the resource.
+        @param directory: an L{IDirectoryService} to provision address books from.
+        @param url: the canonical URL for the resource.
+        """
+        DAVFile.__init__(self, path)
+        DirectoryAddressBookHomeProvisioningResource.__init__(self, directory, url)
+
+        # create with permissions
+        try:
+            os.mkdir(path)
+            os.chmod(path, 0750)
+            if config.UserName and config.GroupName:
+                import pwd
+                import grp
+                uid = pwd.getpwnam(config.UserName)[2]
+                gid = grp.getgrnam(config.GroupName)[2]
+                os.chown(path, uid, gid)
+ 
+            log.msg("Created %s" % (path,))
+            
+        except (OSError,), e:
+            # this is caused by multiprocessor race and is harmless
+            if e.errno != errno.EEXIST:
+                raise
+
+
+    def provisionChild(self, name):
+        if name == uidsResourceNameAddressBook:
+            return AddressBookHomeUIDProvisioningFile(self.fp.child(name).path, self)
+
+        return AddressBookHomeTypeProvisioningFile(self.fp.child(name).path, self, name)
+
+    def createSimilarFile(self, path):
+        raise HTTPError(responsecode.NOT_FOUND)
+
+class AddressBookHomeTypeProvisioningFile (AutoProvisioningFileMixIn, DirectoryAddressBookHomeTypeProvisioningResource, DAVFile):
+    def __init__(self, path, parent, recordType):
+        """
+        @param path: the path to the file which will back the resource.
+        @param parent: the parent of this resource
+        @param recordType: the directory record type to provision.
+        """
+        DAVFile.__init__(self, path)
+        DirectoryAddressBookHomeTypeProvisioningResource.__init__(self, parent, recordType)
+
+class AddressBookHomeUIDProvisioningFile (AutoProvisioningFileMixIn, DirectoryAddressBookHomeUIDProvisioningResource, DAVFile):
+    def __init__(self, path, parent, homeResourceClass=None):
+        """
+        @param path: the path to the file which will back the resource.
+        """
+        DAVFile.__init__(self, path)
+        DirectoryAddressBookHomeUIDProvisioningResource.__init__(self, parent)
+        if homeResourceClass is None:
+            self.homeResourceClass = AddressBookHomeFile
+        else:
+            self.homeResourceClass = homeResourceClass
+
+    def provisionChild(self, name):
+        record = self.directory.recordWithUID(name)
+
+        if record is None:
+            log.msg("No directory record with GUID %r" % (name,))
+            return None
+
+        if not record.enabledForAddressBooks:
+            log.msg("Directory record %r is not enabled for address books" % (record,))
+            return None
+
+        assert len(name) > 4
+        
+        childPath = self.fp.child(name[0:2]).child(name[2:4]).child(name)
+        child = self.homeResourceClass(childPath.path, self, record)
+
+        if not child.exists():
+            self.provision()
+
+            if not childPath.parent().isdir():
+                childPath.parent().makedirs()
+
+            for oldPath in (
+                # Pre 2.0: All in one directory
+                self.fp.child(name),
+                # Pre 1.2: In types hierarchy instead of the GUID hierarchy
+                self.parent.getChild(record.recordType).fp.child(record.shortNames[0]),
+            ):
+                if oldPath.exists():
+                    # The child exists at an old location.  Move to new location.
+                    log.msg("Moving address book home from old location %r to new location %r." % (oldPath, childPath))
+                    try:
+                        oldPath.moveTo(childPath)
+                    except (OSError, IOError), e:
+                        log.err("Error moving address book home %r: %s" % (oldPath, e))
+                        raise HTTPError(StatusResponse(
+                            responsecode.INTERNAL_SERVER_ERROR,
+                            "Unable to move address book home."
+                        ))
+                    child.fp.restat(False)
+                    break
+            else:
+                #
+                # NOTE: provisionDefaultAddressBooks() returns a deferred, which we are ignoring.
+                # The result being that the default calendars will be present at some point
+                # in the future, not necessarily right now, and we don't have a way to wait
+                # on that to finish.
+                #
+                child.provisionDefaultAddressBooks()
+
+                #
+                # Try to work around the above a little by telling the client that something
+                # when wrong temporarily if the child isn't provisioned right away.
+                #
+                if not child.exists():
+                    raise HTTPError(StatusResponse(
+                        responsecode.SERVICE_UNAVAILABLE,
+                        "Provisioning address book home."
+                    ))
+
+            assert child.exists()
+
+        return child
+
+    def createSimilarFile(self, path):
+        raise HTTPError(responsecode.NOT_FOUND)
+
+class AddressBookHomeFile (PropfindCacheMixin, AutoProvisioningFileMixIn, DirectoryAddressBookHomeResource, CalDAVFile):
+    """
+    Address book home collection resource.
+    """
+    cacheNotifierFactory = DisabledCacheNotifier
+
+    liveProperties = CalDAVFile.liveProperties + (
+        (customxml.calendarserver_namespace, "xmpp-uri"),
+    )
+
+    def __init__(self, path, parent, record):
+        """
+        @param path: the path to the file which will back the resource.
+        """
+        self.cacheNotifier = self.cacheNotifierFactory(self)
+        self.clientNotifier = ClientNotifier(self)
+        CalDAVFile.__init__(self, path)
+        DirectoryAddressBookHomeResource.__init__(self, parent, record)
+
+    def provisionChild(self, name):
+ 
+        SearchAddressBookFileClass = None
+        SearchAllAddressBookFileClass = None
+        if config.EnableSearchAddressBook and config.DirectoryAddressBook:
+            SearchAddressBookFileClass = SearchAddressBookFile
+        if config.EnableSearchAllAddressBook:
+            SearchAllAddressBookFileClass = SearchAllAddressBookFile
+        
+        cls = {
+            "search"       : SearchAddressBookFileClass,
+            "searchall"    : SearchAllAddressBookFileClass,
+        }.get(name, None)
+
+        if cls is not None:
+            child = cls(self.fp.child(name).path, self)
+            child.cacheNotifier = self.cacheNotifier
+            child.clientNotifier = self.clientNotifier
+            return child
+
+        return self.createSimilarFile(self.fp.child(name).path)
+
+    def createSimilarFile(self, path):
+        if path == self.fp.path:
+            return self
+        else:
+            similar = CalDAVFile(path, principalCollections=self.principalCollections())
+            similar.cacheNotifier = self.cacheNotifier
+            similar.clientNotifier = self.clientNotifier
+            return similar
+
+    def getChild(self, name):
+        # This avoids finding case variants of put children on case-insensitive filesystems.
+        if name not in self.putChildren and name.lower() in (x.lower() for x in self.putChildren):
+            return None
+
+        return super(AddressBookHomeFile, self).getChild(name)
+
+
+    def readProperty(self, property, request):
+        if type(property) is tuple:
+            qname = property
+        else:
+            qname = property.qname()
+
+        if qname == (customxml.calendarserver_namespace, "xmpp-uri"):
+            pubSubConfiguration = getPubSubConfiguration(config)
+            if pubSubConfiguration['enabled']:
+                return succeed(customxml.PubSubXMPPURIProperty(
+                    getPubSubXMPPURI(self.url(), pubSubConfiguration)))
+            else:
+                return succeed(customxml.PubSubXMPPURIProperty())
+
+        return super(AddressBookHomeFile, self).readProperty(property, request)
+
+
+class SearchAddressBookFile (SearchAddressBookResource, CalDAVFile):
+    """
+    Search collection resource.
+    """
+    def __init__(self, path, parent):
+        SearchAddressBookResource.__init__(self, parent)
+        CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+        self.parent = parent
+
+    def createSimilarFile(self, path):
+        if path == self.fp.path:
+            return self
+
+        # FIXME:  path here is similar too .../addressbooks/__uids__/us/er/user01/search/move2_1.vcf
+        #        so we should really redirect to /directory/move2_1.vcf or perhaps we shouldn't be here in the first place
+        raise HTTPError(responsecode.NOT_FOUND)
+
+class SearchAllAddressBookFile (SearchAllAddressBookResource, CalDAVFile):
+    """
+    Search collection resource.
+    """
+    def __init__(self, path, parent):
+        SearchAllAddressBookResource.__init__(self, parent)
+        CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+        self.parent = parent
+   
+    def createSimilarFile(self, path):
+        if path == self.fp.path:
+            return self
+
+        # FIXME:  see note above
+        raise HTTPError(responsecode.NOT_FOUND)
+
+class DirectoryBackedAddressBookFile (DirectoryBackedAddressBookResource, CalDAVFile):
+    """
+    Directory-backed address book, supporting directory vcard search.
+    """
+    def __init__(self, path, principalCollections):
+        CalDAVFile.__init__(self, path, principalCollections=principalCollections)
+        DirectoryBackedAddressBookResource.__init__(self)
+
+        # create with permissions, similar to CardDAVOptions in tap.py
+        # FIXME:  /Directory does not need to be in file system unless debug-only caching options are used
+        try:
+            os.mkdir(path)
+            os.chmod(path, 0750)
+            if config.UserName and config.GroupName:
+                import pwd
+                import grp
+                uid = pwd.getpwnam(config.UserName)[2]
+                gid = grp.getgrnam(config.GroupName)[2]
+                os.chown(path, uid, gid)
+ 
+            log.msg("Created %s" % (path,))
+            
+        except (OSError,), e:
+            # this is caused by multiprocessor race and is harmless
+            if e.errno != errno.EEXIST:
+                raise
+
+    
+    def getChild(self, name):
+        
+        if name is "":
+            return self
+        else:
+            return CalDAVFile(
+                self.fp,
+                principalCollections=self.principalCollections()
+            ).getChild(name)
+       
+ 
 ##
 # Utilities
 ##
@@ -1255,7 +1637,7 @@
                         davxml.SupportedPrivilege(
                             davxml.Privilege(caldavxml.ReadFreeBusy()),
                             davxml.Description("allow free busy report query", **{"xml:lang": "en"}),
-                        ),
+                        )
                     )
                     all_supported_privileges.append(
                         davxml.SupportedPrivilege(read_privilege, read_description, *read_supported_privileges)
@@ -1295,3 +1677,13 @@
 twistedcaldav.method.mkcol.CalDAVFile    = CalDAVFile
 twistedcaldav.method.propfind.CalDAVFile = CalDAVFile
 twistedcaldav.method.put.CalDAVFile      = CalDAVFile
+
+
+#
+# Attach method to DirectoryCalendarPrincipalResource for "Find Shared Address Books" REPORT method
+#
+from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
+from twistedcaldav import report_addressbook_findshared
+setattr(DirectoryCalendarPrincipalResource, "report_http___addressbookserver_org_ns__addressbook_findshared", report_addressbook_findshared.http___addressbookserver_org_ns__addressbook_findshared)
+
+

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2010-02-09 20:49:43 UTC (rev 5080)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -85,6 +85,32 @@
     },
 }
 
+
+directoryAddressBookBackingServiceDefaultParams = {
+    "twistedcaldav.directory.xmlfile.XMLDirectoryService": {
+        "xmlFile": "/etc/carddavd/accounts.xml",
+    },
+    "twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService": {
+        "queryPeopleRecords": True,
+        "peopleNode": "/Search/Contacts",
+        "queryUserRecords": True,
+        "userNode": "/Search",
+        "maxDSQueryRecords":150,
+        "queryDSLocal": False,
+        "ignoreSystemRecords": True,
+        "dsLocalCacheTimeout":30,
+        "liveQuery": True,
+        "fakeETag": True,
+        "cacheQuery": False,
+        "cacheTimeout": 30,
+        "standardizeSyntheticUIDs": False,
+        "addDSAttrXProperties": False,
+        "appleInternalServer": False,
+        "additionalAttributes" : [],
+        "allowedAttributes" : [],
+    },
+}
+
 DEFAULT_CONFIG = {
     # Note: Don't use None values below; that confuses the command-line parser.
 
@@ -115,6 +141,27 @@
     "InheritSSLFDs": [],   # File descriptors to inherit for HTTPS requests (empty = don't inherit)
 
     #
+    # Types of service provided
+    #
+    "EnableCalDAV"  : True,  # Enable CalDAV service
+    "EnableCardDAV" : False,  # Enable CardDAV service
+
+    # XXX CardDAV
+    "DirectoryAddressBook": {
+        "type": "twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService",
+        "params": directoryAddressBookBackingServiceDefaultParams["twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService"],
+    },
+    "AnonymousDirectoryAddressBookAccess": False, # Anonymous users may access directory address book
+
+    "EnableSearchAddressBook": False,
+    "EnableSearchAllAddressBook":False,
+    "MaxAddressBookQueryResults":1000,
+    "MaxAddressBookMultigetHrefs":5000,
+
+    "EnableFindSharedReport":False,
+    # /XXX CardDAV
+
+    #
     # Data store
     #
     "DataRoot"                : "/Library/CalendarServer/Data",
@@ -572,6 +619,44 @@
             if param not in DEFAULT_SERVICE_PARAMS[configDict.DirectoryService.type]:
                 del configDict.DirectoryService.params[param]
 
+
+def _updateAddressBook(configDict):
+    #
+    # FIXME: Use the config object instead of doing this here
+    #
+    from twistedcaldav.resource import CalendarPrincipalResource
+    CalendarPrincipalResource.enableAddressBooks(configDict.EnableCardDAV)
+
+
+
+def _preUpdateDirectoryAddressBookBackingDirectoryService(configDict, items):
+    #
+    # Special handling for directory address book configs
+    #
+    dsType = items.get("DirectoryAddressBook", {}).get("type", None)
+    if dsType is None:
+        dsType = configDict["DirectoryAddressBook"]["type"]
+    else:
+        if dsType == configDict["DirectoryAddressBook"]["type"]:
+            oldParams = configDict["DirectoryAddressBook"]["params"]
+            newParams = items["DirectoryAddressBook"].get("params", {})
+            _mergeData(oldParams, newParams)
+        else:
+            if dsType in directoryAddressBookBackingServiceDefaultParams:
+                configDict["DirectoryAddressBook"]["params"] = copy.deepcopy(directoryAddressBookBackingServiceDefaultParams[dsType])
+            else:
+                configDict["DirectoryAddressBook"]["params"] = {}
+
+    for param in items.get("DirectoryAddressBook", {}).get("params", {}):
+        if param not in directoryAddressBookBackingServiceDefaultParams[dsType]:
+            raise ConfigurationError("Parameter %s is not supported by service %s" % (param, dsType))
+
+    _mergeData(configDict, items)
+
+    for param in tuple(configDict["DirectoryAddressBook"]["params"]):
+        if param not in directoryAddressBookBackingServiceDefaultParams[configDict["DirectoryAddressBook"]["type"]]:
+            del configDict["DirectoryAddressBook"]["params"][param]
+
 def _postUpdateAugmentService(configDict):
     if configDict.AugmentService.type in DEFAULT_AUGMENT_PARAMS:
         for param in tuple(configDict.AugmentService.params):
@@ -731,6 +816,8 @@
                     raise ConfigurationError("Invalid %s for XMPPNotifierService: %r"
                                              % (key, value))
 
+
+
 def _updateScheduling(configDict):
     #
     # Scheduling
@@ -791,9 +878,11 @@
 
 PRE_UPDATE_HOOKS = (
     _preUpdateDirectoryService,
+    _preUpdateDirectoryAddressBookBackingDirectoryService,
     )
 POST_UPDATE_HOOKS = (
     _updateHostName,
+    _updateAddressBook,
     _postUpdateDirectoryService,
     _postUpdateAugmentService,
     _postUpdateProxyDBService,


Property changes on: CalendarServer/trunk/twistedcaldav/test/data
___________________________________________________________________
Modified: svn:ignore
   - calendars
.calendarserver_version

   + 
calendars
.calendarserver_version
directory
addressbooks


Copied: CalendarServer/trunk/twistedcaldav/vcard.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/vcard.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/vcard.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/vcard.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,357 @@
+##
+# Copyright (c) 2005-2009 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.
+##
+
+"""
+vCard Utilities
+"""
+
+__all__ = [
+    "Property",
+    "Component",
+]
+
+import cStringIO as StringIO
+
+from vobject import newFromBehavior, readComponents
+from vobject.base import Component as vComponent
+from vobject.base import ContentLine as vContentLine
+from vobject.base import ParseError as vParseError
+
+from twisted.web2.stream import IStream
+from twisted.web2.dav.util import allDataFromStream
+
+vCardProductID = "-//CALENDARSERVER.ORG//NONSGML Version 1//EN"
+
+class Property (object):
+    """
+    vCard Property
+    """
+    def __init__(self, name, value, params={}, group=None, encoded=False, **kwargs):
+        """
+        @param name: the property's name
+        @param value: the property's value
+        @param params: a dictionary of parameters, where keys are parameter names and
+            values are (possibly empty) lists of parameter values.
+        """
+        if name is None:
+            assert value  is None
+            assert params is None
+
+            vobj = kwargs["vobject"]
+
+            if not isinstance(vobj, vContentLine):
+                raise TypeError("Not a vContentLine: %r" % (property,))
+
+            self._vobject = vobj
+        else:
+            # Convert params dictionary to list of lists format used by vobject
+            lparams = [[key] + lvalue for key, lvalue in params.items()]
+            self._vobject = vContentLine(name, lparams, value, isNative=True, group=group, encoded=encoded )
+
+    def __str__ (self): return self._vobject.serialize()
+    def __repr__(self): return "<%s: %r: %r>" % (self.__class__.__name__, self.name(), self.value())
+
+    def __hash__(self): return hash((self.name(), self.value()))
+
+    def __ne__(self, other): return not self.__eq__(other)
+    def __eq__(self, other):
+        if not isinstance(other, Property): return False
+        return self.name() == other.name() and self.value() == other.value()
+
+    def __gt__(self, other): return not (self.__eq__(other) or self.__lt__(other))
+    def __lt__(self, other):
+        my_name = self.name()
+        other_name = other.name()
+
+        if my_name < other_name: return True
+        if my_name > other_name: return False
+
+        return self.value() < other.value()
+
+    def __ge__(self, other): return self.__eq__(other) or self.__gt__(other)
+    def __le__(self, other): return self.__eq__(other) or self.__lt__(other)
+
+    def name  (self): return self._vobject.name
+
+    def value (self): return self._vobject.value
+    def setValue(self, value):
+        self._vobject.value = value
+
+    def params(self): return self._vobject.params
+
+    def transformAllFromNative(self):
+        transformed = self._vobject.isNative
+        if transformed:
+            self._vobject = self._vobject.transformFromNative()
+            self._vobject.transformChildrenFromNative()
+        return transformed
+        
+    def transformAllToNative(self):
+        transformed = not self._vobject.isNative
+        if transformed:
+            self._vobject = self._vobject.transformToNative()
+            self._vobject.transformChildrenToNative()
+        return transformed
+
+class Component (object):
+    """
+    X{vCard} component.
+    """
+    @classmethod
+    def fromString(clazz, string):
+        """
+        Construct a L{Component} from a string.
+        @param string: a string containing vCard data.
+        @return: a L{Component} representing the first component described by
+            C{string}.
+        """
+        if type(string) is unicode:
+            string = string.encode("utf-8")
+        return clazz.fromStream(StringIO.StringIO(string))
+
+    @classmethod
+    def fromStream(clazz, stream):
+        """
+        Construct a L{Component} from a stream.
+        @param stream: a C{read()}able stream containing vCard data.
+        @return: a L{Component} representing the first component described by
+            C{stream}.
+        """
+        try:
+            return clazz(None, vobject=readComponents(stream).next())
+        except vParseError, e:
+            raise ValueError(e)
+        except StopIteration, e:
+            raise ValueError(e)
+
+    @classmethod
+    def fromIStream(clazz, stream):
+        """
+        Construct a L{Component} from a stream.
+        @param stream: an L{IStream} containing vCard data.
+        @return: a deferred returning a L{Component} representing the first
+            component described by C{stream}.
+        """
+        #
+        # FIXME:
+        #   This reads the request body into a string and then parses it.
+        #   A better solution would parse directly and incrementally from the
+        #   request stream.
+        #
+        def parse(data): return clazz.fromString(data)
+        return allDataFromStream(IStream(stream), parse)
+
+    def __init__(self, name, **kwargs):
+        """
+        Use this constructor to initialize an empty L{Component}.
+        To create a new L{Component} from X{vCard} data, don't use this
+        constructor directly.  Use one of the factory methods instead.
+        @param name: the name (L{str}) of the X{iCalendar} component type for the
+            component.
+        """
+        if name is None:
+            if "vobject" in kwargs:
+                vobj = kwargs["vobject"]
+
+                if vobj is not None:
+                    if not isinstance(vobj, vComponent):
+                        raise TypeError("Not a vComponent: %r" % (vobj,))
+
+                self._vobject = vobj
+            else:
+                raise AssertionError("name may not be None")
+
+            # FIXME: _parent is not use internally, and appears to be used elsewhere,
+            # even though it's names as a private variable.
+            if "parent" in kwargs:
+                parent = kwargs["parent"]
+                
+                if parent is not None:
+                    if not isinstance(parent, Component):
+                        raise TypeError("Not a Component: %r" % (parent,))
+                    
+                self._parent = parent
+            else:
+                self._parent = None
+        else:
+            self._vobject = newFromBehavior(name)
+            self._parent = None
+
+    def __str__ (self): return self._vobject.serialize()
+    def __repr__(self): return "<%s: %r>" % (self.__class__.__name__, str(self._vobject))
+
+    def __hash__(self):
+        return hash(tuple(sorted(self.properties())))
+
+    def __ne__(self, other): return not self.__eq__(other)
+    def __eq__(self, other):
+        if not isinstance(other, Component):
+            return False
+
+        my_properties = set(self.properties())
+        for property in other.properties():
+            if property in my_properties:
+                my_properties.remove(property)
+            else:
+                return False
+        if my_properties:
+            return False
+
+        return True
+
+    # FIXME: Should this not be in __eq__?
+    def same(self, other):
+        return self._vobject == other._vobject
+    
+    def name(self):
+        """
+        @return: the name of the iCalendar type of this component.
+        """
+        return self._vobject.name
+
+    def setBehavior(self, behavior):
+        """
+        Set the behavior of the underlying iCal obtecy.
+        @param behavior: the behavior type to set.
+        """
+        self._vobject.setBehavior(behavior)
+
+    def duplicate(self):
+        """
+        Duplicate this object and all its contents.
+        @return: the duplicated vcard.
+        """
+        return Component(None, vobject=vComponent.duplicate(self._vobject))
+        
+    def hasProperty(self, name):
+        """
+        @param name: the name of the property whose existence is being tested.
+        @return: True if the named property exists, False otherwise.
+        """
+        try:
+            return len(self._vobject.contents[name.lower()]) > 0
+        except KeyError:
+            return False
+
+    def getProperty(self, name):
+        """
+        Get one property from the property list.
+        @param name: the name of the property to get.
+        @return: the L{Property} found or None.
+        @raise: L{ValueError} if there is more than one property of the given name.
+        """
+        properties = tuple(self.properties(name))
+        if len(properties) == 1: return properties[0]
+        if len(properties) > 1: raise ValueError("More than one %s property in component %r" % (name, self))
+        return None
+        
+    def properties(self, name=None):
+        """
+        @param name: if given and not C{None}, restricts the returned properties
+            to those with the given C{name}.
+        @return: an iterable of L{Property} objects, one for each property of
+            this component.
+        """
+        if name is None:
+            properties = self._vobject.getChildren()
+        else:
+            try:
+                properties = self._vobject.contents[name.lower()]
+            except KeyError:
+                return ()
+
+        return (
+            Property(None, None, None, vobject=p)
+            for p in properties
+            if isinstance(p, vContentLine)
+        )
+
+    def propertyValue(self, name):
+        properties = tuple(self.properties(name))
+        if len(properties) == 1: return properties[0].value()
+        if len(properties) > 1: raise ValueError("More than one %s property in component %r" % (name, self))
+        return None
+
+
+    def propertyNativeValue(self, name):
+        """
+        Return the native property value for the named property in the supplied component.
+        NB Assumes a single property exists in the component.
+        @param name: the name of the property whose value is required
+        @return: the native property value
+        """
+        properties = tuple(self.properties(name))
+
+        if len(properties) == 1:
+            transormed = properties[0].transformAllToNative()
+    
+            result = properties[0].value()
+    
+            if transormed:
+                properties[0].transformAllFromNative()
+                
+            return result
+
+        elif len(properties) > 1:
+            raise ValueError("More than one %s property in component %r" % (name, self))
+        else:
+            return None
+
+    def addProperty(self, property):
+        """
+        Adds a property to this component.
+        @param property: the L{Property} to add to this component.
+        """
+        self._vobject.add(property._vobject)
+
+    def removeProperty(self, property):
+        """
+        Remove a property from this component.
+        @param property: the L{Property} to remove from this component.
+        """
+        self._vobject.remove(property._vobject)
+
+    def resourceUID(self):
+        """
+        @return: the UID of the subcomponents in this component.
+        """
+        assert self.name() == "VCARD", "Not a vcard: %r" % (self,)
+
+        if not hasattr(self, "_resource_uid"):
+            self._resource_uid = self.propertyValue("UID")
+
+        return self._resource_uid
+
+    def validForCardDAV(self):
+        """
+        @raise ValueError: if the given vcard data is not valid.
+        """
+        if self.name() != "VCARD": raise ValueError("Not a vcard")
+
+        version = self.propertyValue("VERSION")
+        if version != "3.0": raise ValueError("Not a version 2.0 vCard (version=%s)" % (version,))
+
+        uid = self.propertyValue("UID")
+        if uid is None:
+            raise ValueError("All vCards must have UIDs")
+
+    def transformAllFromNative(self):
+        self._vobject = self._vobject.transformFromNative()
+        self._vobject.transformChildrenFromNative(False)
+        
+    def transformAllToNative(self):
+        self._vobject = self._vobject.transformToNative()
+        self._vobject.transformChildrenToNative()

Copied: CalendarServer/trunk/twistedcaldav/vcardindex.py (from rev 5080, CalendarServer/branches/users/glyph/contacts-server-merge/twistedcaldav/vcardindex.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/vcardindex.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/vcardindex.py	2010-02-09 21:07:27 UTC (rev 5081)
@@ -0,0 +1,541 @@
+##
+# Copyright (c) 2005-2009 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.
+##
+
+"""
+CardDAV Index.
+
+This API is considered private to static.py and is therefore subject to
+change.
+"""
+
+__all__ = [
+    "AddressBookIndex",
+]
+
+import datetime
+import os
+import time
+import hashlib
+
+try:
+    import sqlite3 as sqlite
+except ImportError:
+    from pysqlite2 import dbapi2 as sqlite
+
+from twisted.internet.defer import maybeDeferred
+
+from twistedcaldav import carddavxml
+from twistedcaldav.query import addressbookquery
+from twistedcaldav.sql import AbstractSQLDatabase
+from twistedcaldav.sql import db_prefix
+from twistedcaldav.vcard import Component
+
+from twext.log import Logger, LoggingMixIn
+from twistedcaldav.config import config
+from twistedcaldav.memcachepool import CachePoolUserMixIn
+
+log = Logger()
+
+db_basename = db_prefix + "sqlite"
+schema_version = "1"
+
+class ReservationError(LookupError):
+    """
+    Attempt to reserve a UID which is already reserved or to unreverse a UID
+    which is not reserved.
+    """
+
+def wrapInDeferred(f):
+    def _(*args, **kwargs):
+        return maybeDeferred(f, *args, **kwargs)
+
+    return _
+
+
+class MemcachedUIDReserver(CachePoolUserMixIn, LoggingMixIn):
+    def __init__(self, index, cachePool=None):
+        self.index = index
+        self._cachePool = cachePool
+
+    def _key(self, uid):
+        return 'reservation:%s' % (
+            hashlib.md5('%s:%s' % (uid,
+                                   self.index.resource.fp.path)).hexdigest())
+
+    def reserveUID(self, uid):
+        uid = uid.encode('utf-8')
+        self.log_debug("Reserving UID %r @ %r" % (
+                uid,
+                self.index.resource.fp.path))
+
+        def _handleFalse(result):
+            if result is False:
+                raise ReservationError(
+                    "UID %s already reserved for address book collection %s."
+                    % (uid, self.index.resource)
+                    )
+
+        d = self.getCachePool().add(self._key(uid),
+                                    'reserved',
+                                    expireTime=config.UIDReservationTimeOut)
+        d.addCallback(_handleFalse)
+        return d
+
+
+    def unreserveUID(self, uid):
+        uid = uid.encode('utf-8')
+        self.log_debug("Unreserving UID %r @ %r" % (
+                uid,
+                self.index.resource.fp.path))
+
+        def _handleFalse(result):
+            if result is False:
+                raise ReservationError(
+                    "UID %s is not reserved for address book collection %s."
+                    % (uid, self.index.resource)
+                    )
+
+        d =self.getCachePool().delete(self._key(uid))
+        d.addCallback(_handleFalse)
+        return d
+
+
+    def isReservedUID(self, uid):
+        uid = uid.encode('utf-8')
+        self.log_debug("Is reserved UID %r @ %r" % (
+                uid,
+                self.index.resource.fp.path))
+
+        def _checkValue((flags, value)):
+            if value is None:
+                return False
+            else:
+                return True
+
+        d = self.getCachePool().get(self._key(uid))
+        d.addCallback(_checkValue)
+        return d
+
+
+
+class SQLUIDReserver(object):
+    def __init__(self, index):
+        self.index = index
+
+    @wrapInDeferred
+    def reserveUID(self, uid):
+        """
+        Reserve a UID for this index's resource.
+        @param uid: the UID to reserve
+        @raise ReservationError: if C{uid} is already reserved
+        """
+
+        try:
+            self.index._db_execute("insert into RESERVED (UID, TIME) values (:1, :2)", uid, datetime.datetime.now())
+            self.index._db_commit()
+        except sqlite.IntegrityError:
+            self.index._db_rollback()
+            raise ReservationError(
+                "UID %s already reserved for address book collection %s."
+                % (uid, self.index.resource)
+            )
+        except sqlite.Error, e:
+            log.err("Unable to reserve UID: %s", (e,))
+            self.index._db_rollback()
+            raise
+
+    def unreserveUID(self, uid):
+        """
+        Unreserve a UID for this index's resource.
+        @param uid: the UID to reserve
+        @raise ReservationError: if C{uid} is not reserved
+        """
+
+        def _cb(result):
+            if result == False:
+                raise ReservationError(
+                    "UID %s is not reserved for address book collection %s."
+                    % (uid, self.index.resource)
+                    )
+            else:
+                try:
+                    self.index._db_execute(
+                        "delete from RESERVED where UID = :1", uid)
+                    self.index._db_commit()
+                except sqlite.Error, e:
+                    log.err("Unable to unreserve UID: %s", (e,))
+                    self.index._db_rollback()
+                    raise
+
+        d = self.isReservedUID(uid)
+        d.addCallback(_cb)
+        return d
+
+
+    @wrapInDeferred
+    def isReservedUID(self, uid):
+        """
+        Check to see whether a UID is reserved.
+        @param uid: the UID to check
+        @return: True if C{uid} is reserved, False otherwise.
+        """
+
+        rowiter = self.index._db_execute("select UID, TIME from RESERVED where UID = :1", uid)
+        for uid, attime in rowiter:
+            # Double check that the time is within a reasonable period of now
+            # otherwise we probably have a stale reservation
+            tm = time.strptime(attime[:19], "%Y-%m-%d %H:%M:%S")
+            dt = datetime.datetime(year=tm.tm_year, month=tm.tm_mon, day=tm.tm_mday, hour=tm.tm_hour, minute=tm.tm_min, second = tm.tm_sec)
+            if datetime.datetime.now() - dt > datetime.timedelta(seconds=config.UIDReservationTimeOut):
+                try:
+                    self.index._db_execute("delete from RESERVED where UID = :1", uid)
+                    self.index._db_commit()
+                except sqlite.Error, e:
+                    log.err("Unable to unreserve UID: %s", (e,))
+                    self.index._db_rollback()
+                    raise
+                return False
+            else:
+                return True
+
+        return False
+
+class AddressBookIndex(AbstractSQLDatabase):
+    """
+    AddressBook collection index abstract base class that defines the apis for the index.
+    """
+
+    def __init__(self, resource):
+        """
+        @param resource: the L{twistedcaldav.static.CalDAVFile} resource to
+            index. C{resource} must be an addressbook collection (ie.
+            C{resource.isAddressBookCollection()} returns C{True}.)
+        """
+        assert resource.isAddressBookCollection(), "non-addressbook collection resource %s has no index." % (resource,)
+        self.resource = resource
+        db_filename = os.path.join(self.resource.fp.path, db_basename)
+        super(AddressBookIndex, self).__init__(db_filename, False)
+
+        if config.Memcached.Pools.Default.ClientEnabled:
+            self.reserver = MemcachedUIDReserver(self)
+        else:
+            self.reserver = SQLUIDReserver(self)
+
+    def create(self):
+        """
+        Create the index and initialize it.
+        """
+        self._db()
+
+    def recreate(self):
+        """
+        Delete the database and re-create it
+        """
+        try:
+            os.remove(self.dbpath)
+        except OSError:
+            pass
+        self.create()
+
+    #
+    # A dict of sets. The dict keys are address book collection paths,
+    # and the sets contains reserved UIDs for each path.
+    #
+    
+    def reserveUID(self, uid):
+        return self.reserver.reserveUID(uid)
+    
+    def unreserveUID(self, uid):
+        return self.reserver.unreserveUID(uid)
+    
+    def isReservedUID(self, uid):
+        return self.reserver.isReservedUID(uid)
+        
+    def isAllowedUID(self, uid, *names):
+        """
+        Checks to see whether to allow an operation which would add the
+        specified UID to the index.  Specifically, the operation may not
+        violate the constraint that UIDs must be unique.
+        @param uid: the UID to check
+        @param names: the names of resources being replaced or deleted by the
+            operation; UIDs associated with these resources are not checked.
+        @return: True if the UID is not in the index and is not reserved,
+            False otherwise.
+        """
+        rname = self.resourceNameForUID(uid)
+        return (rname is None or rname in names)
+ 
+    def resourceNamesForUID(self, uid):
+        """
+        Looks up the names of the resources with the given UID.
+        @param uid: the UID of the resources to look up.
+        @return: a list of resource names
+        """
+        names = self._db_values_for_sql("select NAME from RESOURCE where UID = :1", uid)
+
+        #
+        # Check that each name exists as a child of self.resource.  If not, the
+        # resource record is stale.
+        #
+        resources = []
+        for name in names:
+            name_utf8 = name.encode("utf-8")
+            if name is not None and self.resource.getChild(name_utf8) is None:
+                # Clean up
+                log.err("Stale resource record found for child %s with UID %s in %s" % (name, uid, self.resource))
+                self._delete_from_db(name, uid)
+                self._db_commit()
+            else:
+                resources.append(name)
+
+        return resources
+
+    def resourceNameForUID(self, uid):
+        """
+        Looks up the name of the resource with the given UID.
+        @param uid: the UID of the resource to look up.
+        @return: If the resource is found, its name; C{None} otherwise.
+        """
+        result = None
+
+        for name in self.resourceNamesForUID(uid):
+            assert result is None, "More than one resource with UID %s in address book collection %r" % (uid, self)
+            result = name
+            
+        return result
+
+    def resourceUIDForName(self, name):
+        """
+        Looks up the UID of the resource with the given name.
+        @param name: the name of the resource to look up.
+        @return: If the resource is found, the UID of the resource; C{None}
+            otherwise.
+        """
+        uid = self._db_value_for_sql("select UID from RESOURCE where NAME = :1", name)
+
+        return uid
+
+    def addResource(self, name, vcard, fast=False):
+        """
+        Adding or updating an existing resource.
+        To check for an update we attempt to get an existing UID
+        for the resource name. If present, then the index entries for
+        that UID are removed. After that the new index entries are added.
+        @param name: the name of the resource to add.
+        @param vCard: a L{Component} object representing the resource
+            contents.
+        @param fast: if C{True} do not do commit, otherwise do commit.
+        """
+        oldUID = self.resourceUIDForName(name)
+        if oldUID is not None:
+            self._delete_from_db(name, oldUID)
+        self._add_to_db(name, vcard)
+        if not fast:
+            self._db_commit()
+
+    def deleteResource(self, name):
+        """
+        Remove this resource from the index.
+        @param name: the name of the resource to add.
+        @param uid: the UID of the vcard component in the resource.
+        """
+        uid = self.resourceUIDForName(name)
+        if uid is not None:
+            self._delete_from_db(name, uid)
+            self._db_commit()
+    
+    def resourceExists(self, name):
+        """
+        Determines whether the specified resource name exists in the index.
+        @param name: the name of the resource to test
+        @return: True if the resource exists, False if not
+        """
+        uid = self._db_value_for_sql("select UID from RESOURCE where NAME = :1", name)
+        return uid is not None
+    
+    def resourcesExist(self, names):
+        """
+        Determines whether the specified resource name exists in the index.
+        @param names: a C{list} containing the names of the resources to test
+        @return: a C{list} of all names that exist
+        """
+        statement = "select NAME from RESOURCE where NAME in ("
+        for ctr, ignore_name in enumerate(names):
+            if ctr != 0:
+                statement += ", "
+            statement += ":%s" % (ctr,)
+        statement += ")"
+        results = self._db_values_for_sql(statement, *names)
+        return results
+    
+    def searchValid(self, filter):
+        if isinstance(filter, carddavxml.Filter):
+            qualifiers = addressbookquery.sqladdressbookquery(filter)
+        else:
+            qualifiers = None
+            
+        return qualifiers is not None
+
+    def search(self, filter):
+        """
+        Finds resources matching the given qualifiers.
+        @param filter: the L{Filter} for the addressbook-query to execute.
+        @return: an interable iterable of tuples for each resource matching the
+            given C{qualifiers}. The tuples are C{(name, uid, type)}, where
+            C{name} is the resource name, C{uid} is the resource UID, and
+            C{type} is the resource iCalendar component type.x
+        """
+        # FIXME: Don't forget to use maximum_future_expansion_duration when we
+        # start caching...
+        
+        # Make sure we have a proper Filter element and get the partial SQL statement to use.
+        if isinstance(filter, carddavxml.Filter):
+            qualifiers = addressbookquery.sqladdressbookquery(filter)
+        else:
+            qualifiers = None
+        if qualifiers is not None:
+            rowiter = self._db_execute("select DISTINCT RESOURCE.NAME, RESOURCE.UID" + qualifiers[0], *qualifiers[1])
+        else:
+            rowiter = self._db_execute("select NAME, UID from RESOURCE")
+            
+        for row in rowiter:
+            name = row[0]
+            if self.resource.getChild(name.encode("utf-8")):
+                yield row
+            else:
+                log.err("vCard resource %s is missing from %s. Removing from index."
+                        % (name, self.resource))
+                self.deleteResource(name)
+
+    def _db_version(self):
+        """
+        @return: the schema version assigned to this index.
+        """
+        return schema_version
+        
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this index.
+        """
+        return "AddressBook"
+        
+    def _db_init_data_tables(self, q):
+        """
+        Initialise the underlying database tables.
+        @param q:           a database cursor to use.
+        """
+        
+        # Create database where the RESOURCE table has unique UID column.
+        self._db_init_data_tables_base(q, True)
+
+    def _db_init_data_tables_base(self, q, uidunique):
+        """
+        Initialise the underlying database tables.
+        @param q:           a database cursor to use.
+        """
+        #
+        # RESOURCE table is the primary index table
+        #   NAME: Last URI component (eg. <uid>.ics, RESOURCE primary key)
+        #   UID: iCalendar UID (may or may not be unique)
+        #
+        q.execute(
+            """
+            create table RESOURCE (
+                NAME           text unique,
+                UID            text unique
+            )
+            """
+        )
+
+        #
+        # RESERVED table tracks reserved UIDs
+        #   UID: The UID being reserved
+        #   TIME: When the reservation was made
+        #
+        q.execute(
+            """
+            create table RESERVED (
+                UID  text unique,
+                TIME date
+            )
+            """
+        )
+
+    def _db_recreate(self, do_commit=True):
+        """
+        Re-create the database tables from existing address book data.
+        """
+        
+        #
+        # Populate the DB with data from already existing resources.
+        # This allows for index recovery if the DB file gets
+        # deleted.
+        #
+        fp = self.resource.fp
+        for name in fp.listdir():
+            if name.startswith("."):
+                continue
+
+            try:
+                stream = fp.child(name).open()
+            except (IOError, OSError), e:
+                log.err("Unable to open resource %s: %s" % (name, e))
+                continue
+
+            try:
+                # FIXME: This is blocking I/O
+                try:
+                    vcard = Component.fromStream(stream)
+                    vcard.validForCardDAV()
+                except ValueError:
+                    log.err("Non-addressbook resource: %s" % (name,))
+                else:
+                    #log.msg("Indexing resource: %s" % (name,))
+                    self.addResource(name, vcard, True)
+            finally:
+                stream.close()
+        
+        # Do commit outside of the loop for better performance
+        if do_commit:
+            self._db_commit()
+
+    def _add_to_db(self, name, vcard, cursor = None):
+        """
+        Records the given address book resource in the index with the given name.
+        Resource names and UIDs must both be unique; only one resource name may
+        be associated with any given UID and vice versa.
+        NB This method does not commit the changes to the db - the caller
+        MUST take care of that
+        @param name: the name of the resource to add.
+        @param vcard: a L{AddressBook} object representing the resource
+            contents.
+        """
+        uid = vcard.resourceUID()
+
+        self._db_execute(
+            """
+            insert into RESOURCE (NAME, UID)
+            values (:1, :2)
+            """, name, uid,
+        )
+    
+    def _delete_from_db(self, name, uid):
+        """
+        Deletes the specified entry from all dbs.
+        @param name: the name of the resource to delete.
+        @param uid: the uid of the resource to delete.
+        """
+        self._db_execute("delete from RESOURCE where NAME = :1", name)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100209/b6ee1faa/attachment-0001.html>


More information about the calendarserver-changes mailing list