<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[13037] CalendarServer/branches/users/sagen/move2who-4</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/13037">13037</a></dd>
<dt>Author</dt> <dd>gaya@apple.com</dd>
<dt>Date</dt> <dd>2014-03-28 14:51:51 -0700 (Fri, 28 Mar 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>First checkin of directory gateway based on twext.who</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserssagenmove2who4calendarservertaputilpy">CalendarServer/branches/users/sagen/move2who-4/calendarserver/tap/util.py</a></li>
<li><a href="#CalendarServerbranchesuserssagenmove2who4confcaldavdtestplist">CalendarServer/branches/users/sagen/move2who-4/conf/caldavd-test.plist</a></li>
<li><a href="#CalendarServerbranchesuserssagenmove2who4twistedcaldavdirectorybackedaddressbookpy">CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directorybackedaddressbook.py</a></li>
<li><a href="#CalendarServerbranchesuserssagenmove2who4twistedcaldavmethodreport_addressbook_querypy">CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/method/report_addressbook_query.py</a></li>
<li><a href="#CalendarServerbranchesuserssagenmove2who4twistedcaldavmethodreport_multiget_commonpy">CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/method/report_multiget_common.py</a></li>
</ul>

<h3>Removed Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserssagenmove2who4twistedcaldavdirectoryopendirectorybackerpy">CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directory/opendirectorybacker.py</a></li>
<li><a href="#CalendarServerbranchesuserssagenmove2who4twistedcaldavdirectorytesttest_opendirectorybackerpy">CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directory/test/test_opendirectorybacker.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServerbranchesuserssagenmove2who4calendarservertaputilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sagen/move2who-4/calendarserver/tap/util.py (13036 => 13037)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sagen/move2who-4/calendarserver/tap/util.py        2014-03-28 21:42:50 UTC (rev 13036)
+++ CalendarServer/branches/users/sagen/move2who-4/calendarserver/tap/util.py        2014-03-28 21:51:51 UTC (rev 13037)
</span><span class="lines">@@ -35,6 +35,7 @@
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from txweb2.auth.basic import BasicCredentialFactory
</span><span class="cx"> from txweb2.dav import auth
</span><ins>+from txweb2.dav.util import joinURL
</ins><span class="cx"> from txweb2.http_headers import Headers
</span><span class="cx"> from txweb2.resource import Resource
</span><span class="cx"> from txweb2.static import File as FileResource
</span><span class="lines">@@ -46,25 +47,25 @@
</span><span class="cx"> from twisted.internet.reactor import addSystemEventTrigger
</span><span class="cx"> from twisted.internet.tcp import Connection
</span><span class="cx"> 
</span><ins>+from calendarserver.push.applepush import APNSubscriptionResource
+from calendarserver.push.notifier import NotifierFactory
+from twext.enterprise.adbapi2 import ConnectionPool, ConnectionPoolConnection
+from twext.enterprise.ienterprise import ORACLE_DIALECT
+from twext.enterprise.ienterprise import POSTGRES_DIALECT
</ins><span class="cx"> from twistedcaldav.bind import doBind
</span><span class="cx"> from twistedcaldav.cache import CacheStoreNotifierFactory
</span><span class="cx"> from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
</span><span class="cx"> from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
</span><span class="cx"> from twistedcaldav.directory.digest import QopDigestCredentialFactory
</span><span class="cx"> from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
</span><del>-from calendarserver.push.notifier import NotifierFactory
-from calendarserver.push.applepush import APNSubscriptionResource
</del><span class="cx"> from twistedcaldav.directorybackedaddressbook import DirectoryBackedAddressBookResource
</span><span class="cx"> from twistedcaldav.resource import AuthenticationWrapper
</span><del>-from txdav.caldav.datastore.scheduling.ischedule.dkim import DKIMUtils, DomainKeyResource
-from txdav.caldav.datastore.scheduling.ischedule.resource import IScheduleInboxResource
</del><span class="cx"> from twistedcaldav.simpleresource import SimpleResource, SimpleRedirectResource
</span><span class="cx"> from twistedcaldav.timezones import TimezoneCache
</span><span class="cx"> from twistedcaldav.timezoneservice import TimezoneServiceResource
</span><span class="cx"> from twistedcaldav.timezonestdservice import TimezoneStdServiceResource
</span><del>-from twext.enterprise.ienterprise import POSTGRES_DIALECT
-from twext.enterprise.ienterprise import ORACLE_DIALECT
-from twext.enterprise.adbapi2 import ConnectionPool, ConnectionPoolConnection
</del><ins>+from txdav.caldav.datastore.scheduling.ischedule.dkim import DKIMUtils, DomainKeyResource
+from txdav.caldav.datastore.scheduling.ischedule.resource import IScheduleInboxResource
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> try:
</span><span class="lines">@@ -237,9 +238,9 @@
</span><span class="cx">         quota = None
</span><span class="cx">     if txnFactory is not None:
</span><span class="cx">         if config.EnableSSL:
</span><del>-            uri = &quot;https://%s:%s&quot; % (config.ServerHostName, config.SSLPort,)
</del><ins>+            uri = &quot;https://{config.ServerHostName}:{config.SSLPort}&quot;.format(config=config)
</ins><span class="cx">         else:
</span><del>-            uri = &quot;http://%s:%s&quot; % (config.ServerHostName, config.HTTPPort,)
</del><ins>+            uri = &quot;https://{config.ServerHostName}:{config.HTTPPort}&quot;.format(config=config)
</ins><span class="cx">         attachments_uri = uri + &quot;/calendars/__uids__/%(home)s/dropbox/%(dropbox_id)s/%(name)s&quot;
</span><span class="cx">         store = CommonSQLDataStore(
</span><span class="cx">             txnFactory, notifierFactories,
</span><span class="lines">@@ -283,7 +284,11 @@
</span><span class="cx">         credentials = IPrincipalCredentials(credentials)
</span><span class="cx"> 
</span><span class="cx">         if credentials.authnPrincipal is None:
</span><del>-            raise UnauthorizedLogin(&quot;No such user: %s&quot; % (credentials.credentials.username,))
</del><ins>+            raise UnauthorizedLogin(
+                &quot;No such user: {user}&quot;.format(
+                    user=credentials.credentials.username
+                )
+        )
</ins><span class="cx"> 
</span><span class="cx">         # See if record is enabledForLogin
</span><span class="cx">         if not credentials.authnPrincipal.record.isLoginEnabled():
</span><span class="lines">@@ -321,9 +326,14 @@
</span><span class="cx">                     )
</span><span class="cx">                 )
</span><span class="cx">             else:
</span><del>-                raise UnauthorizedLogin(&quot;Incorrect credentials for %s&quot; % (credentials.credentials.username,))
</del><ins>+                raise UnauthorizedLogin(
+                    &quot;Incorrect credentials for user: {user}&quot;.format(
+                        user=credentials.credentials.username
+                    )
+                )
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx"> def getRootResource(config, newStore, resources=None):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Set up directory service and resource hierarchy based on config.
</span><span class="lines">@@ -468,13 +478,14 @@
</span><span class="cx">             newStore,
</span><span class="cx">         )
</span><span class="cx"> 
</span><del>-        directoryPath = os.path.join(config.DocumentRoot, config.DirectoryAddressBook.name)
</del><span class="cx">         if config.DirectoryAddressBook.Enabled and config.EnableSearchAddressBook:
</span><span class="cx">             log.info(&quot;Setting up directory address book: {cls}&quot;,
</span><span class="cx">                 cls=directoryBackedAddressBookResourceClass)
</span><span class="cx"> 
</span><span class="cx">             directoryBackedAddressBookCollection = directoryBackedAddressBookResourceClass(
</span><del>-                principalCollections=(principalCollection,)
</del><ins>+                principalCollections=(principalCollection,),
+                principalDirectory=directory,
+                uri=joinURL(&quot;/&quot;, config.DirectoryAddressBook.name, &quot;/&quot;)
</ins><span class="cx">             )
</span><span class="cx">             if _reactor._started:
</span><span class="cx">                 directoryBackedAddressBookCollection.provisionDirectory()
</span><span class="lines">@@ -482,6 +493,7 @@
</span><span class="cx">                 addSystemEventTrigger(&quot;after&quot;, &quot;startup&quot;, directoryBackedAddressBookCollection.provisionDirectory)
</span><span class="cx">         else:
</span><span class="cx">             # remove /directory from previous runs that may have created it
</span><ins>+            directoryPath = os.path.join(config.DocumentRoot, config.DirectoryAddressBook.name)
</ins><span class="cx">             try:
</span><span class="cx">                 FilePath(directoryPath).remove()
</span><span class="cx">                 log.info(&quot;Deleted: {path}&quot;, path=directoryPath)
</span></span></pre></div>
<a id="CalendarServerbranchesuserssagenmove2who4confcaldavdtestplist"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sagen/move2who-4/conf/caldavd-test.plist (13036 => 13037)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sagen/move2who-4/conf/caldavd-test.plist        2014-03-28 21:42:50 UTC (rev 13036)
+++ CalendarServer/branches/users/sagen/move2who-4/conf/caldavd-test.plist        2014-03-28 21:51:51 UTC (rev 13037)
</span><span class="lines">@@ -577,7 +577,7 @@
</span><span class="cx"> 
</span><span class="cx">     &lt;!-- Log levels --&gt;
</span><span class="cx">     &lt;key&gt;DefaultLogLevel&lt;/key&gt;
</span><del>-    &lt;string&gt;info&lt;/string&gt; &lt;!-- debug, info, warn, error --&gt;
</del><ins>+    &lt;string&gt;debug&lt;/string&gt; &lt;!-- debug, info, warn, error --&gt;
</ins><span class="cx"> 
</span><span class="cx">     &lt;!-- Log level overrides for specific functionality --&gt;
</span><span class="cx">     &lt;key&gt;LogLevels&lt;/key&gt;
</span><span class="lines">@@ -1017,6 +1017,8 @@
</span><span class="cx">       &lt;string&gt;en&lt;/string&gt;
</span><span class="cx">     &lt;/dict&gt;
</span><span class="cx"> 
</span><del>-
</del><ins>+    &lt;!-- Directory Address Book --&gt;
+    &lt;key&gt;EnableSearchAddressBook&lt;/key&gt;
+    &lt;true/&gt;
</ins><span class="cx">   &lt;/dict&gt;
</span><span class="cx"> &lt;/plist&gt;
</span></span></pre></div>
<a id="CalendarServerbranchesuserssagenmove2who4twistedcaldavdirectoryopendirectorybackerpy"></a>
<div class="delfile"><h4>Deleted: CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directory/opendirectorybacker.py (13036 => 13037)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directory/opendirectorybacker.py        2014-03-28 21:42:50 UTC (rev 13036)
+++ CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directory/opendirectorybacker.py        2014-03-28 21:51:51 UTC (rev 13037)
</span><span class="lines">@@ -1,1960 +0,0 @@
</span><del>-##
-# Copyright (c) 2006-2014 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
-# 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 &quot;AS IS&quot; 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.
-##
-
-
-&quot;&quot;&quot;
-Apple Open Directory directory service implementation for backing up directory-backed address books
-&quot;&quot;&quot;
-
-__all__ = [
-    &quot;OpenDirectoryBackingService&quot;, &quot;VCardRecord&quot;,
-]
-
-from calendarserver.platform.darwin.od import opendirectory, dsattributes, dsquery
-
-from pycalendar.datetime import DateTime
-from pycalendar.vcard.adr import Adr
-from pycalendar.vcard.n import N
-
-
-from twext.python.filepath import CachingFilePath as FilePath
-
-from txweb2.dav.resource import DAVPropertyMixIn
-from txweb2.dav.util import joinURL
-from txweb2.http_headers import MimeType, generateContentType, ETag
-
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator, succeed
-
-from twistedcaldav import customxml, carddavxml
-from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from twistedcaldav.vcard import Component, Property, vCardProductID
-
-from txdav.carddav.datastore.query.filter import IsNotDefined, ParameterFilter, \
-    TextMatch
-from txdav.xml import element as davxml
-from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
-
-from os import listdir
-from os.path import join, abspath
-from random import random
-from socket import getfqdn
-from tempfile import mkstemp, gettempdir
-from xmlrpclib import datetime
-import hashlib
-import os
-import sys
-import time
-import traceback
-
-class OpenDirectoryBackingService(DirectoryService):
-    &quot;&quot;&quot;
-    Open Directory implementation of L{IDirectoryService}.
-    &quot;&quot;&quot;
-
-    baseGUID = &quot;BF07A1A2-5BB5-4A4D-A59A-67260EA7E143&quot;
-
-    def __repr__(self):
-        return &quot;&lt;%s %r&gt;&quot; % (self.__class__.__name__, self.realmName,)
-
-
-    def __init__(self, params):
-        self._actuallyConfigure(**params)
-
-
-    def _actuallyConfigure(
-        self, queryPeopleRecords=True,
-        peopleNode=&quot;/Search/Contacts&quot;,
-        queryUserRecords=True,
-        userNode=&quot;/Search&quot;,
-        maxDSQueryRecords=0,            # 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 &quot;X-&quot; attributes
-        standardizeSyntheticUIDs=False,  # use simple synthetic UIDs --- good for testing
-        appleInternalServer=False,
-
-        additionalAttributes=[],
-        allowedAttributes=[],
-        directoryBackedAddressBook=None
-    ):
-        &quot;&quot;&quot;
-        @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
-
-        &quot;&quot;&quot;
-        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(&quot;Open Directory (node=%s) Initialization error: %s&quot; % (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(&quot;Open Directory (node=%s) Initialization error: %s&quot; % (userNode, e))
-                    raise
-                if self.realmName:
-                    self.realmName += &quot;+&quot; + 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 &gt; 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(&quot;X-INTERNAL-REQUIRED&quot;)
-                                                )))
-            if (self.allowedDSQueryAttributes != VCardRecord.allDSQueryAttributes):
-                self.log.info(&quot;Allowed DS query attributes = %r&quot; % (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 = &quot;/LDAPv3/&quot; + hostname
-        else:
-            self.defaultNodeName = None
-
-        #cleanup
-        self._cleanupTime = time.time()
-
-        # file system locks
-        self._initLockPath = join(config.DocumentRoot, &quot;.directory_address_book_create_lock&quot;)
-        self._createdLockPath = join(config.DocumentRoot, &quot;.directory_address_book_created_lock&quot;)
-        self._updateLockPath = join(config.DocumentRoot, &quot;.directory_address_book_update_lock&quot;)
-        self._tmpDirAddressBookLockPath = join(config.DocumentRoot, &quot;.directory_address_book_tmpFolder_lock&quot;)
-
-        self._updateLock = MemcacheLock(&quot;OpenDirectoryBacker&quot;, self._updateLockPath)
-        self._tmpDirAddressBookLock = MemcacheLock(&quot;OpenDirectoryBacker&quot;, 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 (&quot;directory&quot;, &quot;node&quot;):
-            diff = cmp(getattr(self, attr), getattr(other, attr))
-            if diff != 0:
-                return diff
-        return 0
-
-
-    def __hash__(self):
-        h = hash(self.__class__.__name__)
-        for attr in (&quot;node&quot;,):
-            h = (h + hash(getattr(self, attr))) &amp; sys.maxint
-        return h
-
-
-    @inlineCallbacks
-    def available(self):
-        if not self._triedCreateLock:
-            returnValue(False)
-        elif not self._created:
-            createdLock = MemcacheLock(&quot;OpenDirectoryBacker&quot;, self._createdLockPath)
-            self.log.debug(&quot;blocking on lock of: \&quot;%s\&quot;)&quot; % self._createdLockPath)
-            self._created = (yield createdLock.locked())
-
-        returnValue(self._created)
-
-
-    def updateLock(self):
-        return self._updateLock
-
-
-    @inlineCallbacks
-    def createCache(self):
-        &quot;&quot;&quot;
-        If caching, create the cache for the first time.
-        &quot;&quot;&quot;
-
-        if not self.liveQuery:
-            self.log.info(&quot;loading directory address book&quot;)
-
-            # get init lock
-            initLock = MemcacheLock(&quot;OpenDirectoryBacker&quot;, self._initLockPath, timeout=0)
-            self.log.debug(&quot;Attempt lock of: \&quot;%s\&quot;)&quot; % self._initLockPath)
-            gotCreateLock = False
-            try:
-                yield initLock.acquire()
-                gotCreateLock = True
-            except MemcacheLockTimeoutError:
-                pass
-
-            self._triedCreateLock = True
-
-            if gotCreateLock:
-                self.log.debug(&quot;Got lock!&quot;)
-                yield self._refreshCache(flushCache=False, creating=True)
-            else:
-                self.log.debug(&quot;Could not get lock - directory address book will be filled by peer&quot;)
-
-
-    @inlineCallbacks
-    def _refreshCache(self, flushCache=False, creating=False, reschedule=True, query=None, attributes=None, keepLock=False, clear=False, maxRecords=0):
-        &quot;&quot;&quot;
-        refresh the cache.
-        &quot;&quot;&quot;
-
-        #print(&quot;_refreshCache:, flushCache=%s, creating=%s, reschedule=%s, query = %s&quot; % (flushCache, creating, reschedule, &quot;None&quot; 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(&quot;Refresh directory address book in %d minutes %d seconds&quot; % 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) &lt; 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(&quot;Remove temporary directory address books in %d minutes %d seconds&quot; % 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(&quot;/Volumes/&quot;):
-                tmpDir = absDocPath
-                prefix = &quot;.directoryAddressBook-&quot;
-            else:
-                tmpDir = gettempdir()
-                prefix = &quot;directoryAddressBook-&quot;
-
-            return (tmpDir, prefix, &quot;.tmp&quot;)
-
-        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(&quot;Checking for temporary directory address books&quot;)
-            tmpDir, prefix, suffix = getTmpDirAndTmpFilePrefixSuffix()
-
-            tmpDirLock = self._tmpDirAddressBookLock
-            self.log.debug(&quot;blocking on lock of: \&quot;%s\&quot;)&quot; % 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(&quot;Deleting temporary directory address book at: %s&quot; % path)
-                            FilePath(path).remove()
-                            self.log.debug(&quot;Done deleting&quot;)
-                        except:
-                            self.log.info(&quot;Deletion failed&quot;)
-            finally:
-                self.log.debug(&quot;unlocking: \&quot;%s\&quot;)&quot; % 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 + &quot;:&quot; + self.realmName + &quot;:&quot; + &quot;&quot;.join(str(hash(records[key])) for key in records.keys()))))
-
-                # get the old hash
-                oldAddressBookCTag = &quot;&quot;
-                updateLock = self.updateLock()
-                self.log.debug(&quot;blocking on lock of: \&quot;%s\&quot;)&quot; % self._updateLockPath)
-                yield updateLock.acquire()
-
-                if not flushCache:
-                    # get update lock
-                    try:
-                        oldAddressBookCTag = self.directoryBackedAddressBook.readDeadProperty((calendarserver_namespace, &quot;getctag&quot;))
-                    except:
-                        oldAddressBookCTag = &quot;&quot;
-
-                self.log.debug(&quot;Comparing {http://calendarserver.org/ns/}getctag: new = %s, old = %s&quot; % (newAddressBookCTag, oldAddressBookCTag))
-                if str(newAddressBookCTag) != str(oldAddressBookCTag):
-
-                    self.log.debug(&quot;unlocking: \&quot;%s\&quot;)&quot; % self._updateLockPath)
-                    yield updateLock.release()
-                    updateLock = None
-
-                if not keepLock:
-                    self.log.debug(&quot;unlocking: \&quot;%s\&quot;)&quot; % self._updateLockPath)
-                    yield updateLock.release()
-                    updateLock = None
-
-            except:
-                cleanupLater()
-                if reschedule:
-                    refreshLater()
-                raise
-
-            if creating:
-                createdLock = MemcacheLock(&quot;OpenDirectoryBacker&quot;, self._createdLockPath)
-                self.log.debug(&quot;blocking on lock of: \&quot;%s\&quot;)&quot; % 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(&quot;/Local/Default&quot;)
-                self.log.debug(&quot;opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)&quot; % (
-                        &quot;/DSLocal&quot;,
-                        recordTypes,
-                        self.returnedAttributes,
-                    ))
-                results = list(opendirectory.listAllRecordsWithAttributes_list(
-                        localNodeDirectory,
-                        recordTypes,
-                        self.returnedAttributes,
-                    ))
-            except opendirectory.ODError, ex:
-                self.log.error(&quot;Open Directory (node=%s) error: %s&quot; % (&quot;/Local/Default&quot;, str(ex)))
-                raise
-
-            self._dsLocalRecords = []
-            for (recordShortName, value) in results: #@UnusedVariable
-
-                record = VCardRecord(self, value, &quot;/Local/Default&quot;)
-
-                if self.ignoreSystemRecords:
-                    # remove system users and people
-                    if record.guid.startswith(&quot;FFFFEEEE-DDDD-CCCC-BBBB-AAAA&quot;):
-                        self.log.info(&quot;Ignoring vcard for system record %s&quot; % (record,))
-                        continue
-
-                if record.guid in records:
-                    self.log.info(&quot;Record skipped due to conflict (duplicate uuid): %s&quot; % (record,))
-                else:
-                    try:
-                        vCardText = record.vCardText()
-                    except:
-                        traceback.print_exc()
-                        self.log.info(&quot;Could not get vcard for record %s&quot; % (record,))
-                    else:
-                        self.log.debug(&quot;VCard text =\n%s&quot; % (vCardText,))
-                        records[record.guid] = record
-
-            return records
-
-        if not self.liveQuery or not self.queryDSLocal:
-            return {}
-
-        if time.time() &gt; 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):
-        &quot;&quot;&quot;
-        Get a list of filtered VCardRecord for the given query with the given attributes.
-        query == None gets all records. attribute == None gets VCardRecord.allDSQueryAttributes
-        &quot;&quot;&quot;
-        limited = False
-        queryResults = (yield self._queryDirectory(query, attributes, maxRecords))
-        if maxRecords and len(queryResults) &gt;= maxRecords:
-            limited = True
-            self.log.debug(&quot;Directory address book record limit (= %d) reached.&quot; % (maxRecords,))
-
-        self.log.debug(&quot;Query done. Inspecting %s results&quot; % len(queryResults))
-
-        records = self._getDSLocalRecords().copy()
-        self.log.debug(&quot;Adding %s DSLocal results&quot; % 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(&quot;FFFFEEEE-DDDD-CCCC-BBBB-AAAA&quot;):
-                    self.log.info(&quot;Ignoring vcard for system record %s&quot; % (record,))
-                    continue
-
-            if record.guid in records:
-                self.log.info(&quot;Ignoring vcard for record due to conflict (duplicate uuid): %s&quot; % (record,))
-            else:
-                records[record.guid] = record
-
-        self.log.debug(&quot;After filtering, %s records (limited=%s).&quot; % (len(records), limited))
-        returnValue((records, limited,))
-
-
-    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, dsquery.match) and query.value is not &quot;&quot;:
-                        self.log.debug(&quot;opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r,%r)&quot; % (
-                            node,
-                            query.attribute,
-                            query.value,
-                            query.matchType,
-                            False,
-                            recordType,
-                            attributes,
-                            maxRecords,
-                        ))
-                        results = list(
-                            opendirectory.queryRecordsWithAttribute_list(
-                                directory,
-                                query.attribute,
-                                query.value,
-                                query.matchType,
-                                False,
-                                recordType,
-                                attributes,
-                                maxRecords,
-                            ))
-                    else:
-                        self.log.debug(&quot;opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r)&quot; % (
-                            node,
-                            query.generate(),
-                            False,
-                            recordType,
-                            attributes,
-                            maxRecords,
-                        ))
-                        results = list(
-                            opendirectory.queryRecordsWithAttributes_list(
-                                directory,
-                                query.generate(),
-                                False,
-                                recordType,
-                                attributes,
-                                maxRecords,
-                            ))
-                else:
-                    self.log.debug(&quot;opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r,%r)&quot; % (
-                        node,
-                        recordType,
-                        attributes,
-                        maxRecords,
-                    ))
-                    results = list(
-                        opendirectory.listAllRecordsWithAttributes_list(
-                            directory,
-                            recordType,
-                            attributes,
-                            maxRecords,
-                        ))
-            except opendirectory.ODError, ex:
-                self.log.error(&quot;Open Directory (node=%s) error: %s&quot; % (self.realmName, str(ex)))
-                raise
-
-            allResults.extend(results)
-
-            if maxRecords:
-                maxRecords -= len(results)
-                if maxRecords &lt;= 0:
-                    break
-
-        elaspedTime = time.time() - startTime
-        self.log.info(&quot;Timing: Directory query: %.1f ms (%d records, %.2f records/sec)&quot; % (elaspedTime * 1000, len(allResults), len(allResults) / elaspedTime))
-        return succeed(allResults)
-
-
-    def _getDSFilter(self, addressBookFilter):
-        &quot;&quot;&quot;
-        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
-        &quot;&quot;&quot;
-        def propFilterListQuery(filterAllOf, propFilters):
-
-            def propFilterExpression(filterAllOf, propFilter):
-                #print(&quot;propFilterExpression&quot;)
-                &quot;&quot;&quot;
-                Create an expression for a single prop-filter element.
-
-                @param propFilter: the L{PropertyFilter} element.
-                @return: (needsAllRecords, espressionAttributes, expressions) tuple
-                &quot;&quot;&quot;
-
-                def definedExpression(defined, allOf, filterName, constant, queryAttributes, allAttrStrings):
-                    if constant or filterName in (&quot;N&quot; , &quot;FN&quot;, &quot;UID&quot;,):
-                        return (defined, [], [])     # all records have this property so no records do not have it
-                    else:
-                        matchList = list(set([dsquery.match(attrName, &quot;&quot;, dsattributes.eDSStartsWith) for attrName in allAttrStrings]))
-                        if defined:
-                            return andOrExpression(allOf, queryAttributes, matchList)
-                        else:
-                            if len(matchList) &gt; 1:
-                                expr = dsquery.expression(dsquery.expression.OR, matchList)
-                            else:
-                                expr = matchList
-                            return (False, queryAttributes, [dsquery.expression(dsquery.expression.NOT, expr), ])
-                    #end isNotDefinedExpression()
-
-
-                def andOrExpression(propFilterAllOf, queryAttributes, matchList):
-                    #print(&quot;andOrExpression(propFilterAllOf=%r, queryAttributes%r, matchList%r)&quot; % (propFilterAllOf, queryAttributes, matchList))
-                    if propFilterAllOf and len(matchList):
-                        # add OR expression because parent will AND
-                        return (False, queryAttributes, [dsquery.expression(dsquery.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 == &quot;PHOTO&quot;:
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {&quot;ENCODING&quot;: [&quot;B&quot;, ], &quot;TYPE&quot;: [&quot;JPEG&quot;, ], }):
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == &quot;ADR&quot;:
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;POSTAL&quot;, &quot;PARCEL&quot;, ], }):
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == &quot;LABEL&quot;:
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {&quot;TYPE&quot;: [&quot;POSTAL&quot;, &quot;PARCEL&quot;, ]}):
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == &quot;TEL&quot;:
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {&quot;TYPE&quot;: [], }): # has params derived from ds attributes
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == &quot;EMAIL&quot;:
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {&quot;TYPE&quot;: [], }): # has params derived from ds attributes
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == &quot;URL&quot;:
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {}):
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == &quot;KEY&quot;:
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {&quot;ENCODING&quot;: [&quot;B&quot;, ], &quot;TYPE&quot;: [&quot;PGPPUBILICKEY&quot;, &quot;USERCERTIFICATE&quot;, &quot;USERPKCS12DATA&quot;, &quot;USERSMIMECERTIFICATE&quot;, ]}):
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif not filterName.startswith(&quot;X-&quot;): #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 (&quot;REV&quot; , &quot;BDAY&quot;,):
-                            rawString = matchString
-                            matchString = &quot;&quot;
-                            for c in rawString:
-                                if not c in &quot;TZ-:&quot;:
-                                    matchString += c
-                        elif propFilter.filter_name == &quot;GEO&quot;:
-                            matchString = &quot;,&quot;.join(matchString.split(&quot;;&quot;))
-
-                        if propFilter.filter_name in (&quot;N&quot; , &quot;ADR&quot;, &quot;ORG&quot;,):
-                            # for structured properties, change into multiple strings for ds query
-                            if propFilter.filter_name == &quot;ADR&quot;:
-                                #split by newline and comma
-                                rawStrings = &quot;,&quot;.join(matchString.split(&quot;\n&quot;)).split(&quot;,&quot;)
-                            else:
-                                #split by space
-                                rawStrings = matchString.split(&quot; &quot;)
-
-                            # 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.test([constant, ]), [], [])
-                    else:
-
-                        matchStrings = getMatchStrings(propFilter, textMatchElement.text)
-
-                        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 == &quot;UID&quot;:
-                                matchString = matchStrings[0]
-                                seperatorIndex = matchString.find(VCardRecord.peopleUIDSeparator)
-                                if seperatorIndex &gt; 1:
-                                    recordNameStart = seperatorIndex + len(VCardRecord.peopleUIDSeparator)
-                                else:
-                                    seperatorIndex = matchString.find(VCardRecord.userUIDSeparator)
-                                    if seperatorIndex &gt; 1:
-                                        recordNameStart = seperatorIndex + len(VCardRecord.userUIDSeparator)
-                                    else:
-                                        recordNameStart = sys.maxint
-
-                                if recordNameStart &lt; len(matchString) - 1:
-                                    try:
-                                        recordNameQualifier = matchString[recordNameStart:].decode(&quot;base64&quot;).decode(&quot;utf8&quot;)
-                                    except Exception, e:
-                                        self.log.debug(&quot;Could not decode UID string %r in %r: %r&quot; % (matchString[recordNameStart:], matchString, e,))
-                                    else:
-                                        if textMatchElement.negate:
-                                            return (False, queryAttributes,
-                                                    [dsquery.expression(dsquery.expression.NOT, dsquery.match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact)), ]
-                                                    )
-                                        else:
-                                            return (False, queryAttributes,
-                                                    [dsquery.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 (&quot;NICKNAME&quot; , &quot;TITLE&quot; , &quot;NOTE&quot; , &quot;UID&quot;, &quot;URL&quot;, &quot;N&quot;, &quot;ADR&quot;, &quot;ORG&quot;, &quot;REV&quot;, &quot;LABEL&quot;,):
-                                if textMatchElement.match_type == &quot;equals&quot;:
-                                        matchType = dsattributes.eDSExact
-                                elif textMatchElement.match_type == &quot;starts-with&quot;:
-                                        matchType = dsattributes.eDSStartsWith
-                                elif textMatchElement.match_type == &quot;ends-with&quot;:
-                                        matchType = dsattributes.eDSEndsWith
-
-                            matchList = []
-                            for matchString in matchStrings:
-                                matchList += [dsquery.match(attrName, matchString, matchType) for attrName in stringAttrStrs]
-
-                            matchList = list(set(matchList))
-
-                            if textMatchElement.negate:
-                                if len(matchList) &gt; 1:
-                                    expr = dsquery.expression(dsquery.expression.OR, matchList)
-                                else:
-                                    expr = matchList
-                                return (False, queryAttributes, [dsquery.expression(dsquery.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, IsNotDefined):
-                    return definedExpression(False, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
-
-                paramFilterElements = [paramFilterElement for paramFilterElement in propFilter.filters if isinstance(paramFilterElement, ParameterFilter)]
-                textMatchElements = [textMatchElement for textMatchElement in propFilter.filters if isinstance(textMatchElement, TextMatch)]
-                propFilterAllOf = propFilter.propfilter_test == &quot;allof&quot;
-
-                # handle parameter filter elements
-                if len(paramFilterElements) &gt; 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 &amp;= textMatchNeedsAllRecords
-                    else:
-                        propFilterNeedsAllRecords |= textMatchNeedsAllRecords
-                    propFilterAttributes += textMatchExpressionAttributes
-                    propFilterExpressionList += textMatchExpression
-
-                if (len(propFilterExpressionList) &gt; 1) and (filterAllOf != propFilterAllOf):
-                    propFilterExpressions = [dsquery.expression(dsquery.expression.AND if propFilterAllOf else dsquery.expression.OR , list(set(propFilterExpressionList)))] # remove duplicates
-                else:
-                    propFilterExpressions = list(set(propFilterExpressionList))
-
-                return (propFilterNeedsAllRecords, propFilterAttributes, propFilterExpressions)
-                #end propFilterExpression
-
-            #print(&quot;propFilterListQuery: filterAllOf=%r, propFilters=%r&quot; % (filterAllOf, propFilters,))
-            &quot;&quot;&quot;
-            Create an expression for a list of prop-filter elements.
-
-            @param filterAllOf: the C{True} if parent filter test is &quot;allof&quot;
-            @param propFilters: the C{list} of L{ComponentFilter} elements.
-            @return: (needsAllRecords, espressionAttributes, expression) tuple
-            &quot;&quot;&quot;
-            needsAllRecords = filterAllOf
-            attributes = []
-            expressions = []
-            for propFilter in propFilters:
-
-                propNeedsAllRecords, propExpressionAttributes, propExpression = propFilterExpression(filterAllOf, propFilter)
-                if filterAllOf:
-                    needsAllRecords &amp;= propNeedsAllRecords
-                else:
-                    needsAllRecords |= propNeedsAllRecords
-                attributes += propExpressionAttributes
-                expressions += propExpression
-
-            if len(expressions) &gt; 1:
-                expr = dsquery.expression(dsquery.expression.AND if filterAllOf else dsquery.expression.OR , list(set(expressions))) # remove duplicates
-            elif len(expressions):
-                expr = expressions[0]
-            else:
-                expr = None
-
-            return (needsAllRecords, attributes, expr)
-
-        #print(&quot;_getDSFilter&quot;)
-        # 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 == &quot;allof&quot;
-            if len(addressBookFilter.children) &gt; 0:
-                return propFilterListQuery(filterAllOf, addressBookFilter.children)
-            else:
-                return (filterAllOf, [], [])
-        else:
-            return (False, [], [])
-
-
-    def _attributesForAddressBookQuery(self, addressBookQuery):
-
-        propertyNames = []
-        #print( &quot;addressBookQuery.qname=%r&quot; % addressBookQuery.qname)
-        if addressBookQuery.qname() == (&quot;DAV:&quot;, &quot;prop&quot;):
-
-            for property in addressBookQuery.children:
-                #print(&quot;property = %r&quot; % property )
-                if isinstance(property, carddavxml.AddressData):
-                    for addressProperty in property.children:
-                        #print(&quot;addressProperty = %r&quot; % addressProperty )
-                        if isinstance(addressProperty, carddavxml.Property):
-                            #print(&quot;Adding property %r&quot;, addressProperty.attributes[&quot;name&quot;])
-                            propertyNames.append(addressProperty.attributes[&quot;name&quot;])
-
-                elif not self.fakeETag and property.qname() == (&quot;DAV:&quot;, &quot;getetag&quot;):
-                    # for a real etag == md5(vCard), we need all attributes
-                    propertyNames = None
-                    break
-
-        if not len(propertyNames):
-            #print(&quot;using all attributes&quot;)
-            return self.returnedAttributes
-
-        else:
-            propertyNames.append(&quot;X-INTERNAL-MINIMUM-VCARD-PROPERTIES&quot;) # these properties are required to make a vCard
-            queryAttributes = []
-            for prop in propertyNames:
-                if prop in VCardRecord.dsqueryAttributesForProperty:
-                    #print(&quot;adding attributes %r&quot; % VCardRecord.dsqueryAttributesForProperty.get(prop))
-                    queryAttributes += VCardRecord.dsqueryAttributesForProperty.get(prop)
-
-            return list(set(queryAttributes).intersection(set(self.returnedAttributes)))
-
-
-    @inlineCallbacks
-    def cacheVCardsForAddressBookQuery(self, addressBookFilter, addressBookQuery, maxResults):
-        &quot;&quot;&quot;
-        Cache the vCards for a given addressBookFilder and addressBookQuery
-        &quot;&quot;&quot;
-        startTime = time.time()
-        #print(&quot;Timing: cacheVCardsForAddressBookQuery.starttime=%f&quot; % startTime)
-
-        allRecords, filterAttributes, dsFilter = self._getDSFilter(addressBookFilter)
-        #print(&quot;allRecords = %s, query = %s&quot; % (allRecords, &quot;None&quot; 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 &gt; 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(&quot;Timing: Cache fill: %.1f ms&quot; % (elaspedTime * 1000,))
-
-        returnValue((updateLock, limited))
-
-
-    @inlineCallbacks
-    def vCardRecordsForAddressBookQuery(self, addressBookFilter, addressBookQuery, maxResults):
-        &quot;&quot;&quot;
-        Get vCards for a given addressBookFilder and addressBookQuery
-        &quot;&quot;&quot;
-
-        allRecords, filterAttributes, dsFilter = self._getDSFilter(addressBookFilter)
-        #print(&quot;allRecords = %s, query = %s&quot; % (allRecords, &quot;None&quot; 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 &gt; 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(&quot;Could not get vcard for record %s&quot; % (record,))
-                else:
-                    if not record.firstValueForAttribute(dsattributes.kDSNAttrMetaNodeLocation).startswith(&quot;/Local&quot;):
-                        self.log.debug(&quot;VCard text =\n%s&quot; % (vCardText,))
-                    queryRecords.append(record)
-
-        returnValue((queryRecords, limited,))
-
-
-
-class VCardRecord(DirectoryRecord, DAVPropertyMixIn):
-    &quot;&quot;&quot;
-    Open Directory implementation of L{IDirectoryRecord}.
-    &quot;&quot;&quot;
-
-    # od attributes that may contribute to vcard properties
-    # will be used to translate vCard queries to od queries
-
-    dsqueryAttributesForProperty = {
-
-        &quot;FN&quot; : [
-               dsattributes.kDS1AttrFirstName,
-               dsattributes.kDS1AttrLastName,
-               dsattributes.kDS1AttrMiddleName,
-               dsattributes.kDSNAttrNamePrefix,
-               dsattributes.kDSNAttrNameSuffix,
-               dsattributes.kDS1AttrDistinguishedName,
-               dsattributes.kDSNAttrRecordName,
-               ],
-        &quot;N&quot; : [
-               dsattributes.kDS1AttrFirstName,
-               dsattributes.kDS1AttrLastName,
-               dsattributes.kDS1AttrMiddleName,
-               dsattributes.kDSNAttrNamePrefix,
-               dsattributes.kDSNAttrNameSuffix,
-               dsattributes.kDS1AttrDistinguishedName,
-               dsattributes.kDSNAttrRecordName,
-               ],
-        &quot;NICKNAME&quot; : [
-                dsattributes.kDSNAttrNickName,
-                ],
-        # no binary searching
-        &quot;PHOTO&quot; : [
-                (dsattributes.kDSNAttrJPEGPhoto, &quot;base64&quot;),
-                ],
-        &quot;BDAY&quot; : [
-                dsattributes.kDS1AttrBirthday,
-                ],
-        &quot;ADR&quot; : [
-                dsattributes.kDSNAttrBuilding,
-                dsattributes.kDSNAttrStreet,
-                dsattributes.kDSNAttrCity,
-                dsattributes.kDSNAttrState,
-                dsattributes.kDSNAttrPostalCode,
-                dsattributes.kDSNAttrCountry,
-                ],
-        &quot;LABEL&quot; : [
-                dsattributes.kDSNAttrPostalAddress,
-                dsattributes.kDSNAttrPostalAddressContacts,
-                dsattributes.kDSNAttrAddressLine1,
-                dsattributes.kDSNAttrAddressLine2,
-                dsattributes.kDSNAttrAddressLine3,
-                ],
-         &quot;TEL&quot; : [
-                dsattributes.kDSNAttrPhoneNumber,
-                dsattributes.kDSNAttrMobileNumber,
-                dsattributes.kDSNAttrPagerNumber,
-                dsattributes.kDSNAttrHomePhoneNumber,
-                dsattributes.kDSNAttrPhoneContacts,
-                dsattributes.kDSNAttrFaxNumber,
-                #dsattributes.kDSNAttrAreaCode,
-                ],
-         &quot;EMAIL&quot; : [
-                dsattributes.kDSNAttrEMailAddress,
-                dsattributes.kDSNAttrEMailContacts,
-                ],
-         &quot;GEO&quot; : [
-                dsattributes.kDSNAttrMapCoordinates,
-                ],
-         &quot;TITLE&quot; : [
-                dsattributes.kDSNAttrJobTitle,
-                ],
-         &quot;ORG&quot; : [
-                dsattributes.kDSNAttrCompany,
-                dsattributes.kDSNAttrOrganizationName,
-                dsattributes.kDSNAttrDepartment,
-                ],
-         &quot;NOTE&quot; : [
-                dsattributes.kDS1AttrComment,
-                dsattributes.kDS1AttrNote,
-                ],
-         &quot;REV&quot; : [
-                dsattributes.kDS1AttrModificationTimestamp,
-                ],
-         &quot;UID&quot; : [
-                dsattributes.kDS1AttrGeneratedUID,
-                # special cased
-                #dsattributes.kDSNAttrMetaNodeLocation,
-                #dsattributes.kDSNAttrRecordName,
-                #dsattributes.kDS1AttrDistinguishedName,
-                ],
-         &quot;URL&quot; : [
-                dsattributes.kDS1AttrWeblogURI,
-                dsattributes.kDSNAttrURL,
-                ],
-         &quot;KEY&quot; : [
-                # check on format, are these all binary?
-                (dsattributes.kDSNAttrPGPPublicKey, &quot;base64&quot;),
-                (dsattributes.kDS1AttrUserCertificate, &quot;base64&quot;),
-                (dsattributes.kDS1AttrUserPKCS12Data, &quot;base64&quot;),
-                (dsattributes.kDS1AttrUserSMIMECertificate, &quot;base64&quot;),
-                ],
-         # too bad this is not one X-Attribute with params.     Would make searching easier
-         &quot;X-AIM&quot; : [
-                dsattributes.kDSNAttrIMHandle,
-                ],
-         &quot;X-JABBER&quot; : [
-                dsattributes.kDSNAttrIMHandle,
-                ],
-         &quot;X-MSN&quot; : [
-                dsattributes.kDSNAttrIMHandle,
-                ],
-         &quot;X-YAHOO&quot; : [
-                dsattributes.kDSNAttrIMHandle,
-                ],
-         &quot;X-ICQ&quot; : [
-                dsattributes.kDSNAttrIMHandle,
-                ],
-         &quot;X-ABRELATEDNAMES&quot; : [
-                dsattributes.kDSNAttrRelationships,
-                ],
-          &quot;X-INTERNAL-MINIMUM-VCARD-PROPERTIES&quot; : [
-                dsattributes.kDS1AttrGeneratedUID,
-                dsattributes.kDSNAttrMetaNodeLocation,
-                dsattributes.kDS1AttrFirstName,
-                 dsattributes.kDS1AttrLastName,
-                dsattributes.kDS1AttrMiddleName,
-                   dsattributes.kDSNAttrNamePrefix,
-                  dsattributes.kDSNAttrNameSuffix,
-                 dsattributes.kDS1AttrDistinguishedName,
-                dsattributes.kDSNAttrRecordName,
-                dsattributes.kDSNAttrRecordType,
-                dsattributes.kDS1AttrModificationTimestamp,
-                dsattributes.kDS1AttrCreationTimestamp,
-                ],
-          &quot;X-INTERNAL-REQUIRED&quot; : [
-                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 = &quot;-&quot; + OpenDirectoryBackingService.baseGUID + &quot;-&quot;
-    userUIDSeparator = &quot;-bf07a1a2-&quot;
-    peopleUIDSeparator = &quot;-cf07a1a2-&quot;
-
-    constantProperties = {
-        # 3.6.3 PRODID Type Definition
-        &quot;PRODID&quot;: vCardProductID,
-        # 3.6.9 VERSION Type Definition
-        &quot;VERSION&quot;: &quot;3.0&quot;,
-        }
-
-
-    def __init__(self, service, recordAttributes, defaultNodeName=None):
-
-        self.log.debug(&quot;service=%s, attributes=%s&quot; % (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(&quot;utf8&quot;) for val in values]
-                else:
-                    self.attributes[key] = removeControlChars(values).decode(&quot;utf8&quot;)
-            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 == &quot;/LDAPv3/127.0.0.1&quot;:
-            node = defaultNodeName if defaultNodeName else service.realmName
-            self.attributes[dsattributes.kDSNAttrMetaNodeLocation] = node
-
-        guid = self.firstValueForAttribute(dsattributes.kDS1AttrGeneratedUID)
-        if not guid:
-            if service.standardizeSyntheticUIDs:
-                nodeUUIDStr = &quot;00000000&quot;
-            else:
-                nodeUUIDStr = &quot;%x&quot; % abs(hash(node))
-            nameUUIDStr = &quot;&quot;.join(self.firstValueForAttribute(dsattributes.kDSNAttrRecordName).encode(&quot;utf8&quot;).encode(&quot;base64&quot;).split(&quot;\n&quot;))
-            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 = &quot;/&quot;.join(guid.split(&quot;:&quot;)).upper()
-        self.attributes[dsattributes.kDS1AttrGeneratedUID] = guid
-
-        if self.firstValueForAttribute(dsattributes.kDS1AttrLastName) == &quot;99&quot;:
-            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 &quot;&lt;%s[%s(%s)] %s(%s) %r&gt;&quot; % (
-            self.__class__.__name__,
-            self.firstValueForAttribute(dsattributes.kDSNAttrRecordType),
-            self.firstValueForAttribute(dsattributes.kDSNAttrMetaNodeLocation),
-            self.guid,
-            self.shortNames,
-            self.fullName
-        )
-
-
-    def __hash__(self):
-        s = &quot;&quot;.join([
-              &quot;%s:%s&quot; % (attribute, self.valuesForAttribute(attribute),)
-              for attribute in self.attributes
-              ])
-        return hash(s)
-
-    &quot;&quot;&quot;
-    def nextFileName(self):
-        self.renameCounter += 1
-        self.fileName = self.baseFileName + &quot;-&quot; + str(self.renameCounter)
-        self.fileNameLower = self.fileName.lower()
-    &quot;&quot;&quot;
-
-    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.encode(&quot;utf-8&quot;) if isinstance(value, unicode) else value) for value in values if len(value) &gt; 0]
-
-        if len(nonEmptyValues) &gt; 0:
-            return nonEmptyValues
-        else:
-            return default_values
-
-
-    def firstValueForAttribute(self, attributeName, default_value=&quot;&quot;):
-        values = self.attributes.get(attributeName)
-        if values is None:
-            return default_value
-        elif isinstance(values, list):
-            return values[0].encode(&quot;utf_8&quot;) if isinstance(values[0], unicode) else values[0]
-        else:
-            return values.encode(&quot;utf_8&quot;) if isinstance(values, unicode) else values
-
-
-    def joinedValuesForAttribute(self, attributeName, separator=&quot;,&quot;, default_string=&quot;&quot;):
-        values = self.valuesForAttribute(attributeName, None)
-        if not values:
-            return default_string
-        else:
-            return separator.join(values)
-
-
-    def isoDateStringForDateAttribute(self, attributeName, default_string=&quot;&quot;):
-        modDate = self.firstValueForAttribute(attributeName, default_string)
-        revDate = None
-        if modDate:
-            if len(modDate) &gt;= len(&quot;YYYYMMDD&quot;) and modDate[:8].isdigit():
-                revDate = &quot;%s-%s-%s&quot; % (modDate[:4], modDate[4:6], modDate[6:8],)
-            if len(modDate) &gt;= len(&quot;YYYYMMDDHHMMSS&quot;) and modDate[8:14].isdigit():
-                revDate += &quot;T%s:%s:%sZ&quot; % (modDate[8:10], modDate[10:12], modDate[12:14],)
-        return revDate
-
-
-    def vCard(self):
-
-
-        def generateVCard():
-
-            def isUniqueProperty(vcard, newProperty, ignoreParams=None):
-                existingProperties = vcard.properties(newProperty.name())
-                for existingProperty in existingProperties:
-                    if ignoreParams:
-                        existingProperty = existingProperty.duplicate()
-                        for paramname, paramvalue in ignoreParams:
-                            existingProperty.removeParameterValue(paramname, paramvalue)
-                    if existingProperty == newProperty:
-                        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(&quot;Ignoring attribute %r with value %r in creating property %r. A duplicate property already exists.&quot; % (attrType, attrValue, newProperty,))
-
-            def addPropertyAndLabel(groupCount, label, propertyName, propertyValue, parameters=None):
-                groupCount[0] += 1
-                groupPrefix = &quot;item%d&quot; % groupCount[0]
-                vcard.addProperty(Property(propertyName, propertyValue, params=parameters, group=groupPrefix))
-                vcard.addProperty(Property(&quot;X-ABLabel&quot;, 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(&quot;|&quot;)
-                            if len(splitValue) &gt; 1:
-                                attrValue = splitValue[0]
-
-                        colonIndex = attrValue.find(&quot;:&quot;)
-                        if (colonIndex &gt; len(attrValue) - 2):
-                            raise ValueError(&quot;Nothing after colon.&quot;)
-
-                        propertyValue = attrValue[colonIndex + 1:]
-                        labelString = attrValue[:colonIndex] if colonIndex &gt; 0 else defaultLabel
-                        paramTypeString = labelString.upper()
-
-                        # add PREF to first prop's parameters
-                        paramTypeStrings = [paramTypeString, ]
-                        if preferred and &quot;PREF&quot; != paramTypeString:
-                            paramTypeStrings += [&quot;PREF&quot;, ]
-                        parameters = {&quot;TYPE&quot;: 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(&quot;addPropertiesAndLabelsForPrefixedAttribute(): groupCount=%r, propertyPrefix=%r, propertyName=%r, nolabelParamTypes=%r, labelMap=%r, attrType=%r&quot; % (groupCount[0], propertyPrefix, propertyName, nolabelParamTypes, labelMap, attrType,))
-                        self.log.error(&quot;addPropertiesAndLabelsForPrefixedAttribute(): Trouble parsing attribute %s, with value \&quot;%s\&quot;.  Error = %s&quot; % (attrType, attrValue, e,))
-
-            #print(&quot;VCardRecord.vCard&quot;)
-            # create vCard
-            vcard = Component(&quot;VCARD&quot;)
-            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(&quot;FN&quot;, 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
-            # 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, &quot;&quot;)
-
-            nameObject = N(
-                first=self.valuesForAttribute(dsattributes.kDS1AttrFirstName, &quot;&quot;),
-                last=familyName,
-                middle=self.valuesForAttribute(dsattributes.kDS1AttrMiddleName, &quot;&quot;),
-                prefix=self.valuesForAttribute(dsattributes.kDSNAttrNamePrefix, &quot;&quot;),
-                suffix=self.valuesForAttribute(dsattributes.kDSNAttrNameSuffix, &quot;&quot;),
-            )
-            vcard.addProperty(Property(&quot;N&quot;, 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
-            vcard.addProperty(Property(&quot;FN&quot;, nameObject.getFullName()))
-
-            # 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(&quot;NICKNAME&quot;, 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(&quot;PHOTO&quot;, photo, params={&quot;ENCODING&quot;: [&quot;b&quot;, ], &quot;TYPE&quot;: [&quot;JPEG&quot;, ], }), 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(&quot;BDAY&quot;, DateTime.parseText(birthdate, fullISO=True)))
-
-            # 3.2 Delivery Addressing Types http://tools.ietf.org/html/rfc2426#section-3.2
-            #
-            # 3.2.1 ADR Type Definition
-
-            #address
-            # vcard says: Each address attribute can be a string or a list of strings.
-            extended = self.valuesForAttribute(dsattributes.kDSNAttrBuilding, &quot;&quot;)
-            street = self.valuesForAttribute(dsattributes.kDSNAttrStreet, &quot;&quot;)
-            city = self.valuesForAttribute(dsattributes.kDSNAttrCity, &quot;&quot;)
-            region = self.valuesForAttribute(dsattributes.kDSNAttrState, &quot;&quot;)
-            code = self.valuesForAttribute(dsattributes.kDSNAttrPostalCode, &quot;&quot;)
-            country = self.valuesForAttribute(dsattributes.kDSNAttrCountry, &quot;&quot;)
-
-            if len(extended) &gt; 0 or len(street) &gt; 0 or len(city) &gt; 0 or len(region) &gt; 0 or len(code) &gt; 0 or len(country) &gt; 0:
-                vcard.addProperty(Property(&quot;ADR&quot;,
-                    Adr(
-                        #pobox = box,
-                        extended=extended,
-                        street=street,
-                        locality=city,
-                        region=region,
-                        postalcode=code,
-                        country=country,
-                    ),
-                    params={&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;POSTAL&quot;, &quot;PARCEL&quot;, ], }
-                ))
-
-            # 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(&quot;LABEL&quot;, label, params={&quot;TYPE&quot;: [&quot;POSTAL&quot;, &quot;PARCEL&quot;, ]}), None, dsattributes.kDSNAttrPostalAddress, label)
-
-            for label in self.valuesForAttribute(dsattributes.kDSNAttrPostalAddressContacts):
-                addUniqueProperty(vcard, Property(&quot;LABEL&quot;, label, params={&quot;TYPE&quot;: [&quot;POSTAL&quot;, &quot;PARCEL&quot;, ]}), None, dsattributes.kDSNAttrPostalAddressContacts, label)
-
-            address = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine1)
-            addressLine2 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine2)
-            if len(addressLine2) &gt; 0:
-                address += &quot;\n&quot; + addressLine2
-            addressLine3 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine3)
-            if len(addressLine3) &gt; 0:
-                address += &quot;\n&quot; + addressLine3
-
-            if len(address) &gt; 0:
-                vcard.addProperty(Property(&quot;LABEL&quot;, address, params={&quot;TYPE&quot;: [&quot;POSTAL&quot;, &quot;PARCEL&quot;, ]}))
-
-            # 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 = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;VOICE&quot;, ], }
-            for phone in self.valuesForAttribute(dsattributes.kDSNAttrPhoneNumber):
-                addUniqueProperty(vcard, Property(&quot;TEL&quot;, phone, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), phone, dsattributes.kDSNAttrPhoneNumber)
-                params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;VOICE&quot;, ], }
-
-            params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;CELL&quot;, ], }
-            for phone in self.valuesForAttribute(dsattributes.kDSNAttrMobileNumber):
-                addUniqueProperty(vcard, Property(&quot;TEL&quot;, phone, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), phone, dsattributes.kDSNAttrMobileNumber)
-                params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;CELL&quot;, ], }
-
-            params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;FAX&quot;, ], }
-            for phone in self.valuesForAttribute(dsattributes.kDSNAttrFaxNumber):
-                addUniqueProperty(vcard, Property(&quot;TEL&quot;, phone, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), phone, dsattributes.kDSNAttrFaxNumber)
-                params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;FAX&quot;, ], }
-
-            params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;PAGER&quot;, ], }
-            for phone in self.valuesForAttribute(dsattributes.kDSNAttrPagerNumber):
-                addUniqueProperty(vcard, Property(&quot;TEL&quot;, phone, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), phone, dsattributes.kDSNAttrPagerNumber)
-                params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PAGER&quot;, ], }
-
-            params = {&quot;TYPE&quot;: [&quot;HOME&quot;, &quot;PREF&quot;, &quot;VOICE&quot;, ], }
-            for phone in self.valuesForAttribute(dsattributes.kDSNAttrHomePhoneNumber):
-                addUniqueProperty(vcard, Property(&quot;TEL&quot;, phone, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), phone, dsattributes.kDSNAttrHomePhoneNumber)
-                params = {&quot;TYPE&quot;: [&quot;HOME&quot;, &quot;VOICE&quot;, ], }
-
-            addPropertiesAndLabelsForPrefixedAttribute(groupCount, None, &quot;TEL&quot;, &quot;work&quot;,
-                                                        [&quot;VOICE&quot;, &quot;CELL&quot;, &quot;FAX&quot;, &quot;PAGER&quot;, ], {},
-                                                        dsattributes.kDSNAttrPhoneContacts,)
-
-            &quot;&quot;&quot;
-            # EXTEND:  Use this attribute
-            # dsattributes.kDSNAttrAreaCode,            # Area code of a user's phone number.
-            &quot;&quot;&quot;
-
-            # 3.3.2 EMAIL Type Definition
-            # dsattributes.kDSNAttrEMailAddress,        # Email address of usually a user record.
-
-            # setup some params
-            preferredWorkParams = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;INTERNET&quot;, ], }
-            workParams = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;INTERNET&quot;, ], }
-            params = preferredWorkParams
-            for emailAddress in self.valuesForAttribute(dsattributes.kDSNAttrEMailAddress):
-                addUniqueProperty(vcard, Property(&quot;EMAIL&quot;, emailAddress, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), 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@mymail.com
-
-            # check to see if parameters type are open ended. Could be any string
-            addPropertiesAndLabelsForPrefixedAttribute(groupCount, None, &quot;EMAIL&quot;, &quot;work&quot;,
-                                                        [&quot;WORK&quot;, &quot;HOME&quot;, ], {},
-                                                        dsattributes.kDSNAttrEMailContacts,)
-
-            &quot;&quot;&quot;
-            # UNIMPLEMENTED:
-            # 3.3.3 MAILER Type Definition
-            &quot;&quot;&quot;
-            # 3.4 GEOGRAPHICAL TYPES http://tools.ietf.org/html/rfc2426#section-3.4
-            &quot;&quot;&quot;
-            # UNIMPLEMENTED:
-            # 3.4.1 TZ Type Definition
-            &quot;&quot;&quot;
-            # 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(&quot;,&quot;)
-                if (len(parts) == 2):
-                    vcard.addProperty(Property(&quot;GEO&quot;, parts))
-                else:
-                    self.log.info(&quot;Ignoring malformed attribute %r with value %r. Well-formed example: 7.7,10.6.&quot; % (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(&quot;TITLE&quot;, jobTitle), None, dsattributes.kDSNAttrJobTitle, jobTitle)
-
-            &quot;&quot;&quot;
-            # UNIMPLEMENTED:
-            # 3.5.2 ROLE Type Definition
-            # 3.5.3 LOGO Type Definition
-            # 3.5.4 AGENT Type Definition
-            &quot;&quot;&quot;
-            # 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) &gt; 0 or len(department) &gt; 0:
-                vcard.addProperty(Property(&quot;ORG&quot;, (company, department, extra,),))
-
-            # 3.6 EXPLANATORY TYPES http://tools.ietf.org/html/rfc2426#section-3.6
-            &quot;&quot;&quot;
-            # UNIMPLEMENTED:
-            # 3.6.1 CATEGORIES Type Definition
-            &quot;&quot;&quot;
-            # 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(&quot;NOTE&quot;, comment), None, dsattributes.kDS1AttrComment, comment)
-
-            for note in self.valuesForAttribute(dsattributes.kDS1AttrNote):
-                addUniqueProperty(vcard, Property(&quot;NOTE&quot;, note), None, dsattributes.kDS1AttrNote, note)
-
-            # 3.6.3 PRODID Type Definition
-            #vcard.addProperty(Property(&quot;PRODID&quot;, vCardProductID + &quot;//BUILD %s&quot; % twistedcaldav.__version__))
-            #vcard.addProperty(Property(&quot;PRODID&quot;, vCardProductID))
-            # ADDED WITH CONTSTANT PROPERTIES
-
-            # 3.6.4 REV Type Definition
-            revDate = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrModificationTimestamp)
-            if revDate:
-                vcard.addProperty(Property(&quot;REV&quot;, DateTime.parseText(revDate, fullISO=True)))
-
-            &quot;&quot;&quot;
-            # UNIMPLEMENTED:
-            # 3.6.5 SORT-STRING Type Definition
-            # 3.6.6 SOUND Type Definition
-            &quot;&quot;&quot;
-            # 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 &quot;A579E95E-CDFE-4EBC-B7E7-F2158562170F&quot;.
-                                                        #      The standard format contains 32 hex characters and four hyphen characters.
-            # !! don't use self.guid which is URL encoded
-            vcard.addProperty(Property(&quot;UID&quot;, 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, &quot;weblog&quot;, &quot;URL&quot;, url, parameters={&quot;TYPE&quot;: [&quot;Weblog&quot;, ]})
-
-            for url in self.valuesForAttribute(dsattributes.kDSNAttrURL):
-                addPropertyAndLabel(groupCount, &quot;_$!&lt;HomePage&gt;!$_&quot;, &quot;URL&quot;, url, parameters={&quot;TYPE&quot;: [&quot;Homepage&quot;, ]})
-
-            # 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(&quot;KEY&quot;, key, params={&quot;ENCODING&quot;: [&quot;b&quot;, ], &quot;TYPE&quot;: [&quot;PGPPublicKey&quot;, ]}), None, dsattributes.kDSNAttrPGPPublicKey, key)
-
-            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserCertificate):
-                addUniqueProperty(vcard, Property(&quot;KEY&quot;, key, params={&quot;ENCODING&quot;: [&quot;b&quot;, ], &quot;TYPE&quot;: [&quot;UserCertificate&quot;, ]}), None, dsattributes.kDS1AttrUserCertificate, key)
-
-            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserPKCS12Data):
-                addUniqueProperty(vcard, Property(&quot;KEY&quot;, key, params={&quot;ENCODING&quot;: [&quot;b&quot;, ], &quot;TYPE&quot;: [&quot;UserPKCS12Data&quot;, ]}), None, dsattributes.kDS1AttrUserPKCS12Data, key)
-
-            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserSMIMECertificate):
-                addUniqueProperty(vcard, Property(&quot;KEY&quot;, key, params={&quot;ENCODING&quot;: [&quot;b&quot;, ], &quot;TYPE&quot;: [&quot;UserSMIMECertificate&quot;, ]}), None, dsattributes.kDS1AttrUserSMIMECertificate, key)
-
-            &quot;&quot;&quot;
-            X- attributes, Address Book support
-            &quot;&quot;&quot;
-            # 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, &quot;X-&quot;, None, &quot;aim&quot;,
-                                                        [&quot;AIM&quot;, &quot;JABBER&quot;, &quot;MSN&quot;, &quot;YAHOO&quot;, &quot;ICQ&quot;],
-                                                        {},
-                                                        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, &quot;X-ABRELATEDNAMES&quot;, &quot;friend&quot;,
-                                                        [],
-                                                        {&quot;FATHER&quot;: &quot;_$!&lt;Father&gt;!$_&quot;,
-                                                         &quot;MOTHER&quot;: &quot;_$!&lt;Mother&gt;!$_&quot;,
-                                                         &quot;PARENT&quot;: &quot;_$!&lt;Parent&gt;!$_&quot;,
-                                                         &quot;BROTHER&quot;: &quot;_$!&lt;Brother&gt;!$_&quot;,
-                                                         &quot;SISTER&quot;: &quot;_$!&lt;Sister&gt;!$_&quot;,
-                                                         &quot;CHILD&quot;: &quot;_$!&lt;Child&gt;!$_&quot;,
-                                                         &quot;FRIEND&quot;: &quot;_$!&lt;Friend&gt;!$_&quot;,
-                                                         &quot;SPOUSE&quot;: &quot;_$!&lt;Spouse&gt;!$_&quot;,
-                                                         &quot;PARTNER&quot;: &quot;_$!&lt;Partner&gt;!$_&quot;,
-                                                         &quot;ASSISTANT&quot;: &quot;_$!&lt;Assistant&gt;!$_&quot;,
-                                                         &quot;MANAGER&quot;: &quot;_$!&lt;Manager&gt;!$_&quot;, },
-                                                        dsattributes.kDSNAttrRelationships,)
-
-            # special case for Apple
-            if self.service.appleInternalServer:
-                for manager in self.valuesForAttribute(&quot;dsAttrTypeNative:appleManager&quot;):
-                    splitManager = manager.split(&quot;|&quot;)
-                    if len(splitManager) &gt;= 4:
-                        managerValue = &quot;%s %s, %s&quot; % (splitManager[0], splitManager[1], splitManager[3],)
-                    elif len(splitManager) &gt;= 2:
-                        managerValue = &quot;%s %s&quot; % (splitManager[0], splitManager[1])
-                    else:
-                        managerValue = manager
-                    addPropertyAndLabel(groupCount, &quot;_$!&lt;Manager&gt;!$_&quot;, &quot;X-ABRELATEDNAMES&quot;, managerValue, parameters={&quot;TYPE&quot;: [&quot;Manager&quot;, ]})
-
-            &quot;&quot;&quot;
-            # 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.
-
-            &quot;&quot;&quot;
-
-            # 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(&quot;X-&quot; + &quot;-&quot;.join(attribute.split(&quot;:&quot;)), 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(&quot;UID&quot;).value() + &quot;.vcf&quot;
-        #print(&quot;uriName():self._uriName=%s&quot; % self._uriName)
-        return self._uriName
-
-
-    def hRef(self, parentURI=&quot;/directory/&quot;):
-        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(&quot;VCardResource.readProperty: qname = %s&quot; % (qname, ))
-
-        if namespace == dav_namespace:
-            if name == &quot;resourcetype&quot;:
-                result = davxml.ResourceType.empty #@UndefinedVariable
-                #print(&quot;VCardResource.readProperty: qname = %s, result = %s&quot; % (qname, result))
-                return result
-            elif name == &quot;getetag&quot;:
-                result = davxml.GETETag(ETag(hashlib.md5(self.vCardText()).hexdigest()).generate())
-                #print(&quot;VCardResource.readProperty: qname = %s, result = %s&quot; % (qname, result))
-                return result
-            elif name == &quot;getcontenttype&quot;:
-                mimeType = MimeType('text', 'vcard', {})
-                result = davxml.GETContentType(generateContentType(mimeType))
-                #print(&quot;VCardResource.readProperty: qname = %s, result = %s&quot; % (qname, result))
-                return result
-            elif name == &quot;getcontentlength&quot;:
-                result = davxml.GETContentLength.fromString(str(len(self.vCardText())))
-                #print(&quot;VCardResource.readProperty: qname = %s, result = %s&quot; % (qname, result))
-                return result
-            elif name == &quot;getlastmodified&quot;:
-                if self.vCard().hasProperty(&quot;REV&quot;):
-                    modDatetime = parse_date(self.vCard().propertyValue(&quot;REV&quot;))
-                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(&quot;VCardResource.readProperty: qname = %s, result = %s&quot; % (qname, result))
-                return result
-            elif name == &quot;creationdate&quot;:
-                creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
-                if creationDateString:
-                    creationDatetime = parse_date(creationDateString)
-                elif self.vCard().hasProperty(&quot;REV&quot;):    # use modification date property if it exists
-                    creationDatetime = parse_date(self.vCard().propertyValue(&quot;REV&quot;))
-                else:
-                    creationDatetime = datetime.datetime.utcnow()
-                result = davxml.CreationDate.fromDate(creationDatetime)
-                #print(&quot;VCardResource.readProperty: qname = %s, result = %s&quot; % (qname, result))
-                return result
-            elif name == &quot;displayname&quot;:
-                # AddressBook.app uses N. Use FN or UID instead?
-                result = davxml.DisplayName.fromString(self.vCard().propertyValue(&quot;N&quot;))
-                #print(&quot;VCardResource.readProperty: qname = %s, result = %s&quot; % (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(&quot;VCardResource.listProperties()&quot;)
-        qnames = set(self.liveProperties())
-
-        # Add dynamic live properties that exist
-        dynamicLiveProperties = (
-            (dav_namespace, &quot;quota-available-bytes&quot;),
-            (dav_namespace, &quot;quota-used-bytes&quot;),
-        )
-        for dqname in dynamicLiveProperties:
-            #print(&quot;VCardResource.listProperties: removing dqname=%s&quot; % (dqname,))
-            qnames.remove(dqname)
-
-        for qname in self.deadProperties().list():
-            if (qname not in qnames) and (qname[0] != twisted_private_namespace):
-                #print(&quot;listProperties: adding qname=%s&quot; % (qname,))
-                qnames.add(qname)
-
-        #for qn in qnames: print(&quot;VCardResource.listProperties: qn=%s&quot; % (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' &lt;= a &lt;= '\x1F':
-            result = &quot;&quot;
-            for c in utf8String:
-                if '\x00' &lt;= c &lt;= '\x1F':
-                    pass
-                else:
-                    result += c
-    #if utf8String != result: print (&quot;changed %r to %r&quot; % (utf8String, result))
-    return result
</del></span></pre></div>
<a id="CalendarServerbranchesuserssagenmove2who4twistedcaldavdirectorytesttest_opendirectorybackerpy"></a>
<div class="delfile"><h4>Deleted: CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directory/test/test_opendirectorybacker.py (13036 => 13037)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directory/test/test_opendirectorybacker.py        2014-03-28 21:42:50 UTC (rev 13036)
+++ CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directory/test/test_opendirectorybacker.py        2014-03-28 21:51:51 UTC (rev 13037)
</span><span class="lines">@@ -1,55 +0,0 @@
</span><del>-##
-# Copyright (c) 2011-2014 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
-# 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 &quot;AS IS&quot; 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.
-##
-
-try:
-    from twistedcaldav.directory.opendirectorybacker import VCardRecord
-except ImportError:
-    pass
-else:
-    from twistedcaldav.test.util import TestCase
-
-    class VCardRecordTestCase(TestCase):
-
-
-        def test_multiplePhoneNumbersAndEmailAddresses(self):
-            attributes = {
-                u'dsAttrTypeStandard:AppleMetaRecordName': ['uid=odtestamanda,cn=users,dc=dalek,dc=example,dc=com'],
-                u'dsAttrTypeStandard:ModificationTimestamp': '20111017170937Z',
-                u'dsAttrTypeStandard:PhoneNumber': ['408 555-1212', '415 555-1212'],
-                u'dsAttrTypeStandard:RecordType': ['dsRecTypeStandard:Users'],
-                u'dsAttrTypeStandard:AppleMetaNodeLocation': ['/LDAPv3/127.0.0.1'],
-                u'dsAttrTypeStandard:RecordName': ['odtestamanda'],
-                u'dsAttrTypeStandard:FirstName': 'Amanda',
-                u'dsAttrTypeStandard:GeneratedUID': '9DC04A70-E6DD-11DF-9492-0800200C9A66',
-                u'dsAttrTypeStandard:LastName': 'Test',
-                u'dsAttrTypeStandard:CreationTimestamp': '20110927182945Z',
-                u'dsAttrTypeStandard:EMailAddress': ['amanda@example.com', 'second@example.com'],
-                u'dsAttrTypeStandard:RealName': 'Amanda Test',
-            }
-            vcardRecord = VCardRecord(StubService(), attributes)
-            vcard = vcardRecord.vCard()
-            properties = set([prop.value() for prop in vcard.properties(&quot;TEL&quot;)])
-            self.assertEquals(properties, set([&quot;408 555-1212&quot;, &quot;415 555-1212&quot;]))
-            properties = set([prop.value() for prop in vcard.properties(&quot;EMAIL&quot;)])
-            self.assertEquals(properties, set([&quot;amanda@example.com&quot;, &quot;second@example.com&quot;]))
-
-
-
-    class StubService(object):
-        addDSAttrXProperties = False
-        directoryBackedAddressBook = None
-        appleInternalServer = False
-        realmName = &quot;testing&quot;
</del></span></pre></div>
<a id="CalendarServerbranchesuserssagenmove2who4twistedcaldavdirectorybackedaddressbookpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directorybackedaddressbook.py (13036 => 13037)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directorybackedaddressbook.py        2014-03-28 21:42:50 UTC (rev 13036)
+++ CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directorybackedaddressbook.py        2014-03-28 21:51:51 UTC (rev 13037)
</span><span class="lines">@@ -23,17 +23,23 @@
</span><span class="cx"> ]
</span><span class="cx"> 
</span><span class="cx"> from twext.python.log import Logger
</span><ins>+from twext.who.expression import Operand, MatchType, MatchFlags, \
+    MatchExpression, CompoundExpression
+from twext.who.idirectory import FieldName, RecordType
+from twisted.internet.defer import succeed, inlineCallbacks, maybeDeferred, \
+    returnValue
+from twisted.python.constants import NamedConstant
+from twistedcaldav import carddavxml
+from twistedcaldav.config import config
+from twistedcaldav.resource import CalDAVResource
+from txdav.carddav.datastore.query.filter import IsNotDefined, TextMatch, \
+    ParameterFilter
+from txdav.who.idirectory import FieldName as CalFieldName, \
+    RecordType as CalRecordType
+from txdav.xml import element as davxml
</ins><span class="cx"> from txweb2 import responsecode
</span><del>-from txdav.xml import element as davxml
</del><span class="cx"> from txweb2.dav.resource import TwistedACLInheritable
</span><span class="cx"> from txweb2.http import HTTPError, StatusResponse
</span><del>-
-from twisted.internet.defer import succeed, inlineCallbacks, maybeDeferred, returnValue
-from twisted.python.reflect import namedClass
-
-from twistedcaldav.config import config
-from twistedcaldav.resource import CalDAVResource
-
</del><span class="cx"> import uuid
</span><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="lines">@@ -45,66 +51,31 @@
</span><span class="cx">     Directory-backed address book
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-    def __init__(self, principalCollections):
</del><ins>+    def __init__(self, principalCollections, principalDirectory, uri):
</ins><span class="cx"> 
</span><span class="cx">         CalDAVResource.__init__(self, principalCollections=principalCollections)
</span><span class="cx"> 
</span><del>-        self.directory = None       # creates directory attribute
</del><ins>+        self.principalDirectory = principalDirectory
+        self.uri = uri
+        self.directory = None
</ins><span class="cx"> 
</span><del>-        # 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.info(&quot;Created %s&quot; % (path,))
-#
-#        except (OSError,), e:
-#            # this is caused by multiprocessor race and is harmless
-#            if e.errno != errno.EEXIST:
-#                raise
</del><span class="cx"> 
</span><del>-
</del><span class="cx">     def makeChild(self, name):
</span><del>-        from twistedcaldav.simpleresource import SimpleCalDAVResource
-        return SimpleCalDAVResource(principalCollections=self.principalCollections())
</del><ins>+        return self.directory
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def provisionDirectory(self):
</span><span class="cx">         if self.directory is None:
</span><del>-            directoryClass = namedClass(config.DirectoryAddressBook.type)
-
</del><span class="cx">             log.info(
</span><del>-                &quot;Configuring: {t}:{p}&quot;,
-                t=config.DirectoryAddressBook.type,
-                p=config.DirectoryAddressBook.params,
-            )
</del><ins>+                &quot;Setting search directory to {principalDirectory}&quot;,
+                principalDirectory=self.principalDirectory)
+            self.directory = self.principalDirectory
+            # future: instantiate another directory based on /Search/Contacts (?)
</ins><span class="cx"> 
</span><del>-            #add self as &quot;directoryBackedAddressBook&quot; parameter
-            params = config.DirectoryAddressBook.params.copy()
-            params[&quot;directoryBackedAddressBook&quot;] = self
-
-            try:
-                self.directory = directoryClass(params)
-            except ImportError, e:
-                log.error(&quot;Unable to set up directory address book: %s&quot; % (e,))
-                return succeed(None)
-
-            return self.directory.createCache()
-
-            #print (&quot;DirectoryBackedAddressBookResource.provisionDirectory: provisioned&quot;)
-
</del><span class="cx">         return succeed(None)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def defaultAccessControlList(self):
</span><del>-        #print( &quot;DirectoryBackedAddressBookResource.defaultAccessControlList&quot; )
</del><span class="cx">         if config.AnonymousDirectoryAddressBookAccess:
</span><span class="cx">             # DAV:Read for all principals (includes anonymous)
</span><span class="cx">             accessPrincipal = davxml.All()
</span><span class="lines">@@ -152,7 +123,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def isAddressBookCollection(self):
</span><del>-        #print( &quot;DirectoryBackedAddressBookResource.isAddressBookCollection: return True&quot; )
</del><span class="cx">         return True
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -169,21 +139,1320 @@
</span><span class="cx">     def renderHTTP(self, request):
</span><span class="cx">         if not self.directory:
</span><span class="cx">             raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE, &quot;Service is starting up&quot;))
</span><del>-        elif self.directory.liveQuery:
-            response = (yield maybeDeferred(super(DirectoryBackedAddressBookResource, self).renderHTTP, request))
-            returnValue(response)
</del><ins>+
+        response = (yield maybeDeferred(super(DirectoryBackedAddressBookResource, self).renderHTTP, request))
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def doAddressBookDirectoryQuery(self, addressBookFilter, addressBookQuery, maxResults):
+        &quot;&quot;&quot;
+        Get vCards for a given addressBookFilter and addressBookQuery
+        &quot;&quot;&quot;
+
+        log.debug(&quot;doAddressBookDirectoryQuery: directory={directory} addressBookFilter={addressBookFilter}, addressBookQuery={addressBookQuery}, maxResults={maxResults}&quot;,
+                  directory=self.directory, addressBookFilter=addressBookFilter, addressBookQuery=addressBookQuery, maxResults=maxResults)
+        results = []
+        limited = False
+        maxQueryRecords = 0
+
+        schema = {
+            RecordType.user: {
+                &quot;FN&quot;: (
+                        FieldName.fullNames,
+                        FieldName.shortNames,
+                        ),
+                &quot;N&quot;: (
+                        FieldName.fullNames,
+                        FieldName.shortNames,
+                        ),
+                &quot;EMAIL&quot;: FieldName.emailAddresses,
+                &quot;UID&quot;: FieldName.uid,
+                &quot;ADR&quot;: CalFieldName.streetAddress,
+             },
+            RecordType.group: {
+                &quot;FN&quot;: (
+                        FieldName.fullNames,
+                        FieldName.shortNames,
+                        ),
+                &quot;N&quot;: (
+                        FieldName.fullNames,
+                        FieldName.shortNames,
+                        ),
+                &quot;EMAIL&quot;: FieldName.emailAddresses,
+                &quot;UID&quot;: FieldName.uid,
+                &quot;ADR&quot;: CalFieldName.streetAddress,
+                # LATER &quot;X-ADDRESSBOOKSERVER-MEMBER&quot;: FieldName.members,
+             },
+        }
+
+        recordTypeToKindMap = {
+                       RecordType.user: &quot;individual&quot;,
+                       RecordType.group: &quot;group&quot;,
+                       CalRecordType.location: &quot;location&quot;,
+                       CalRecordType.resource: &quot;device&quot;,
+                       }
+
+        allowedRecordTypes = set(self.directory.recordTypes()) &amp; set(recordTypeToKindMap.keys()) &amp; set(schema.keys())
+        log.debug(&quot;doAddressBookDirectoryQuery: allowedRecordTypes={allowedRecordTypes}&quot;, allowedRecordTypes=allowedRecordTypes,)
+
+        expressions = []
+        for recordType in allowedRecordTypes:
+
+            #log.debug(&quot;doAddressBookDirectoryQuery: recordType={recordType}&quot;, recordType=recordType,)
+
+            vcardPropToRecordFieldMap = schema[recordType]
+            kind = recordTypeToKindMap[recordType]
+            constantProperties = ABDirectoryQueryResult.constantProperties.copy()
+            constantProperties[&quot;KIND&quot;] = kind
+            # add KIND as constant so that query can be skipped if addressBookFilter needs a different kind
+
+            propNames, expression = expressionFromABFilter(addressBookFilter, vcardPropToRecordFieldMap, recordType=recordType, constantProperties=constantProperties)
+            log.debug(&quot;doAddressBookDirectoryQuery: recordType={recordType}, expression={expression!r}, propNames={propNames}&quot;, recordType=recordType, expression=expression, propNames=propNames)
+            if expression:
+                expressions.append(expression)
+
+        if expressions:
+            if expressions &gt; 1:
+                expression = CompoundExpression(expressions, Operand.OR)
+            else:
+                expression = expressions[0]
+
+            maxRecords = int(maxResults * 1.2)
+
+            # keep trying query till we get results based on filter.  Especially when doing &quot;all results&quot; query
+            while True:
+
+                log.debug(&quot;doAddressBookDirectoryQuery: expression={expression!r}, &quot;, expression=expression)
+
+                records = yield self.directory.recordsFromExpression(expression)
+                log.debug(&quot;doAddressBookDirectoryQuery: #records={n}, records={records!r}&quot;, n=len(records), records=records)
+                queryLimited = False
+
+                vCardsResults = []#[ABDirectoryQueryResult(self, record) for record in records]
+
+                filteredResults = []
+                for vCardResult in vCardsResults:
+                    if addressBookFilter.match(vCardResult.vCard()):
+                        filteredResults.append(vCardResult)
+                    else:
+                        log.debug(&quot;doAddressBookQuery: vCard did not match filter: {vCard}&quot;, vcard=vCardResult.vCard())
+
+                #no more results
+                if not queryLimited:
+                    break
+
+                # more than requested results
+                if maxResults and len(filteredResults) &gt;= maxResults:
+                    break
+
+                # more than max report results
+                if len(filteredResults) &gt;= config.MaxQueryWithDataResults:
+                    break
+
+                # more than self limit
+                if maxQueryRecords and maxRecords &gt;= maxQueryRecords:
+                    break
+
+                # try again with 2x
+                maxRecords *= 2
+                if maxQueryRecords and maxRecords &gt; maxQueryRecords:
+                    maxRecords = maxQueryRecords
+
+            results = sorted(list(filteredResults), key=lambda result: result.vCard().propertyValue(&quot;UID&quot;))
+            limited = maxResults and len(results) &gt;= maxResults
+
+        log.info(&quot;limited={l} result count={n}&quot;, l=limited, n=len(results))
+        returnValue((results, limited,))
+
+
+
+def propertiesInAddressBookQuery(addressBookQuery):
+    &quot;&quot;&quot;
+    Get the vCard properties requested by a given query
+    &quot;&quot;&quot;
+
+    etagRequested = False
+    propertyNames = []
+    if addressBookQuery.qname() == (&quot;DAV:&quot;, &quot;prop&quot;):
+
+        for property in addressBookQuery.children:
+            if isinstance(property, carddavxml.AddressData):
+                for addressProperty in property.children:
+                    if isinstance(addressProperty, carddavxml.Property):
+                        propertyNames += [addressProperty.attributes[&quot;name&quot;], ]
+
+            elif property.qname() == (&quot;DAV:&quot;, &quot;getetag&quot;):
+                # for a real etag == md5(vCard), we need all properties
+                etagRequested = True
+
+    return (etagRequested, propertyNames if len(propertyNames) else None)
+
+
+
+def expressionFromABFilter(addressBookFilter, vcardPropToSearchableFieldMap, recordType, constantProperties={}):
+    &quot;&quot;&quot;
+    Convert the supplied addressbook-query into a ds expression tree.
+
+    @param addressBookFilter: the L{Filter} for the addressbook-query to convert.
+    @param vcardPropToSearchableFieldMap: a mapping from vcard properties to searchable query attributes.
+    @param constantProperties: a mapping of constant properties.  A query on a constant property will return all or None
+    @return: (filterProperyNames, expressions) tuple.  expression==True means list all results, expression==False means no results
+    &quot;&quot;&quot;
+
+    def propFilterListQuery(filterAllOf, propFilters):
+
+        def combineExpressionLists(expressionList, allOf, addedExpressions):
+            &quot;&quot;&quot;
+            deal with the 4-state logic
+                addedExpressions=None means ignore
+                addedExpressions=True means all records
+                addedExpressions=False means no records
+                addedExpressions=[expressionlist] add to expression list
+            &quot;&quot;&quot;
+            if expressionList is None:
+                expressionList = addedExpressions
+            elif addedExpressions is not None:
+                if addedExpressions is True:
+                    if not allOf:
+                        expressionList = True  # expressionList or True is True
+                    #else  expressionList and True is expressionList
+                elif addedExpressions is False:
+                    if allOf:
+                        expressionList = False  # expressionList and False is False
+                    #else expressionList or False is expressionList
+                else:
+                    if expressionList is False:
+                        if not allOf:
+                            expressionList = addedExpressions  # False or addedExpressions is addedExpressions
+                        #else False and addedExpressions is False
+                    elif expressionList is True:
+                        if allOf:
+                            expressionList = addedExpressions  # False or addedExpressions is addedExpressions
+                        #else False and addedExpressions is False
+                    else:
+                        expressionList += addedExpressions
+            return expressionList
+
+
+        def propFilterExpression(filterAllOf, propFilter):
+            &quot;&quot;&quot;
+            Create an expression for a single prop-filter element.
+
+            @param propFilter: the L{PropertyFilter} element.
+            @return: (filterProperyNames, expressions) tuple.  expression==True means list all results, expression==False means no results
+            &quot;&quot;&quot;
+
+            def definedExpression(defined, allOf):
+                if constant or propFilter.filter_name in (&quot;N&quot; , &quot;FN&quot;, &quot;UID&quot;, &quot;SOURCE&quot;,):
+                    return defined  # all records have this property so no records do not have it
+                else:
+                    if defined:
+                        matchList = [MatchExpression(fieldName, u&quot;&quot;, MatchType.startsWith) for fieldName in searchableFields]
+                    else:
+                        # this may generate inefficient LDAP query stirng
+                        matchList = [MatchExpression(fieldName, u&quot;&quot;, MatchType.startsWith, MatchFlags.NOT) for fieldName in searchableFields]
+                    return andOrExpression(allOf, matchList)
+
+            def andOrExpression(propFilterAllOf, matchList):
+                matchList = list(set(matchList))
+                if propFilterAllOf and len(matchList) &gt; 1:
+                    # add OR expression because parent will AND
+                    return [CompoundExpression(matchList, Operand.OR), ]
+                else:
+                    return matchList
+
+
+            def paramFilterElementExpression(propFilterAllOf, paramFilterElement): #@UnusedVariable
+
+                params = ABDirectoryQueryResult.vcardPropToParamMap.get(propFilter.filter_name.upper())
+                defined = params and paramFilterElement.filter_name.upper() in params
+
+                #defined test
+                if defined != paramFilterElement.defined:
+                    return False
+
+                #parameter value text match
+                if defined and paramFilterElement.filters:
+                    paramValues = params[paramFilterElement.filter_name.upper()]
+                    if paramValues and paramFilterElement.filters[0].text.upper() not in paramValues:
+                        return False
+
+                return True
+
+
+            def textMatchElementExpression(propFilterAllOf, textMatchElement):
+
+                # pre process text match strings for ds query
+                def getMatchStrings(propFilter, matchString):
+
+                    if propFilter.filter_name in (&quot;REV&quot; , &quot;BDAY&quot;,):
+                        rawString = matchString
+                        matchString = &quot;&quot;
+                        for c in rawString:
+                            if not c in &quot;TZ-:&quot;:
+                                matchString += c
+                    elif propFilter.filter_name == &quot;GEO&quot;:
+                        matchString = &quot;,&quot;.join(matchString.split(&quot;;&quot;))
+
+                    if propFilter.filter_name in (&quot;N&quot; , &quot;ADR&quot;, &quot;ORG&quot;,):
+                        # for structured properties, change into multiple strings for ds query
+                        if propFilter.filter_name == &quot;ADR&quot;:
+                            #split by newline and comma
+                            rawStrings = &quot;,&quot;.join(matchString.split(&quot;\n&quot;)).split(&quot;,&quot;)
+                        else:
+                            #split by space
+                            rawStrings = matchString.split(&quot; &quot;)
+
+                        # 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:
+                    #FIXME: match is not implemented in twisteddaldav.query.Filter.TextMatch so use _match for now
+                    return textMatchElement._match([constant, ])
+                else:
+
+                    matchStrings = getMatchStrings(propFilter, textMatchElement.text)
+
+                    if not len(matchStrings):
+                        # no searching text in binary ds attributes, so change to defined/not defined case
+                        if textMatchElement.negate:
+                            return definedExpression(False, propFilterAllOf)
+                        # else fall through to attribute exists case below
+                    else:
+
+                        # use match_type where possible depending on property/attribute mapping
+                        # FIXME: case-sensitive negate will not work.  This should return all all records in that case
+                        matchType = MatchType.contains
+                        if propFilter.filter_name in (&quot;NICKNAME&quot; , &quot;TITLE&quot; , &quot;NOTE&quot; , &quot;UID&quot;, &quot;URL&quot;, &quot;N&quot;, &quot;ADR&quot;, &quot;ORG&quot;, &quot;REV&quot;, &quot;LABEL&quot;,):
+                            if textMatchElement.match_type == &quot;equals&quot;:
+                                matchType = MatchType.equals
+                            elif textMatchElement.match_type == &quot;starts-with&quot;:
+                                matchType = MatchType.startsWith
+                            elif textMatchElement.match_type == &quot;ends-with&quot;:
+                                matchType = MatchType.endsWith
+
+                        matchList = []
+                        for matchString in matchStrings:
+                            if textMatchElement.negate:
+                                matchList = [MatchExpression(fieldName, matchString.decode(&quot;utf-8&quot;), matchType, MatchFlags.NOT) for fieldName in searchableFields]
+                            else:
+                                matchList = [MatchExpression(fieldName, matchString.decode(&quot;utf-8&quot;), matchType) for fieldName in searchableFields]
+                            matchList.extend(matchList)
+                        return andOrExpression(propFilterAllOf, matchList)
+
+                # attribute exists search
+                return definedExpression(True, propFilterAllOf)
+                #end textMatchElementExpression()
+
+            # searchablePropFilterAttrNames are attributes to be used by this propfilter's expression
+            searchableFields = vcardPropToSearchableFieldMap.get(propFilter.filter_name, [])
+            if isinstance(searchableFields, NamedConstant):
+                searchableFields = (searchableFields,)
+
+            constant = constantProperties.get(propFilter.filter_name)
+            if not searchableFields and not constant:
+                # not allAttrNames means propFilter.filter_name is not mapped
+                # return None to try to match all items if this is the only property filter
+                return None
+
+            #create a textMatchElement for the IsNotDefined qualifier
+            if isinstance(propFilter.qualifier, IsNotDefined):
+                textMatchElement = TextMatch(carddavxml.TextMatch.fromString(&quot;&quot;))
+                textMatchElement.negate = True
+                propFilter.filters.append(textMatchElement)
+
+            # if only one propFilter, then use filterAllOf as propFilterAllOf to reduce subexpressions and simplify generated query string
+            if len(propFilter.filters) == 1:
+                propFilterAllOf = filterAllOf
+            else:
+                propFilterAllOf = propFilter.propfilter_test == &quot;allof&quot;
+
+            propFilterExpressions = None
+            for propFilterElement in propFilter.filters:
+                propFilterExpression = None
+                if isinstance(propFilterElement, ParameterFilter):
+                    propFilterExpression = paramFilterElementExpression(propFilterAllOf, propFilterElement)
+                elif isinstance(propFilterElement, TextMatch):
+                    propFilterExpression = textMatchElementExpression(propFilterAllOf, propFilterElement)
+                propFilterExpressions = combineExpressionLists(propFilterExpressions, propFilterAllOf, propFilterExpression)
+                if isinstance(propFilterExpressions, bool) and propFilterAllOf != propFilterExpression:
+                    break
+
+            if isinstance(propFilterExpressions, list):
+                propFilterExpressions = list(set(propFilterExpressions))
+                if propFilterExpressions and (filterAllOf != propFilterAllOf):
+                    propFilterExpressions = [CompoundExpression(propFilterExpressions, Operand.AND if propFilterAllOf else Operand.OR)]
+
+            return propFilterExpressions
+            #end propFilterExpression
+
+        &quot;&quot;&quot;
+        Create an expression for a list of prop-filter elements.
+
+        @param filterAllOf: the C{True} if parent filter test is &quot;allof&quot;
+        @param propFilters: the C{list} of L{ComponentFilter} elements.
+        @return: (filterProperyNames, expressions) tuple.  expression==True means list all results, expression==False means no results
+        &quot;&quot;&quot;
+        expressions = None
+        for propFilter in propFilters:
+
+            propExpressions = propFilterExpression(filterAllOf, propFilter)
+            expressions = combineExpressionLists(expressions, filterAllOf, propExpressions)
+
+            # early loop exit
+            if isinstance(expressions, bool) and filterAllOf != expressions:
+                break
+
+        # convert to needsAllRecords to return
+        log.debug(&quot;expressionFromABFilter: expressions={q!r}&quot;, q=expressions,)
+        if isinstance(expressions, list):
+            expressions = list(set(expressions))
+            if len(expressions) &gt; 1:
+                expr = CompoundExpression(expressions, Operand.AND if filterAllOf else Operand.OR)
+            elif len(expressions):
+                expr = expressions[0]
+            else:
+                expr = not filterAllOf  # empty expression list. should not happen
+        elif expressions is None:
+            expr = not filterAllOf
</ins><span class="cx">         else:
</span><del>-            available = (yield maybeDeferred(self.directory.available,))
</del><ins>+            # True or False
+            expr = expressions
</ins><span class="cx"> 
</span><del>-            if not available:
-                raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE, &quot;Service is starting up&quot;))
</del><ins>+        properties = [propFilter.filter_name for propFilter in propFilters]
+
+        return (tuple(set(properties)), expr)
+
+    # Assume the filter is valid
+
+    # Top-level filter contains zero or more prop-filters
+    properties = tuple()
+    expression = None
+    if addressBookFilter:
+        filterAllOf = addressBookFilter.filter_test == &quot;allof&quot;
+        if len(addressBookFilter.children):
+            properties, expression = propFilterListQuery(filterAllOf, addressBookFilter.children)
+        else:
+            expression = not filterAllOf
+
+    #log.debug(&quot;expressionFromABFilter: recordType={rdn!r}, expression={q!r}, properties={pn}&quot;, rdn=recordType, q=expression, pn=properties)
+    if expression:
+        recordTypeExpression = MatchExpression(FieldName.recordType, recordType, MatchType.equals)
+        if expression is True:
+            expression = recordTypeExpression
+        else:
+            expression = CompoundExpression((expression, recordTypeExpression,), Operand.AND)
+
+    #log.debug(&quot;expressionFromABFilter: expression={q!r}, properties={pn}&quot;, q=expression, pn=properties)
+    return((properties, expression))
+
+
+
+#===============================================================================
+# Taken from obsolete twistedcaldav.directory.opendirctorybacker
+# Work in Progress
+#===============================================================================
+
+from calendarserver.platform.darwin.od import dsattributes
+from pycalendar.datetime import DateTime
+from pycalendar.vcard.adr import Adr
+from pycalendar.vcard.n import N
+from twisted.internet.defer import deferredGenerator
+from twistedcaldav.vcard import Component, Property, vCardProductID
+from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
+from txweb2.dav.resource import DAVPropertyMixIn
+from txweb2.dav.util import joinURL
+from txweb2.http_headers import MimeType, generateContentType, ETag
+from xmlrpclib import datetime
+import hashlib
+
+addSourceProperty = False
+
+
+class ABDirectoryQueryResult(DAVPropertyMixIn):
+    &quot;&quot;&quot;
+    Result from ab query report or multiget on directory
+    &quot;&quot;&quot;
+
+    log = Logger()
+
+    # od attributes that may contribute to vcard properties
+    # will be used to translate vCard queries to od queries
+
+    vcardPropToDSAttrMap = {
+
+        &quot;FN&quot;: [
+               dsattributes.kDS1AttrFirstName,
+               dsattributes.kDS1AttrLastName,
+               dsattributes.kDS1AttrMiddleName,
+               dsattributes.kDSNAttrNamePrefix,
+               dsattributes.kDSNAttrNameSuffix,
+               dsattributes.kDS1AttrDistinguishedName,
+               dsattributes.kDSNAttrRecordName,
+               ],
+        &quot;N&quot;: [
+               dsattributes.kDS1AttrFirstName,
+               dsattributes.kDS1AttrLastName,
+               dsattributes.kDS1AttrMiddleName,
+               dsattributes.kDSNAttrNamePrefix,
+               dsattributes.kDSNAttrNameSuffix,
+               dsattributes.kDS1AttrDistinguishedName,
+               dsattributes.kDSNAttrRecordName,
+               ],
+        &quot;NICKNAME&quot;: [
+                dsattributes.kDSNAttrNickName,
+                ],
+        # no binary searching
+        &quot;PHOTO&quot;: [
+                (dsattributes.kDSNAttrJPEGPhoto, &quot;base64&quot;),
+                ],
+        &quot;BDAY&quot;: [
+                dsattributes.kDS1AttrBirthday,
+                ],
+        &quot;ADR&quot;: [
+                dsattributes.kDSNAttrBuilding,
+                dsattributes.kDSNAttrStreet,
+                dsattributes.kDSNAttrCity,
+                dsattributes.kDSNAttrState,
+                dsattributes.kDSNAttrPostalCode,
+                dsattributes.kDSNAttrCountry,
+                ],
+        &quot;LABEL&quot;: [
+                dsattributes.kDSNAttrPostalAddress,
+                dsattributes.kDSNAttrPostalAddressContacts,
+                dsattributes.kDSNAttrAddressLine1,
+                dsattributes.kDSNAttrAddressLine2,
+                dsattributes.kDSNAttrAddressLine3,
+                ],
+         &quot;TEL&quot;: [
+                dsattributes.kDSNAttrPhoneNumber,
+                dsattributes.kDSNAttrMobileNumber,
+                dsattributes.kDSNAttrPagerNumber,
+                dsattributes.kDSNAttrHomePhoneNumber,
+                dsattributes.kDSNAttrPhoneContacts,
+                dsattributes.kDSNAttrFaxNumber,
+                #dsattributes.kDSNAttrAreaCode,
+                ],
+         &quot;EMAIL&quot;: [
+                dsattributes.kDSNAttrEMailAddress,
+                dsattributes.kDSNAttrEMailContacts,
+                ],
+         &quot;GEO&quot;: [
+                dsattributes.kDSNAttrMapCoordinates,
+                ],
+         &quot;TITLE&quot;: [
+                dsattributes.kDSNAttrJobTitle,
+                ],
+         &quot;ORG&quot;: [
+                dsattributes.kDSNAttrCompany,
+                dsattributes.kDSNAttrOrganizationName,
+                dsattributes.kDSNAttrDepartment,
+                ],
+         &quot;NOTE&quot;: [
+                dsattributes.kDS1AttrComment,
+                dsattributes.kDS1AttrNote,
+                ],
+         &quot;REV&quot;: [
+                dsattributes.kDS1AttrModificationTimestamp,
+                ],
+         &quot;UID&quot;: [
+                dsattributes.kDS1AttrGeneratedUID,
+                dsattributes.kDSNAttrRecordName,
+                ],
+         &quot;URL&quot;: [
+                dsattributes.kDS1AttrWeblogURI,
+                dsattributes.kDSNAttrURL,
+                ],
+         &quot;KEY&quot;: [
+                (dsattributes.kDSNAttrPGPPublicKey, &quot;base64&quot;),
+                (dsattributes.kDS1AttrUserCertificate, &quot;base64&quot;),
+                (dsattributes.kDS1AttrUserPKCS12Data, &quot;base64&quot;),
+                (dsattributes.kDS1AttrUserSMIMECertificate, &quot;base64&quot;),
+                ],
+         &quot;IMPP&quot;: [
+                dsattributes.kDSNAttrIMHandle,
+                ],
+         &quot;X-ABRELATEDNAMES&quot;: [
+                dsattributes.kDSNAttrRelationships,
+                ],
+         &quot;SOURCE&quot;: [
+                dsattributes.kDS1AttrGeneratedUID,
+                dsattributes.kDSNAttrRecordName,
+                ],
+    }
+
+    allDSQueryAttributes = list(set([attr for lookupAttributes in vcardPropToDSAttrMap.values()
+                                      for attr in lookupAttributes]))
+    binaryDSAttrNames = [attr[0] for attr in allDSQueryAttributes
+                                if isinstance(attr, tuple)]
+    stringDSAttrNames = [attr for attr in allDSQueryAttributes
+                                if isinstance(attr, str)]
+    allDSAttrNames = stringDSAttrNames + binaryDSAttrNames
+
+    # all possible generated parameters.
+    vcardPropToParamMap = {
+        &quot;PHOTO&quot;: {&quot;ENCODING&quot;: (&quot;B&quot;,), &quot;TYPE&quot;: (&quot;JPEG&quot;,), },
+        &quot;ADR&quot;: {&quot;TYPE&quot;: (&quot;WORK&quot;, &quot;PREF&quot;, &quot;POSTAL&quot;, &quot;PARCEL&quot;,), },
+        &quot;LABEL&quot;: {&quot;TYPE&quot;: (&quot;POSTAL&quot;, &quot;PARCEL&quot;,)},
+        &quot;TEL&quot;: {&quot;TYPE&quot;: None, },  # None means param can contain can be anything
+        &quot;EMAIL&quot;: {&quot;TYPE&quot;: None, },
+        &quot;KEY&quot;: {&quot;ENCODING&quot;: (&quot;B&quot;,), &quot;TYPE&quot;: (&quot;PGPPUBILICKEY&quot;, &quot;USERCERTIFICATE&quot;, &quot;USERPKCS12DATA&quot;, &quot;USERSMIMECERTIFICATE&quot;,)},
+        &quot;URL&quot;: {&quot;TYPE&quot;: (&quot;WEBLOG&quot;, &quot;HOMEPAGE&quot;,)},
+        &quot;IMPP&quot;: {&quot;TYPE&quot;: (&quot;PREF&quot;,), &quot;X-SERVICE-TYPE&quot;: None, },
+        &quot;X-ABRELATEDNAMES&quot;: {&quot;TYPE&quot;: None, },
+        &quot;X-AIM&quot;: {&quot;TYPE&quot;: (&quot;PREF&quot;,), },
+        &quot;X-JABBER&quot;: {&quot;TYPE&quot;: (&quot;PREF&quot;,), },
+        &quot;X-MSN&quot;: {&quot;TYPE&quot;: (&quot;PREF&quot;,), },
+        &quot;X-ICQ&quot;: {&quot;TYPE&quot;: (&quot;PREF&quot;,), },
+    }
+
+    uidSeparator = &quot;-cf07a1a2-&quot;
+
+    constantProperties = {
+        # 3.6.3 PRODID Type Definition
+        &quot;PRODID&quot;: vCardProductID,
+        # 3.6.9 VERSION Type Definition
+        &quot;VERSION&quot;: &quot;3.0&quot;,
+        }
+
+
+    def __init__(self, directoryBackedAddressBook, recordAttributes,
+                 kind=None,
+                 additionalVCardProps=None,
+                 ):
+
+        self.log.debug(&quot;directoryBackedAddressBook={directoryBackedAddressBook}, attributes={attributes}, additionalVCardProps={additionalVCardProps}&quot;,
+                       directoryBackedAddressBook=directoryBackedAddressBook, attributes=recordAttributes, additionalVCardProps=additionalVCardProps,)
+
+        constantProperties = ABDirectoryQueryResult.constantProperties.copy()
+        if additionalVCardProps:
+            for key, value in additionalVCardProps.iteritems():
+                if key not in constantProperties:
+                    constantProperties[key] = value
+        self.constantProperties = constantProperties
+        self.log.debug(&quot;directoryBackedAddressBook={directoryBackedAddressBook}, attributes={attributes}, constantProperties={constantProperties}&quot;,
+                       directoryBackedAddressBook=directoryBackedAddressBook, attributes=recordAttributes, constantProperties=self.constantProperties,)
+
+        self._directoryBackedAddressBook = directoryBackedAddressBook
+        self._vCard = None
+
+        #clean attributes
+        self.attributes = {}
+        for key, values in recordAttributes.items():
+            if key in ABDirectoryQueryResult.stringDSAttrNames:
+                if isinstance(values, list):
+                    self.attributes[key] = [removeControlChars(val).decode(&quot;utf8&quot;) for val in values]
+                else:
+                    self.attributes[key] = removeControlChars(values).decode(&quot;utf8&quot;)
</ins><span class="cx">             else:
</span><del>-                updateLock = self.directory.updateLock()
-                yield updateLock.acquire()
-                try:
-                    response = (yield maybeDeferred(super(DirectoryBackedAddressBookResource, self).renderHTTP, request))
</del><ins>+                self.attributes[key] = values
</ins><span class="cx"> 
</span><del>-                finally:
-                    yield updateLock.release()
</del><ins>+        # find or create guid
+        guid = self.firstValueForAttribute(dsattributes.kDS1AttrGeneratedUID)
+        if not guid:
+            nameUUIDStr = &quot;&quot;.join(self.firstValueForAttribute(dsattributes.kDSNAttrRecordName).encode(&quot;base64&quot;).split(&quot;\n&quot;))
+            guid = ABDirectoryQueryResult.uidSeparator.join([&quot;00000000&quot;, nameUUIDStr, ])
+            #guid =  ABDirectoryQueryResult.uidSeparator.join([&quot;d9a8e41b&quot;, nameUUIDStr,])
</ins><span class="cx"> 
</span><del>-                returnValue(response)
</del><ins>+            self.attributes[dsattributes.kDS1AttrGeneratedUID] = guid
+
+        if not kind:
+            dsRecordTypeToKindMap = {
+                           #dsattributes.kDSStdRecordTypePeople:&quot;individual&quot;,
+                           #dsattributes.kDSStdRecordTypeUsers:&quot;individual&quot;,
+                           dsattributes.kDSStdRecordTypeGroups: &quot;group&quot;,
+                           dsattributes.kDSStdRecordTypeLocations: &quot;location&quot;,
+                           dsattributes.kDSStdRecordTypeResources: &quot;device&quot;,
+                           }
+            recordType = self.firstValueForAttribute(dsattributes.kDSNAttrRecordType)
+            kind = dsRecordTypeToKindMap.get(recordType, &quot;individual&quot;)
+        self.kind = kind.lower()
+
+        #generate a vCard here.  May throw an exception
+        self.vCard()
+
+
+    def __repr__(self):
+        return &quot;&lt;{self.__class__.__name__}[{rn}({uid})]&gt;&quot;.format(
+            self=self,
+            fn=self.vCard().propertyValue(&quot;FN&quot;),
+            uid=self.vCard().propertyValue(&quot;UID&quot;)
+        )
+
+
+    def __hash__(self):
+        s = &quot;&quot;.join([
+              &quot;{attr}:{values}&quot;.format(attr=attribute, values=self.valuesForAttribute(attribute),)
+              for attribute in self.attributes
+              ])
+        return hash(s)
+
+
+    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.encode(&quot;utf-8&quot;) if isinstance(value, unicode) else value) for value in values if len(value) &gt; 0]
+
+        if len(nonEmptyValues) &gt; 0:
+            return nonEmptyValues
+        else:
+            return default_values
+
+
+    def firstValueForAttribute(self, attributeName, default_value=&quot;&quot;):
+        values = self.attributes.get(attributeName)
+        if values is None:
+            return default_value
+        elif isinstance(values, list):
+            return values[0].encode(&quot;utf_8&quot;) if isinstance(values[0], unicode) else values[0]
+        else:
+            return values.encode(&quot;utf_8&quot;) if isinstance(values, unicode) else values
+
+
+    def joinedValuesForAttribute(self, attributeName, separator=&quot;,&quot;, default_string=&quot;&quot;):
+        values = self.valuesForAttribute(attributeName, None)
+        if not values:
+            return default_string
+        else:
+            return separator.join(values)
+
+
+    def isoDateStringForDateAttribute(self, attributeName, default_string=&quot;&quot;):
+        modDate = self.firstValueForAttribute(attributeName, default_string)
+        revDate = None
+        if modDate:
+            if len(modDate) &gt;= len(&quot;YYYYMMDD&quot;) and modDate[:8].isdigit():
+                revDate = &quot;{YYYY}-{MM}-{DD}&quot;.format(YYYY=modDate[:4], MM=modDate[4:6], DD=modDate[6:8],)
+            if len(modDate) &gt;= len(&quot;YYYYMMDDHHMMSS&quot;) and modDate[8:14].isdigit():
+                revDate += &quot;T{HH}:{MM}:{SS}Z&quot;.format(HH=modDate[8:10], MM=modDate[10:12], SS=modDate[12:14],)
+        return revDate
+
+
+    def vCard(self):
+
+        def generateVCard():
+
+            def isUniqueProperty(vcard, newProperty, ignoreParams=None):
+                existingProperties = vcard.properties(newProperty.name())
+                for existingProperty in existingProperties:
+                    if ignoreParams:
+                        existingProperty = existingProperty.duplicate()
+                        for paramname, paramvalue in ignoreParams:
+                            existingProperty.removeParameterValue(paramname, paramvalue)
+                    if existingProperty == newProperty:
+                        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(&quot;Ignoring attribute %r with value %r in creating property %r. A duplicate property already exists.&quot; % (attrType, attrValue, newProperty,))
+
+
+            def addPropertyAndLabel(groupCount, label, propertyName, propertyValue, parameters=None):
+                groupCount[0] += 1
+                groupPrefix = &quot;item%d&quot; % groupCount[0]
+                vcard.addProperty(Property(propertyName, propertyValue, params=parameters, group=groupPrefix))
+                vcard.addProperty(Property(&quot;X-ABLabel&quot;, label, group=groupPrefix))
+
+
+            # for attributes of the form  param:value
+            def addPropertiesAndLabelsForPrefixedAttribute(groupCount, propertyPrefix, propertyName, attrType, defaultLabel, nolabelParamTypes=(), labelMap={}, specialParamType=None):
+                preferred = True
+                for attrValue in self.valuesForAttribute(attrType):
+                    try:
+                        colonIndex = attrValue.find(&quot;:&quot;)
+                        if (colonIndex &gt; len(attrValue) - 2):
+                            raise ValueError(&quot;Nothing after colon.&quot;)
+
+                        propertyValue = attrValue[colonIndex + 1:]
+                        labelString = attrValue[:colonIndex] if colonIndex &gt; 0 else defaultLabel
+                        paramTypeString = labelString.upper()
+
+                        if specialParamType:
+                            parameters = {specialParamType: (paramTypeString,)}
+                            if preferred:
+                                parameters[&quot;TYPE&quot;] = (&quot;PREF&quot;,)
+                        else:
+                            # add PREF to first prop's parameters
+                            paramTypeStrings = [paramTypeString, ]
+                            if preferred and &quot;PREF&quot; != paramTypeString:
+                                paramTypeStrings += [&quot;PREF&quot;, ]
+                            parameters = {&quot;TYPE&quot;: 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
+                            localizedABLabelString = labelMap.get(labelString, labelString)
+                            addPropertyAndLabel(groupCount, localizedABLabelString, propertyName, propertyValue, parameters)
+                        preferred = False
+
+                    except Exception, e:
+                        self.log.debug(
+                            &quot;addPropertiesAndLabelsForPrefixedAttribute(): groupCount={groupCount}, propertyPrefix={propertyPrefix}, propertyName={propertyName}, nolabelParamTypes={nolabelParamTypes}, labelMap={labelMap}, attrType={attrType}&quot;,
+                            groupCount=groupCount[0], propertyPrefix=propertyPrefix, propertyName=propertyName, nolabelParamTypes=nolabelParamTypes, labelMap=labelMap, attrType=attrType,
+                        )
+                        self.log.error(
+                            &quot;addPropertiesAndLabelsForPrefixedAttribute(): Trouble parsing attribute {attrType}, with value \&quot;{attrValue}\&quot;.  Error = {e}&quot;,
+                            attrType=attrType, attrValue=attrValue, e=e
+                        )
+
+            # create vCard
+            vcard = Component(&quot;VCARD&quot;)
+            groupCount = [0]
+
+            # add constant properties - properties that are the same regardless of the record attributes
+            for key, value in self.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(&quot;FN&quot;, 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
+            # 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, &quot;&quot;)
+
+            nameObject = N(
+                first=self.valuesForAttribute(dsattributes.kDS1AttrFirstName, &quot;&quot;),
+                last=familyName,
+                middle=self.valuesForAttribute(dsattributes.kDS1AttrMiddleName, &quot;&quot;),
+                prefix=self.valuesForAttribute(dsattributes.kDSNAttrNamePrefix, &quot;&quot;),
+                suffix=self.valuesForAttribute(dsattributes.kDSNAttrNameSuffix, &quot;&quot;),
+            )
+            vcard.addProperty(Property(&quot;N&quot;, 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
+            vcard.addProperty(Property(&quot;FN&quot;, nameObject.getFullName()))
+
+            # 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(&quot;NICKNAME&quot;, 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):
+                photo = &quot;&quot;.join(&quot;&quot;.join(photo.split(&quot;\r&quot;)).split(&quot;\n&quot;)) # get rid of line folding: for PHOTO
+                addUniqueProperty(vcard, Property(&quot;PHOTO&quot;, photo, params={&quot;ENCODING&quot;: [&quot;b&quot;, ], &quot;TYPE&quot;: [&quot;JPEG&quot;, ], }), 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(&quot;BDAY&quot;, DateTime.parseText(birthdate, fullISO=True)))
+
+            # 3.2 Delivery Addressing Types http://tools.ietf.org/html/rfc2426#section-3.2
+            #
+            # 3.2.1 ADR Type Definition
+
+            #address
+            # vcard says: Each address attribute can be a string or a list of strings.
+            extended = self.valuesForAttribute(dsattributes.kDSNAttrBuilding, &quot;&quot;)
+            street = self.valuesForAttribute(dsattributes.kDSNAttrStreet, &quot;&quot;)
+            city = self.valuesForAttribute(dsattributes.kDSNAttrCity, &quot;&quot;)
+            region = self.valuesForAttribute(dsattributes.kDSNAttrState, &quot;&quot;)
+            code = self.valuesForAttribute(dsattributes.kDSNAttrPostalCode, &quot;&quot;)
+            country = self.valuesForAttribute(dsattributes.kDSNAttrCountry, &quot;&quot;)
+
+            if len(extended) &gt; 0 or len(street) &gt; 0 or len(city) &gt; 0 or len(region) &gt; 0 or len(code) &gt; 0 or len(country) &gt; 0:
+                vcard.addProperty(Property(&quot;ADR&quot;,
+                    Adr(
+                        #pobox = box,
+                        extended=extended,
+                        street=street,
+                        locality=city,
+                        region=region,
+                        postalcode=code,
+                        country=country,
+                    ),
+                    params={&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;POSTAL&quot;, &quot;PARCEL&quot;, ], }
+                ))
+
+            # 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(&quot;LABEL&quot;, label, params={&quot;TYPE&quot;: [&quot;POSTAL&quot;, &quot;PARCEL&quot;, ]}), None, dsattributes.kDSNAttrPostalAddress, label)
+
+            for label in self.valuesForAttribute(dsattributes.kDSNAttrPostalAddressContacts):
+                addUniqueProperty(vcard, Property(&quot;LABEL&quot;, label, params={&quot;TYPE&quot;: [&quot;POSTAL&quot;, &quot;PARCEL&quot;, ]}), None, dsattributes.kDSNAttrPostalAddressContacts, label)
+
+            address = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine1)
+            addressLine2 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine2)
+            if len(addressLine2) &gt; 0:
+                address += &quot;\n&quot; + addressLine2
+            addressLine3 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine3)
+            if len(addressLine3) &gt; 0:
+                address += &quot;\n&quot; + addressLine3
+
+            if len(address) &gt; 0:
+                vcard.addProperty(Property(&quot;LABEL&quot;, address, params={&quot;TYPE&quot;: [&quot;POSTAL&quot;, &quot;PARCEL&quot;, ]}))
+
+            # 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 = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;VOICE&quot;, ], }
+            for phone in self.valuesForAttribute(dsattributes.kDSNAttrPhoneNumber):
+                addUniqueProperty(vcard, Property(&quot;TEL&quot;, phone, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), phone, dsattributes.kDSNAttrPhoneNumber)
+                params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;VOICE&quot;, ], }
+
+            params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;CELL&quot;, ], }
+            for phone in self.valuesForAttribute(dsattributes.kDSNAttrMobileNumber):
+                addUniqueProperty(vcard, Property(&quot;TEL&quot;, phone, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), phone, dsattributes.kDSNAttrMobileNumber)
+                params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;CELL&quot;, ], }
+
+            params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;FAX&quot;, ], }
+            for phone in self.valuesForAttribute(dsattributes.kDSNAttrFaxNumber):
+                addUniqueProperty(vcard, Property(&quot;TEL&quot;, phone, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), phone, dsattributes.kDSNAttrFaxNumber)
+                params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;FAX&quot;, ], }
+
+            params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;PAGER&quot;, ], }
+            for phone in self.valuesForAttribute(dsattributes.kDSNAttrPagerNumber):
+                addUniqueProperty(vcard, Property(&quot;TEL&quot;, phone, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), phone, dsattributes.kDSNAttrPagerNumber)
+                params = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PAGER&quot;, ], }
+
+            params = {&quot;TYPE&quot;: [&quot;HOME&quot;, &quot;PREF&quot;, &quot;VOICE&quot;, ], }
+            for phone in self.valuesForAttribute(dsattributes.kDSNAttrHomePhoneNumber):
+                addUniqueProperty(vcard, Property(&quot;TEL&quot;, phone, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), phone, dsattributes.kDSNAttrHomePhoneNumber)
+                params = {&quot;TYPE&quot;: [&quot;HOME&quot;, &quot;VOICE&quot;, ], }
+
+            addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName=&quot;TEL&quot;, defaultLabel=&quot;work&quot;,
+                                                        nolabelParamTypes=(&quot;VOICE&quot;, &quot;CELL&quot;, &quot;FAX&quot;, &quot;PAGER&quot;,),
+                                                        attrType=dsattributes.kDSNAttrPhoneContacts,)
+
+            &quot;&quot;&quot;
+            # EXTEND:  Use this attribute
+            # dsattributes.kDSNAttrAreaCode,            # Area code of a user's phone number.
+            &quot;&quot;&quot;
+
+            # 3.3.2 EMAIL Type Definition
+            # dsattributes.kDSNAttrEMailAddress,        # Email address of usually a user record.
+
+            # setup some params
+            preferredWorkParams = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;PREF&quot;, &quot;INTERNET&quot;, ], }
+            workParams = {&quot;TYPE&quot;: [&quot;WORK&quot;, &quot;INTERNET&quot;, ], }
+            params = preferredWorkParams
+            for emailAddress in self.valuesForAttribute(dsattributes.kDSNAttrEMailAddress):
+                addUniqueProperty(vcard, Property(&quot;EMAIL&quot;, emailAddress, params=params), ((&quot;TYPE&quot;, &quot;PREF&quot;),), 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@mymail.com
+
+            # check to see if parameters type are open ended. Could be any string
+            addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName=&quot;EMAIL&quot;, defaultLabel=&quot;work&quot;,
+                                                        nolabelParamTypes=(&quot;WORK&quot;, &quot;HOME&quot;,),
+                                                        attrType=dsattributes.kDSNAttrEMailContacts,)
+
+            &quot;&quot;&quot;
+            # UNIMPLEMENTED:
+            # 3.3.3 MAILER Type Definition
+            &quot;&quot;&quot;
+            # 3.4 GEOGRAPHICAL TYPES http://tools.ietf.org/html/rfc2426#section-3.4
+            &quot;&quot;&quot;
+            # UNIMPLEMENTED:
+            # 3.4.1 TZ Type Definition
+            &quot;&quot;&quot;
+            # 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(&quot;,&quot;)
+                if (len(parts) == 2):
+                    vcard.addProperty(Property(&quot;GEO&quot;, parts))
+                else:
+                    log.info(&quot;Ignoring malformed attribute %r with value %r.&quot; % (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(&quot;TITLE&quot;, jobTitle), None, dsattributes.kDSNAttrJobTitle, jobTitle)
+
+            &quot;&quot;&quot;
+            # UNIMPLEMENTED:
+            # 3.5.2 ROLE Type Definition
+            # 3.5.3 LOGO Type Definition
+            # 3.5.4 AGENT Type Definition
+            &quot;&quot;&quot;
+            # 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) &gt; 0 or len(department) &gt; 0:
+                vcard.addProperty(Property(&quot;ORG&quot;, (company, department, extra,),))
+
+            # 3.6 EXPLANATORY TYPES http://tools.ietf.org/html/rfc2426#section-3.6
+            &quot;&quot;&quot;
+            # UNIMPLEMENTED:
+            # 3.6.1 CATEGORIES Type Definition
+            &quot;&quot;&quot;
+            # 3.6.2 NOTE Type Definition
+            # dsattributes.kDS1AttrComment,               # Attribute used for unformatted comment.
+            # dsattributes.kDS1AttrNote,                  # Note attribute. Commonly used in printer records.
+            notes = self.valuesForAttribute(dsattributes.kDS1AttrComment, []) + self.valuesForAttribute(dsattributes.kDS1AttrNote, [])
+            if len(notes):
+                vcard.addProperty(Property(&quot;NOTE&quot;, &quot;\n&quot;.join(notes),))
+
+            # 3.6.3 PRODID Type Definition
+            #vcard.addProperty(Property(&quot;PRODID&quot;, vCardProductID + &quot;//BUILD {build}&quot;.format(build=twistedcaldav.__version__))
+            #vcard.addProperty(Property(&quot;PRODID&quot;, vCardProductID))
+            # ADDED WITH CONTSTANT PROPERTIES
+
+            # 3.6.4 REV Type Definition
+            revDate = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrModificationTimestamp)
+            if revDate:
+                vcard.addProperty(Property(&quot;REV&quot;, DateTime.parseText(revDate, fullISO=True)))
+
+            &quot;&quot;&quot;
+            # UNIMPLEMENTED:
+            # 3.6.5 SORT-STRING Type Definition
+            # 3.6.6 SOUND Type Definition
+            &quot;&quot;&quot;
+            # 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 &quot;A579E95E-CDFE-4EBC-B7E7-F2158562170F&quot;.
+                                                        #      The standard format contains 32 hex characters and four hyphen characters.
+
+            vcard.addProperty(Property(&quot;UID&quot;, 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, &quot;weblog&quot;, &quot;URL&quot;, url, parameters={&quot;TYPE&quot;: [&quot;WEBLOG&quot;, ]})
+
+            for url in self.valuesForAttribute(dsattributes.kDSNAttrURL):
+                addPropertyAndLabel(groupCount, &quot;_$!&lt;HomePage&gt;!$_&quot;, &quot;URL&quot;, url, parameters={&quot;TYPE&quot;: [&quot;HOMEPAGE&quot;, ]})
+
+
+            # 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(&quot;KEY&quot;, key, params={&quot;ENCODING&quot;: [&quot;b&quot;, ], &quot;TYPE&quot;: [&quot;PGPPublicKey&quot;, ]}), None, dsattributes.kDSNAttrPGPPublicKey, key)
+
+            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserCertificate):
+                addUniqueProperty(vcard, Property(&quot;KEY&quot;, key, params={&quot;ENCODING&quot;: [&quot;b&quot;, ], &quot;TYPE&quot;: [&quot;UserCertificate&quot;, ]}), None, dsattributes.kDS1AttrUserCertificate, key)
+
+            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserPKCS12Data):
+                addUniqueProperty(vcard, Property(&quot;KEY&quot;, key, params={&quot;ENCODING&quot;: [&quot;b&quot;, ], &quot;TYPE&quot;: [&quot;UserPKCS12Data&quot;, ]}), None, dsattributes.kDS1AttrUserPKCS12Data, key)
+
+            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserSMIMECertificate):
+                addUniqueProperty(vcard, Property(&quot;KEY&quot;, key, params={&quot;ENCODING&quot;: [&quot;b&quot;, ], &quot;TYPE&quot;: [&quot;UserSMIMECertificate&quot;, ]}), None, dsattributes.kDS1AttrUserSMIMECertificate, key)
+
+            &quot;&quot;&quot;
+            X- attributes, Address Book support
+            &quot;&quot;&quot;
+            # 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).
+            imNolabelParamTypes = (&quot;AIM&quot;, &quot;FACEBOOK&quot;, &quot;GAGU-GAGU&quot;, &quot;GOOGLE TALK&quot;, &quot;ICQ&quot;, &quot;JABBER&quot;, &quot;MSN&quot;, &quot;QQ&quot;, &quot;SKYPE&quot;, &quot;YAHOO&quot;,)
+            addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=&quot;X-&quot;, propertyName=None, defaultLabel=&quot;aim&quot;,
+                                                        nolabelParamTypes=imNolabelParamTypes,
+                                                        attrType=dsattributes.kDSNAttrIMHandle,)
+
+            # IMPP
+            # Address Book's implementation of http://tools.ietf.org/html/rfc6350#section-6.4.3
+            # adding IMPP property allows ab query report search on one property
+            addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName=&quot;IMPP&quot;, defaultLabel=&quot;aim&quot;,
+                                                        specialParamType=&quot;X-SERVICE-TYPE&quot;,
+                                                        nolabelParamTypes=imNolabelParamTypes,
+                                                        attrType=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=groupCount, propertyPrefix=None, propertyName=&quot;X-ABRELATEDNAMES&quot;, defaultLabel=&quot;friend&quot;,
+                                                        labelMap={&quot;FATHER&quot;: &quot;_$!&lt;Father&gt;!$_&quot;,
+                                                            &quot;MOTHER&quot;: &quot;_$!&lt;Mother&gt;!$_&quot;,
+                                                            &quot;PARENT&quot;: &quot;_$!&lt;Parent&gt;!$_&quot;,
+                                                            &quot;BROTHER&quot;: &quot;_$!&lt;Brother&gt;!$_&quot;,
+                                                            &quot;SISTER&quot;: &quot;_$!&lt;Sister&gt;!$_&quot;,
+                                                            &quot;CHILD&quot;: &quot;_$!&lt;Child&gt;!$_&quot;,
+                                                            &quot;FRIEND&quot;: &quot;_$!&lt;Friend&gt;!$_&quot;,
+                                                            &quot;SPOUSE&quot;: &quot;_$!&lt;Spouse&gt;!$_&quot;,
+                                                            &quot;PARTNER&quot;: &quot;_$!&lt;Partner&gt;!$_&quot;,
+                                                            &quot;ASSISTANT&quot;: &quot;_$!&lt;Assistant&gt;!$_&quot;,
+                                                            &quot;MANAGER&quot;: &quot;_$!&lt;Manager&gt;!$_&quot;, },
+                                                        attrType=dsattributes.kDSNAttrRelationships,)
+
+            # add apple-defined group vcard properties if record type is group
+            if self.kind == &quot;group&quot;:
+                vcard.addProperty(Property(&quot;X-ADDRESSBOOKSERVER-KIND&quot;, &quot;group&quot;))
+
+            # add members
+            for memberguid in self.valuesForAttribute(dsattributes.kDSNAttrGroupMembers):
+                vcard.addProperty(Property(&quot;X-ADDRESSBOOKSERVER-MEMBER&quot;, &quot;urn:uuid:&quot; + memberguid))
+
+            &quot;&quot;&quot;
+            # 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.
+
+            &quot;&quot;&quot;
+
+            # 2.1.4 SOURCE Type http://tools.ietf.org/html/rfc2426#section-2.1.4
+            #    If the SOURCE type is present, then its value provides information
+            #    how to find the source for the vCard.
+
+            # add the source, so that if the SOURCE is copied out and preserved, the client can refresh information
+            # However, client should really do a ab-query report matching UID on /directory/ not a multiget.
+            uri = joinURL(self._directoryBackedAddressBook.uri, vcard.propertyValue(&quot;UID&quot;) + &quot;.vcf&quot;)
+
+            # seems like this should be in some standard place.
+            if config.EnableSSL and config.SSLPort:
+                if config.SSLPort == 443:
+                    source = &quot;https://{server}{uri}&quot;.format(server=config.ServerHostName, uri=uri)
+                else:
+                    source = &quot;https://{server}:{port}{uri}&quot;.format(server=config.ServerHostName, port=config.SSLPort, uri=uri)
+            else:
+                if config.HTTPPort == 80:
+                    source = &quot;https://{server}{uri}&quot;.format(server=config.ServerHostName, uri=uri)
+                else:
+                    source = &quot;https://{server}:{port}{uri}&quot;.format(server=config.ServerHostName, port=config.HTTPPort, uri=uri)
+            vcard.addProperty(Property(&quot;SOURCE&quot;, source))
+
+            #  in 4.0 spec:
+            # 6.1.4.  KIND http://tools.ietf.org/html/rfc6350#section-6.1.4
+            #
+            # see also: http://www.iana.org/assignments/vcard-elements/vcard-elements.xml
+            #
+            vcard.addProperty(Property(&quot;KIND&quot;, self.kind))
+
+            # one more X- related to kind
+            if self.kind == &quot;org&quot;:
+                vcard.addProperty(Property(&quot;X-ABShowAs&quot;, &quot;COMPANY&quot;))
+
+            return vcard
+
+        if not self._vCard:
+            self._vCard = generateVCard()
+
+        return self._vCard
+
+
+    def vCardText(self):
+        return str(self.vCard())
+
+
+    def uri(self):
+        return self.vCard().propertyValue(&quot;UID&quot;) + &quot;.vcf&quot;
+
+
+    def hRef(self, parentURI=None):
+        return davxml.HRef.fromString(joinURL(parentURI if parentURI else  self._directoryBackedAddressBook.uri, self.uri()))
+
+
+    def readProperty(self, property, request):
+
+        if type(property) is tuple:
+            qname = property
+        else:
+            qname = property.qname()
+        namespace, name = qname
+
+        if namespace == dav_namespace:
+            if name == &quot;resourcetype&quot;:
+                result = davxml.ResourceType.empty #@UndefinedVariable
+                return result
+            elif name == &quot;getetag&quot;:
+                result = davxml.GETETag(ETag(hashlib.md5(self.vCardText()).hexdigest()).generate())
+                return result
+            elif name == &quot;getcontenttype&quot;:
+                mimeType = MimeType('text', 'vcard', {})
+                result = davxml.GETContentType(generateContentType(mimeType))
+                return result
+            elif name == &quot;getcontentlength&quot;:
+                result = davxml.GETContentLength.fromString(str(len(self.vCardText())))
+                return result
+            elif name == &quot;getlastmodified&quot;:
+                if self.vCard().hasProperty(&quot;REV&quot;):
+                    modDatetime = parse_date(self.vCard().propertyValue(&quot;REV&quot;))
+                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)
+                return result
+            elif name == &quot;creationdate&quot;:
+                creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
+                if creationDateString:
+                    creationDatetime = parse_date(creationDateString)
+                elif self.vCard().hasProperty(&quot;REV&quot;):  # use modification date property if it exists
+                    creationDatetime = parse_date(self.vCard().propertyValue(&quot;REV&quot;))
+                else:
+                    creationDatetime = datetime.datetime.utcnow()
+                result = davxml.CreationDate.fromDate(creationDatetime)
+                return result
+            elif name == &quot;displayname&quot;:
+                # AddressBook.app uses N. Use FN or UID instead?
+                result = davxml.DisplayName.fromString(self.vCard().propertyValue(&quot;N&quot;))
+                return result
+
+        elif namespace == twisted_dav_namespace:
+            return super(ABDirectoryQueryResult, self).readProperty(property, request)
+
+        return self._directoryBackedAddressBook.readProperty(property, request)
+
+
+    def listProperties(self, request):  # @UnusedVariable
+        qnames = set(self.liveProperties())
+
+        # Add dynamic live properties that exist
+        dynamicLiveProperties = (
+            (dav_namespace, &quot;quota-available-bytes&quot;),
+            (dav_namespace, &quot;quota-used-bytes&quot;),
+        )
+        for dqname in dynamicLiveProperties:
+            qnames.remove(dqname)
+
+        for qname in self.deadProperties().list():
+            if (qname not in qnames) and (qname[0] != twisted_private_namespace):
+                qnames.add(qname)
+
+        yield qnames
+
+    listProperties = deferredGenerator(listProperties)
+
+
+
+#remove illegal XML
+def removeControlChars(utf8String):
+    result = ''.join([c for c in utf8String if c not in &quot;\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f&quot;])
+    return result
</ins></span></pre></div>
<a id="CalendarServerbranchesuserssagenmove2who4twistedcaldavmethodreport_addressbook_querypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/method/report_addressbook_query.py (13036 => 13037)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/method/report_addressbook_query.py        2014-03-28 21:42:50 UTC (rev 13036)
+++ CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/method/report_addressbook_query.py        2014-03-28 21:51:51 UTC (rev 13037)
</span><span class="lines">@@ -21,26 +21,22 @@
</span><span class="cx"> 
</span><span class="cx"> __all__ = [&quot;report_urn_ietf_params_xml_ns_carddav_addressbook_query&quot;]
</span><span class="cx"> 
</span><del>-import urllib
-
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
-
</del><span class="cx"> from twext.python.log import Logger
</span><del>-from txweb2 import responsecode
-from txweb2.dav.http import ErrorResponse, MultiStatusResponse
-from txweb2.dav.method.report import NumberOfMatchesWithinLimits
-from txweb2.dav.util import joinURL
-from txweb2.http import HTTPError, StatusResponse
-
</del><ins>+from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
</ins><span class="cx"> from twistedcaldav import carddavxml
</span><del>-from twistedcaldav.config import config
</del><span class="cx"> from twistedcaldav.carddavxml import carddav_namespace, NResults
</span><ins>+from twistedcaldav.config import config
</ins><span class="cx"> from twistedcaldav.method import report_common
</span><del>-
</del><span class="cx"> from txdav.carddav.datastore.query.filter import Filter
</span><span class="cx"> from txdav.common.icommondatastore import ConcurrentModification, \
</span><span class="cx">     IndexedSearchException
</span><span class="cx"> from txdav.xml import element as davxml
</span><ins>+from txweb2 import responsecode
+from txweb2.dav.http import ErrorResponse, MultiStatusResponse
+from txweb2.dav.method.report import NumberOfMatchesWithinLimits
+from txweb2.dav.util import joinURL
+from txweb2.http import HTTPError, StatusResponse
+import urllib
</ins><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="lines">@@ -52,12 +48,12 @@
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     # Verify root element
</span><span class="cx">     if addressbook_query.qname() != (carddav_namespace, &quot;addressbook-query&quot;):
</span><del>-        raise ValueError(&quot;{CardDAV:}addressbook-query expected as root element, not %s.&quot; % (addressbook_query.sname(),))
</del><ins>+        raise ValueError(&quot;{CardDAV:}addressbook-query expected as root element, not {elementName}.&quot;.format(elementName=addressbook_query.sname()))
</ins><span class="cx"> 
</span><span class="cx">     if not self.isCollection():
</span><span class="cx">         parent = (yield self.locateParent(request, request.uri))
</span><span class="cx">         if not parent.isAddressBookCollection():
</span><del>-            log.error(&quot;addressbook-query report is not allowed on a resource outside of an address book collection %s&quot; % (self,))
</del><ins>+            log.error(&quot;addressbook-query report is not allowed on a resource outside of an address book collection {parent}&quot;, parent=self)
</ins><span class="cx">             raise HTTPError(StatusResponse(responsecode.FORBIDDEN, &quot;Must be address book collection or address book resource&quot;))
</span><span class="cx"> 
</span><span class="cx">     responses = []
</span><span class="lines">@@ -154,160 +150,127 @@
</span><span class="cx">                     # of one of these resources in another request.  In this
</span><span class="cx">                     # case, we ignore the now missing resource rather
</span><span class="cx">                     # than raise an error for the entire report.
</span><del>-                    log.error(&quot;Missing resource during sync: %s&quot; % (href,))
</del><ins>+                    log.error(&quot;Missing resource during sync: {href}&quot;, href=href)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">         @inlineCallbacks
</span><span class="cx">         def queryDirectoryBackedAddressBook(directoryBackedAddressBook, addressBookFilter):
</span><span class="cx">             &quot;&quot;&quot;
</span><span class="cx">             &quot;&quot;&quot;
</span><del>-            records, limited[0] = (yield directoryBackedAddressBook.directory.vCardRecordsForAddressBookQuery(addressBookFilter, query, max_number_of_results[0]))
-            for vCardRecord in records:
</del><ins>+            results, limited[0] = (yield directoryBackedAddressBook.doAddressBookDirectoryQuery(addressBookFilter, query, max_number_of_results[0]))
+            for vCardResult in results:
</ins><span class="cx"> 
</span><del>-                # match against original filter
-                if filter.match((yield vCardRecord.vCard())):
</del><ins>+                # match against original filter if different from addressBookFilter
+                if addressBookFilter is filter or filter.match((yield vCardResult.vCard())):
</ins><span class="cx"> 
</span><span class="cx">                     # Check size of results is within limit
</span><span class="cx">                     checkMaxResults()
</span><span class="cx"> 
</span><span class="cx">                     try:
</span><del>-                        yield report_common.responseForHref(request, responses, vCardRecord.hRef(), vCardRecord, propertiesForResource, query, vcard=(yield vCardRecord.vCard()))
</del><ins>+                        yield report_common.responseForHref(request, responses, vCardResult.hRef(), vCardResult, propertiesForResource, query, vcard=(yield vCardResult.vCard()))
</ins><span class="cx">                     except ConcurrentModification:
</span><span class="cx">                         # This can happen because of a race-condition between the
</span><span class="cx">                         # time we determine which resources exist and the deletion
</span><span class="cx">                         # of one of these resources in another request.  In this
</span><span class="cx">                         # case, we ignore the now missing resource rather
</span><span class="cx">                         # than raise an error for the entire report.
</span><del>-                        log.error(&quot;Missing resource during sync: %s&quot; % (vCardRecord.hRef(),))
</del><ins>+                        log.error(&quot;Missing resource during sync: {href}&quot;, href=vCardResult.hRef())
</ins><span class="cx"> 
</span><del>-        directoryAddressBookLock = None
-        try:
</del><ins>+        if not addrresource.isAddressBookCollection():
</ins><span class="cx"> 
</span><del>-            if addrresource.isDirectoryBackedAddressBookCollection() and addrresource.directory.cacheQuery:
</del><ins>+            #do UID lookup on last part of uri
+            resource_name = urllib.unquote(uri[uri.rfind(&quot;/&quot;) + 1:])
+            if resource_name.endswith(&quot;.vcf&quot;) and len(resource_name) &gt; 4:
</ins><span class="cx"> 
</span><del>-                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]))
</del><ins>+                # see if parent is directory backed address book
+                parent = (yield  addrresource.locateParent(request, uri))
</ins><span class="cx"> 
</span><del>-                elif directory.maxDSQueryRecords and directory.maxDSQueryRecords &lt; max_number_of_results[0]:
-                    max_number_of_results[0] = directory.maxDSQueryRecords
</del><ins>+        # Check whether supplied resource is an address book or an address book object resource
+        if addrresource.isAddressBookCollection():
</ins><span class="cx"> 
</span><del>-            elif not addrresource.isAddressBookCollection():
</del><ins>+            if addrresource.isDirectoryBackedAddressBookCollection():
+                yield  maybeDeferred(queryDirectoryBackedAddressBook, addrresource, filter)
</ins><span class="cx"> 
</span><del>-                #do UID lookup on last part of uri
-                resource_name = urllib.unquote(uri[uri.rfind(&quot;/&quot;) + 1:])
-                if resource_name.endswith(&quot;.vcf&quot;) and len(resource_name) &gt; 4:
</del><ins>+            else:
</ins><span class="cx"> 
</span><del>-                    # see if parent is directory backed address book
-                    parent = (yield  addrresource.locateParent(request, uri))
</del><ins>+                # 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))
</ins><span class="cx"> 
</span><del>-                    if parent.isDirectoryBackedAddressBookCollection() and parent.directory.cacheQuery:
</del><ins>+                # Check for disabled access
+                if filteredaces is not None:
+                    index_query_ok = True
+                    try:
+                        # Get list of children that match the search and have read access
+                        names = [name for name, ignore_uid in (yield addrresource.search(filter))] #@UnusedVariable
+                    except IndexedSearchException:
+                        names = yield addrresource.listChildren()
+                        index_query_ok = False
+                    if not names:
+                        return
</ins><span class="cx"> 
</span><del>-                        directory = parent.directory
-                        if directory.liveQuery:
-                            vCardFilter = carddavxml.Filter(*[carddavxml.PropertyFilter(
-                                                        carddavxml.TextMatch.fromString(resource_name[:-4]),
-                                                        name=&quot;UID&quot;, # attributes
-                                                        ), ])
-                            vCardFilter = Filter(vCardFilter)
</del><ins>+                    # Now determine which valid resources are readable and which are not
+                    ok_resources = []
+                    yield addrresource.findChildrenFaster(
+                        &quot;1&quot;,
+                        request,
+                        lambda x, y: ok_resources.append((x, y)),
+                        None,
+                        None,
+                        None,
+                        names,
+                        (davxml.Read(),),
+                        inherited_aces=filteredaces
+                    )
+                    for child, child_uri in ok_resources:
+                        child_uri_name = child_uri[child_uri.rfind(&quot;/&quot;) + 1:]
</ins><span class="cx"> 
</span><del>-                            directoryAddressBookLock, limited[0] = (yield  directory.cacheVCardsForAddressBookQuery(vCardFilter, query, max_number_of_results[0]))
</del><ins>+                        if generate_address_data or not index_query_ok:
+                            vcard = yield child.vCard()
+                            assert vcard is not None, &quot;vCard {name} is missing from address book collection {collection!r}&quot;.format(name=child_uri_name, collection=self)
+                        else:
+                            vcard = None
</ins><span class="cx"> 
</span><del>-                        elif directory.maxDSQueryRecords and directory.maxDSQueryRecords &lt; max_number_of_results[0]:
-                            max_number_of_results[0] = directory.maxDSQueryRecords
</del><ins>+                        yield queryAddressBookObjectResource(child, uri, child_uri_name, vcard, query_ok=index_query_ok)
</ins><span class="cx"> 
</span><del>-            # Check whether supplied resource is an address book or an address book object resource
-            if addrresource.isAddressBookCollection():
</del><ins>+        else:
</ins><span class="cx"> 
</span><del>-                if addrresource.isDirectoryBackedAddressBookCollection() and addrresource.directory.liveQuery and not addrresource.directory.cacheQuery:
-                    yield  maybeDeferred(queryDirectoryBackedAddressBook, addrresource, filter)
</del><ins>+            handled = False
+            resource_name = urllib.unquote(uri[uri.rfind(&quot;/&quot;) + 1:])
+            if resource_name.endswith(&quot;.vcf&quot;) and len(resource_name) &gt; 4:
</ins><span class="cx"> 
</span><del>-                else:
</del><ins>+                # see if parent is directory backed address book
+                parent = (yield  addrresource.locateParent(request, uri))
</ins><span class="cx"> 
</span><del>-                    # 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))
</del><ins>+                if parent.isDirectoryBackedAddressBookCollection():
</ins><span class="cx"> 
</span><del>-                    # Check for disabled access
-                    if filteredaces is not None:
-                        index_query_ok = True
-                        try:
-                            # Get list of children that match the search and have read access
-                            names = [name for name, ignore_uid in (yield addrresource.search(filter))] #@UnusedVariable
-                        except IndexedSearchException:
-                            names = yield addrresource.listChildren()
-                            index_query_ok = False
-                        if not names:
-                            return
</del><ins>+                    vCardFilter = carddavxml.Filter(*[carddavxml.PropertyFilter(
+                                                carddavxml.TextMatch.fromString(resource_name[:-4]),
+                                                name=&quot;UID&quot;, # attributes
+                                                ), ])
+                    vCardFilter = Filter(vCardFilter)
</ins><span class="cx"> 
</span><del>-                        # Now determine which valid resources are readable and which are not
-                        ok_resources = []
-                        yield addrresource.findChildrenFaster(
-                            &quot;1&quot;,
-                            request,
-                            lambda x, y: ok_resources.append((x, y)),
-                            None,
-                            None,
-                            None,
-                            names,
-                            (davxml.Read(),),
-                            inherited_aces=filteredaces
-                        )
-                        for child, child_uri in ok_resources:
-                            child_uri_name = child_uri[child_uri.rfind(&quot;/&quot;) + 1:]
</del><ins>+                    yield  maybeDeferred(queryDirectoryBackedAddressBook, parent, vCardFilter)
+                    handled = True
</ins><span class="cx"> 
</span><del>-                            if generate_address_data or not index_query_ok:
-                                vcard = yield child.vCard()
-                                assert vcard is not None, &quot;vCard %s is missing from address book collection %r&quot; % (child_uri_name, self)
-                            else:
-                                vcard = None
</del><ins>+            if not handled:
+                vcard = yield addrresource.vCard()
+                yield queryAddressBookObjectResource(addrresource, uri, None, vcard)
</ins><span class="cx"> 
</span><del>-                            yield queryAddressBookObjectResource(child, uri, child_uri_name, vcard, query_ok=index_query_ok)
</del><ins>+        if limited[0]:
+            raise NumberOfMatchesWithinLimits(matchcount[0])
</ins><span class="cx"> 
</span><del>-            else:
-
-                handled = False
-                resource_name = urllib.unquote(uri[uri.rfind(&quot;/&quot;) + 1:])
-                if resource_name.endswith(&quot;.vcf&quot;) and len(resource_name) &gt; 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=&quot;UID&quot;, # attributes
-                                                    ), ])
-                        vCardFilter = Filter(vCardFilter)
-
-                        yield  maybeDeferred(queryDirectoryBackedAddressBook, parent, vCardFilter)
-                        handled = True
-
-                if not handled:
-                    vcard = yield addrresource.vCard()
-                    yield queryAddressBookObjectResource(addrresource, uri, None, vcard)
-
-            if limited[0]:
-                raise NumberOfMatchesWithinLimits(matchcount[0])
-
-        finally:
-            if directoryAddressBookLock:
-                yield directoryAddressBookLock.release()
-
</del><span class="cx">     # Run report taking depth into account
</span><span class="cx">     try:
</span><span class="cx">         depth = request.headers.getHeader(&quot;depth&quot;, &quot;0&quot;)
</span><span class="cx">         yield report_common.applyToAddressBookCollections(self, request, request.uri, depth, doQuery, (davxml.Read(),))
</span><span class="cx">     except NumberOfMatchesWithinLimits, e:
</span><del>-        self.log.info(&quot;Too many matching components in addressbook-query report. Limited to %d items&quot; % e.maxLimit())
</del><ins>+        self.log.info(&quot;Too many matching components in addressbook-query report. Limited to {limit} items&quot;, limit=e.maxLimit())
</ins><span class="cx">         responses.append(davxml.StatusResponse(
</span><span class="cx">                         davxml.HRef.fromString(request.uri),
</span><span class="cx">                         davxml.Status.fromResponseCode(responsecode.INSUFFICIENT_STORAGE_SPACE),
</span><span class="cx">                         davxml.Error(davxml.NumberOfMatchesWithinLimits()),
</span><del>-                        #davxml.ResponseDescription(&quot;Results limited by %s at %d&quot; % resultsWereLimited),
-                        davxml.ResponseDescription(&quot;Results limited to %d items&quot; % e.maxLimit()),
</del><ins>+                        davxml.ResponseDescription(&quot;Results limited to {limit} items&quot;.format(limit=e.maxLimit())),
</ins><span class="cx">                     ))
</span><span class="cx"> 
</span><span class="cx">     if not hasattr(request, &quot;extendedLogItems&quot;):
</span></span></pre></div>
<a id="CalendarServerbranchesuserssagenmove2who4twistedcaldavmethodreport_multiget_commonpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/method/report_multiget_common.py (13036 => 13037)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/method/report_multiget_common.py        2014-03-28 21:42:50 UTC (rev 13036)
+++ CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/method/report_multiget_common.py        2014-03-28 21:51:51 UTC (rev 13037)
</span><span class="lines">@@ -20,17 +20,8 @@
</span><span class="cx"> 
</span><span class="cx"> __all__ = [&quot;multiget_common&quot;]
</span><span class="cx"> 
</span><del>-from urllib import unquote
-
</del><span class="cx"> from twext.python.log import Logger
</span><del>-
-from txweb2 import responsecode
-from txweb2.dav.http import ErrorResponse, MultiStatusResponse
-from txweb2.dav.resource import AccessDeniedError
-from txweb2.http import HTTPError, StatusResponse
-
</del><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><del>-
</del><span class="cx"> from twistedcaldav import carddavxml
</span><span class="cx"> from twistedcaldav.caldavxml import caldav_namespace
</span><span class="cx"> from twistedcaldav.carddavxml import carddav_namespace
</span><span class="lines">@@ -38,11 +29,15 @@
</span><span class="cx"> from twistedcaldav.method import report_common
</span><span class="cx"> from twistedcaldav.method.report_common import COLLECTION_TYPE_CALENDAR, \
</span><span class="cx">     COLLECTION_TYPE_ADDRESSBOOK
</span><del>-
</del><span class="cx"> from txdav.carddav.datastore.query.filter import Filter
</span><span class="cx"> from txdav.common.icommondatastore import ConcurrentModification
</span><span class="cx"> from txdav.xml import element as davxml
</span><span class="cx"> from txdav.xml.base import dav_namespace
</span><ins>+from txweb2 import responsecode
+from txweb2.dav.http import ErrorResponse, MultiStatusResponse
+from txweb2.dav.resource import AccessDeniedError
+from txweb2.http import HTTPError, StatusResponse
+from urllib import unquote
</ins><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="lines">@@ -58,11 +53,11 @@
</span><span class="cx"> 
</span><span class="cx">         if collection_type == COLLECTION_TYPE_CALENDAR:
</span><span class="cx">             if not parent.isPseudoCalendarCollection():
</span><del>-                log.error(&quot;calendar-multiget report is not allowed on a resource outside of a calendar collection %s&quot; % (self,))
</del><ins>+                log.error(&quot;calendar-multiget report is not allowed on a resource outside of a calendar collection {res}&quot;, res=self)
</ins><span class="cx">                 raise HTTPError(StatusResponse(responsecode.FORBIDDEN, &quot;Must be calendar resource&quot;))
</span><span class="cx">         elif collection_type == COLLECTION_TYPE_ADDRESSBOOK:
</span><span class="cx">             if not parent.isAddressBookCollection():
</span><del>-                log.error(&quot;addressbook-multiget report is not allowed on a resource outside of an address book collection %s&quot; % (self,))
</del><ins>+                log.error(&quot;addressbook-multiget report is not allowed on a resource outside of an address book collection {res}&quot;, res=self)
</ins><span class="cx">                 raise HTTPError(StatusResponse(responsecode.FORBIDDEN, &quot;Must be address book resource&quot;))
</span><span class="cx"> 
</span><span class="cx">     responses = []
</span><span class="lines">@@ -106,7 +101,7 @@
</span><span class="cx"> 
</span><span class="cx">     # Check size of results is within limit when data property requested
</span><span class="cx">     if hasData and len(resources) &gt; config.MaxMultigetWithDataHrefs:
</span><del>-        log.error(&quot;Too many results in multiget report returning data: %d&quot; % len(resources))
</del><ins>+        log.error(&quot;Too many resources in multiget report: {count}&quot;, count=len(resources))
</ins><span class="cx">         raise HTTPError(ErrorResponse(
</span><span class="cx">             responsecode.FORBIDDEN,
</span><span class="cx">             davxml.NumberOfMatchesWithinLimits(),
</span><span class="lines">@@ -172,7 +167,7 @@
</span><span class="cx"> 
</span><span class="cx">             # Special for addressbooks
</span><span class="cx">             if collection_type == COLLECTION_TYPE_ADDRESSBOOK:
</span><del>-                if self.isDirectoryBackedAddressBookCollection() and self.directory.liveQuery:
</del><ins>+                if self.isDirectoryBackedAddressBookCollection():
</ins><span class="cx">                     result = (yield doDirectoryAddressBookResponse())
</span><span class="cx">                     returnValue(result)
</span><span class="cx"> 
</span><span class="lines">@@ -214,8 +209,7 @@
</span><span class="cx">                         isowner=isowner
</span><span class="cx">                     )
</span><span class="cx">                 except ValueError:
</span><del>-                    log.error(&quot;Invalid calendar resource during multiget: %s&quot; %
-                              (href,))
</del><ins>+                    log.error(&quot;Invalid calendar resource during multiget: {href}&quot;, href=href)
</ins><span class="cx">                     responses.append(davxml.StatusResponse(
</span><span class="cx">                         davxml.HRef.fromString(href),
</span><span class="cx">                         davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
</span><span class="lines">@@ -225,7 +219,7 @@
</span><span class="cx">                     # of one of these resources in another request.  In this
</span><span class="cx">                     # case, return a 404 for the now missing resource rather
</span><span class="cx">                     # than raise an error for the entire report.
</span><del>-                    log.error(&quot;Missing resource during multiget: %s&quot; % (href,))
</del><ins>+                    log.error(&quot;Missing resource during multiget: {href}&quot;, href=href)
</ins><span class="cx">                     responses.append(davxml.StatusResponse(
</span><span class="cx">                         davxml.HRef.fromString(href),
</span><span class="cx">                         davxml.Status.fromResponseCode(responsecode.NOT_FOUND)
</span><span class="lines">@@ -255,11 +249,13 @@
</span><span class="cx">                     resource_name = unquote(resource_uri[resource_uri.rfind(&quot;/&quot;) + 1:])
</span><span class="cx">                     if self._isChildURI(request, resource_uri) and resource_name.endswith(&quot;.vcf&quot;) and len(resource_name) &gt; 4:
</span><span class="cx">                         valid_hrefs.append(href)
</span><ins>+                        textMatchElement = carddavxml.TextMatch.fromString(resource_name[:-4])
+                        textMatchElement.attributes[&quot;match-type&quot;] = &quot;equals&quot; # do equals compare. Default is &quot;contains&quot;
</ins><span class="cx">                         vCardFilters.append(carddavxml.PropertyFilter(
</span><del>-                                                carddavxml.TextMatch.fromString(resource_name[:-4]),
</del><ins>+                                                textMatchElement,
</ins><span class="cx">                                                 name=&quot;UID&quot;, # attributes
</span><span class="cx">                                             ))
</span><del>-                    elif not self.directory.cacheQuery:
</del><ins>+                    else:
</ins><span class="cx">                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
</span><span class="cx"> 
</span><span class="cx">                 # exit if not valid
</span><span class="lines">@@ -268,40 +264,29 @@
</span><span class="cx"> 
</span><span class="cx">                 addressBookFilter = carddavxml.Filter(*vCardFilters)
</span><span class="cx">                 addressBookFilter = Filter(addressBookFilter)
</span><del>-                if self.directory.cacheQuery:
-                    # add vcards to directory address book and run &quot;normal case&quot; below
-                    limit = config.DirectoryAddressBook.MaxQueryResults
-                    directoryAddressBookLock, limited = (yield  self.directory.cacheVCardsForAddressBookQuery(addressBookFilter, propertyreq, limit))
-                    if limited:
-                        log.error(&quot;Too many results in multiget report: %d&quot; % len(resources))
-                        raise HTTPError(ErrorResponse(
-                            responsecode.FORBIDDEN,
-                            (dav_namespace, &quot;number-of-matches-within-limits&quot;),
-                            &quot;Too many results&quot;,
-                        ))
-                else:
-                    #get vCards and filter
-                    limit = config.DirectoryAddressBook.MaxQueryResults
-                    vCardRecords, limited = (yield self.directory.vCardRecordsForAddressBookQuery(addressBookFilter, propertyreq, limit))
-                    if limited:
-                        log.error(&quot;Too many results in multiget report: %d&quot; % len(resources))
-                        raise HTTPError(ErrorResponse(
-                            responsecode.FORBIDDEN,
-                            (dav_namespace, &quot;number-of-matches-within-limits&quot;),
-                            &quot;Too many results&quot;,
-                        ))
</del><span class="cx"> 
</span><del>-                    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
</del><ins>+                #get vCards and filter
+                limit = config.DirectoryAddressBook.MaxQueryResults
+                results, limited = (yield self.doAddressBookDirectoryQuery(addressBookFilter, propertyreq, limit))
+                if limited:
+                    log.error(&quot;Too many results in multiget report: {count}&quot;, count=len(resources))
+                    raise HTTPError(ErrorResponse(
+                        responsecode.FORBIDDEN,
+                        (dav_namespace, &quot;number-of-matches-within-limits&quot;),
+                        &quot;Too many results&quot;,
+                    ))
</ins><span class="cx"> 
</span><del>-                        if matchingRecord:
-                            yield report_common.responseForHref(request, responses, href, matchingRecord, propertiesForResource, propertyreq, vcard=matchingRecord.vCard())
-                        else:
-                            responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
</del><ins>+                for href in valid_hrefs:
+                    matchingResource = None
+                    for vCardResource in results:
+                        if href == vCardResource.hRef(): # might need to compare urls instead - also case sens ok?
+                            matchingResource = vCardResource
+                            break
+
+                    if matchingResource:
+                        yield report_common.responseForHref(request, responses, href, matchingResource, propertiesForResource, propertyreq, vcard=matchingResource.vCard())
+                    else:
+                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
</ins><span class="cx">             finally:
</span><span class="cx">                 if directoryAddressBookLock:
</span><span class="cx">                     yield directoryAddressBookLock.release()
</span></span></pre>
</div>
</div>

</body>
</html>