<!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>[13046] 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/13046">13046</a></dd>
<dt>Author</dt> <dd>gaya@apple.com</dd>
<dt>Date</dt> <dd>2014-03-28 21:55:59 -0700 (Fri, 28 Mar 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>10 CDT CardDAV/report.xml directory gateway tests now succeed</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserssagenmove2who4twistedcaldavdirectorybackedaddressbookpy">CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directorybackedaddressbook.py</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserssagenmove2who4txdavwhovcardpy">CalendarServer/branches/users/sagen/move2who-4/txdav/who/vcard.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServerbranchesuserssagenmove2who4twistedcaldavdirectorybackedaddressbookpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directorybackedaddressbook.py (13045 => 13046)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directorybackedaddressbook.py        2014-03-29 01:35:23 UTC (rev 13045)
+++ CalendarServer/branches/users/sagen/move2who-4/twistedcaldav/directorybackedaddressbook.py        2014-03-29 04:55:59 UTC (rev 13046)
</span><span class="lines">@@ -22,10 +22,12 @@
</span><span class="cx">     &quot;DirectoryBackedAddressBookResource&quot;,
</span><span class="cx"> ]
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from twext.who.expression import Operand, MatchType, MatchFlags, \
</span><span class="cx">     MatchExpression, CompoundExpression
</span><span class="cx"> from twext.who.idirectory import FieldName, RecordType
</span><ins>+from twisted.internet.defer import deferredGenerator
</ins><span class="cx"> from twisted.internet.defer import succeed, inlineCallbacks, maybeDeferred, \
</span><span class="cx">     returnValue
</span><span class="cx"> from twisted.python.constants import NamedConstant
</span><span class="lines">@@ -34,18 +36,25 @@
</span><span class="cx"> from twistedcaldav.resource import CalDAVResource
</span><span class="cx"> from txdav.carddav.datastore.query.filter import IsNotDefined, TextMatch, \
</span><span class="cx">     ParameterFilter
</span><del>-from txdav.who.idirectory import FieldName as CalFieldName, \
-    RecordType as CalRecordType
</del><ins>+from txdav.who.idirectory import FieldName as CalFieldName
+from txdav.who.vcard import recordTypeToVCardKindMap, vCardPropToParamMap, \
+    vCardConstantProperties, vCardFromRecord
</ins><span class="cx"> from txdav.xml import element as davxml
</span><ins>+from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, \
+    twisted_private_namespace
</ins><span class="cx"> from txweb2 import responsecode
</span><ins>+from txweb2.dav.resource import DAVPropertyMixIn
</ins><span class="cx"> from txweb2.dav.resource import TwistedACLInheritable
</span><ins>+from txweb2.dav.util import joinURL
</ins><span class="cx"> from txweb2.http import HTTPError, StatusResponse
</span><ins>+from txweb2.http_headers import MimeType, generateContentType, ETag
+from xmlrpclib import datetime
+import hashlib
</ins><span class="cx"> import uuid
</span><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-
</del><span class="cx"> class DirectoryBackedAddressBookResource (CalDAVResource):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Directory-backed address book
</span><span class="lines">@@ -156,44 +165,31 @@
</span><span class="cx">         limited = False
</span><span class="cx">         maxQueryRecords = 0
</span><span class="cx"> 
</span><del>-        schema = {
</del><ins>+        searchableFields = {
</ins><span class="cx">             RecordType.user: {
</span><del>-                &quot;FN&quot;: (
-                        FieldName.fullNames,
-                        FieldName.shortNames,
-                        ),
-                &quot;N&quot;: (
-                        FieldName.fullNames,
-                        FieldName.shortNames,
-                        ),
</del><ins>+                &quot;FN&quot;: FieldName.fullNames,
+                &quot;N&quot;: FieldName.fullNames,
</ins><span class="cx">                 &quot;EMAIL&quot;: FieldName.emailAddresses,
</span><span class="cx">                 &quot;UID&quot;: FieldName.uid,
</span><del>-                &quot;ADR&quot;: CalFieldName.streetAddress,
</del><ins>+                &quot;ADR&quot;: (
+                        CalFieldName.streetAddress,
+                        CalFieldName.floor,
+                        )
</ins><span class="cx">              },
</span><span class="cx">             RecordType.group: {
</span><del>-                &quot;FN&quot;: (
-                        FieldName.fullNames,
-                        FieldName.shortNames,
-                        ),
-                &quot;N&quot;: (
-                        FieldName.fullNames,
-                        FieldName.shortNames,
-                        ),
</del><ins>+                &quot;FN&quot;: FieldName.fullNames,
+                &quot;N&quot;: FieldName.fullNames,
</ins><span class="cx">                 &quot;EMAIL&quot;: FieldName.emailAddresses,
</span><span class="cx">                 &quot;UID&quot;: FieldName.uid,
</span><del>-                &quot;ADR&quot;: CalFieldName.streetAddress,
-                # LATER &quot;X-ADDRESSBOOKSERVER-MEMBER&quot;: FieldName.members,
</del><ins>+                &quot;ADR&quot;: (
+                        CalFieldName.streetAddress,
+                        CalFieldName.floor,
+                        )
+                # LATER &quot;X-ADDRESSBOOKSERVER-MEMBER&quot;: FieldName.membersUIDs,
</ins><span class="cx">              },
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-        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())
</del><ins>+        allowedRecordTypes = set(self.directory.recordTypes()) &amp; set(recordTypeToVCardKindMap.keys()) &amp; set(searchableFields.keys())
</ins><span class="cx">         log.debug(&quot;doAddressBookDirectoryQuery: allowedRecordTypes={allowedRecordTypes}&quot;, allowedRecordTypes=allowedRecordTypes,)
</span><span class="cx"> 
</span><span class="cx">         expressions = []
</span><span class="lines">@@ -201,14 +197,14 @@
</span><span class="cx"> 
</span><span class="cx">             #log.debug(&quot;doAddressBookDirectoryQuery: recordType={recordType}&quot;, recordType=recordType,)
</span><span class="cx"> 
</span><del>-            vcardPropToRecordFieldMap = schema[recordType]
-            kind = recordTypeToKindMap[recordType]
-            constantProperties = ABDirectoryQueryResult.constantProperties.copy()
</del><ins>+            vcardPropToRecordFieldMap = searchableFields[recordType]
+            kind = recordTypeToVCardKindMap[recordType]
+            constantProperties = vCardConstantProperties.copy()
</ins><span class="cx">             constantProperties[&quot;KIND&quot;] = kind
</span><span class="cx">             # add KIND as constant so that query can be skipped if addressBookFilter needs a different kind
</span><span class="cx"> 
</span><span class="cx">             propNames, expression = expressionFromABFilter(addressBookFilter, vcardPropToRecordFieldMap, recordType=recordType, constantProperties=constantProperties)
</span><del>-            log.debug(&quot;doAddressBookDirectoryQuery: recordType={recordType}, expression={expression!r}, propNames={propNames}&quot;, recordType=recordType, expression=expression, propNames=propNames)
</del><ins>+            #log.debug(&quot;doAddressBookDirectoryQuery: recordType={recordType}, expression={expression!r}, propNames={propNames}&quot;, recordType=recordType, expression=expression, propNames=propNames)
</ins><span class="cx">             if expression:
</span><span class="cx">                 expressions.append(expression)
</span><span class="cx"> 
</span><span class="lines">@@ -229,14 +225,14 @@
</span><span class="cx">                 log.debug(&quot;doAddressBookDirectoryQuery: #records={n}, records={records!r}&quot;, n=len(records), records=records)
</span><span class="cx">                 queryLimited = False
</span><span class="cx"> 
</span><del>-                vCardsResults = []#[ABDirectoryQueryResult(self, record) for record in records]
</del><ins>+                vCardsResults = [ABDirectoryQueryResult(self, record) for record in records]
</ins><span class="cx"> 
</span><span class="cx">                 filteredResults = []
</span><span class="cx">                 for vCardResult in vCardsResults:
</span><span class="cx">                     if addressBookFilter.match(vCardResult.vCard()):
</span><span class="cx">                         filteredResults.append(vCardResult)
</span><span class="cx">                     else:
</span><del>-                        log.debug(&quot;doAddressBookQuery: vCard did not match filter: {vCard}&quot;, vcard=vCardResult.vCard())
</del><ins>+                        log.debug(&quot;doAddressBookDirectoryQuery: vCard did not match filter:\n{vcard}&quot;, vcard=vCardResult.vCard())
</ins><span class="cx"> 
</span><span class="cx">                 #no more results
</span><span class="cx">                 if not queryLimited:
</span><span class="lines">@@ -262,7 +258,7 @@
</span><span class="cx">             results = sorted(list(filteredResults), key=lambda result: result.vCard().propertyValue(&quot;UID&quot;))
</span><span class="cx">             limited = maxResults and len(results) &gt;= maxResults
</span><span class="cx"> 
</span><del>-        log.info(&quot;limited={l} result count={n}&quot;, l=limited, n=len(results))
</del><ins>+        log.info(&quot;limited={l} #results={n}&quot;, l=limited, n=len(results))
</ins><span class="cx">         returnValue((results, limited,))
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -280,7 +276,7 @@
</span><span class="cx">             if isinstance(property, carddavxml.AddressData):
</span><span class="cx">                 for addressProperty in property.children:
</span><span class="cx">                     if isinstance(addressProperty, carddavxml.Property):
</span><del>-                        propertyNames += [addressProperty.attributes[&quot;name&quot;], ]
</del><ins>+                        propertyNames.append(addressProperty.attributes[&quot;name&quot;])
</ins><span class="cx"> 
</span><span class="cx">             elif property.qname() == (&quot;DAV:&quot;, &quot;getetag&quot;):
</span><span class="cx">                 # for a real etag == md5(vCard), we need all properties
</span><span class="lines">@@ -302,6 +298,14 @@
</span><span class="cx"> 
</span><span class="cx">     def propFilterListQuery(filterAllOf, propFilters):
</span><span class="cx"> 
</span><ins>+        &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;
+
</ins><span class="cx">         def combineExpressionLists(expressionList, allOf, addedExpressions):
</span><span class="cx">             &quot;&quot;&quot;
</span><span class="cx">             deal with the 4-state logic
</span><span class="lines">@@ -331,7 +335,7 @@
</span><span class="cx">                             expressionList = addedExpressions  # False or addedExpressions is addedExpressions
</span><span class="cx">                         #else False and addedExpressions is False
</span><span class="cx">                     else:
</span><del>-                        expressionList += addedExpressions
</del><ins>+                        expressionList.extend(addedExpressions)
</ins><span class="cx">             return expressionList
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -365,7 +369,7 @@
</span><span class="cx"> 
</span><span class="cx">             def paramFilterElementExpression(propFilterAllOf, paramFilterElement): #@UnusedVariable
</span><span class="cx"> 
</span><del>-                params = ABDirectoryQueryResult.vcardPropToParamMap.get(propFilter.filter_name.upper())
</del><ins>+                params = vCardPropToParamMap.get(propFilter.filter_name.upper())
</ins><span class="cx">                 defined = params and paramFilterElement.filter_name.upper() in params
</span><span class="cx"> 
</span><span class="cx">                 #defined test
</span><span class="lines">@@ -497,13 +501,6 @@
</span><span class="cx">             return propFilterExpressions
</span><span class="cx">             #end propFilterExpression
</span><span class="cx"> 
</span><del>-        &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;
</del><span class="cx">         expressions = None
</span><span class="cx">         for propFilter in propFilters:
</span><span class="cx"> 
</span><span class="lines">@@ -515,7 +512,7 @@
</span><span class="cx">                 break
</span><span class="cx"> 
</span><span class="cx">         # convert to needsAllRecords to return
</span><del>-        log.debug(&quot;expressionFromABFilter: expressions={q!r}&quot;, q=expressions,)
</del><ins>+        # log.debug(&quot;expressionFromABFilter: expressions={q!r}&quot;, q=expressions,)
</ins><span class="cx">         if isinstance(expressions, list):
</span><span class="cx">             expressions = list(set(expressions))
</span><span class="cx">             if len(expressions) &gt; 1:
</span><span class="lines">@@ -534,8 +531,6 @@
</span><span class="cx"> 
</span><span class="cx">         return (tuple(set(properties)), expr)
</span><span class="cx"> 
</span><del>-    # Assume the filter is valid
-
</del><span class="cx">     # Top-level filter contains zero or more prop-filters
</span><span class="cx">     properties = tuple()
</span><span class="cx">     expression = None
</span><span class="lines">@@ -559,228 +554,19 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-#===============================================================================
-# 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
-
-
</del><span class="cx"> class ABDirectoryQueryResult(DAVPropertyMixIn):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Result from ab query report or multiget on directory
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-    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,
</del><ins>+    def __init__(self, directoryBackedAddressBook, record,
</ins><span class="cx">                  kind=None,
</span><del>-                 additionalVCardProps=None,
</del><ins>+                 addProps=None,
</ins><span class="cx">                  ):
</span><span class="cx"> 
</span><del>-        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,)
-
</del><span class="cx">         self._directoryBackedAddressBook = directoryBackedAddressBook
</span><del>-        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;)
-            else:
-                self.attributes[key] = values
-
-        # 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,])
-
-            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()
-
</del><span class="cx">         #generate a vCard here.  May throw an exception
</span><del>-        self.vCard()
</del><ins>+        self._vCard = vCardFromRecord(record, kind, addProps, directoryBackedAddressBook.uri)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def __repr__(self):
</span><span class="lines">@@ -790,572 +576,16 @@
</span><span class="cx">             uid=self.vCard().propertyValue(&quot;UID&quot;)
</span><span class="cx">         )
</span><span class="cx"> 
</span><del>-
</del><ins>+    '''
</ins><span class="cx">     def __hash__(self):
</span><span class="cx">         s = &quot;&quot;.join([
</span><span class="cx">               &quot;{attr}:{values}&quot;.format(attr=attribute, values=self.valuesForAttribute(attribute),)
</span><span class="cx">               for attribute in self.attributes
</span><span class="cx">               ])
</span><span class="cx">         return hash(s)
</span><ins>+    '''
</ins><span class="cx"> 
</span><del>-
-    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
-
-
</del><span class="cx">     def vCard(self):
</span><del>-
-        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()
-
</del><span class="cx">         return self._vCard
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -1397,12 +627,7 @@
</span><span class="cx">                 if self.vCard().hasProperty(&quot;REV&quot;):
</span><span class="cx">                     modDatetime = parse_date(self.vCard().propertyValue(&quot;REV&quot;))
</span><span class="cx">                 else:
</span><del>-                    # use creation date attribute if it exists
-                    creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
-                    if creationDateString:
-                        modDatetime = parse_date(creationDateString)
-                    else:
-                        modDatetime = datetime.datetime.utcnow()
</del><ins>+                    modDatetime = datetime.datetime.utcnow()
</ins><span class="cx"> 
</span><span class="cx">                 #strip time zone because time zones are unimplemented in davxml.GETLastModified.fromDate
</span><span class="cx">                 d = modDatetime.date()
</span><span class="lines">@@ -1411,10 +636,7 @@
</span><span class="cx">                 result = davxml.GETLastModified.fromDate(modDatetimeNoTZ)
</span><span class="cx">                 return result
</span><span class="cx">             elif name == &quot;creationdate&quot;:
</span><del>-                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
</del><ins>+                if self.vCard().hasProperty(&quot;REV&quot;):  # use modification date property if it exists
</ins><span class="cx">                     creationDatetime = parse_date(self.vCard().propertyValue(&quot;REV&quot;))
</span><span class="cx">                 else:
</span><span class="cx">                     creationDatetime = datetime.datetime.utcnow()
</span><span class="lines">@@ -1449,10 +671,3 @@
</span><span class="cx">         yield qnames
</span><span class="cx"> 
</span><span class="cx">     listProperties = deferredGenerator(listProperties)
</span><del>-
-
-
-#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
</del></span></pre></div>
<a id="CalendarServerbranchesuserssagenmove2who4txdavwhovcardpy"></a>
<div class="addfile"><h4>Added: CalendarServer/branches/users/sagen/move2who-4/txdav/who/vcard.py (0 => 13046)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sagen/move2who-4/txdav/who/vcard.py                                (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-4/txdav/who/vcard.py        2014-03-29 04:55:59 UTC (rev 13046)
</span><span class="lines">@@ -0,0 +1,324 @@
</span><ins>+##
+# 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;
+    Utilities to converting a Record to a vCard
+&quot;&quot;&quot;
+
+__all__ = [
+    &quot;vCardFromRecord&quot;
+]
+
+from pycalendar.vcard.adr import Adr
+from pycalendar.vcard.n import N
+from twext.python.log import Logger
+from twext.who.idirectory import FieldName, RecordType
+from twistedcaldav.config import config
+from twistedcaldav.vcard import Component, Property, vCardProductID
+from txdav.who.idirectory import FieldName as CalFieldName, \
+    RecordType as CalRecordType
+from txweb2.dav.util import joinURL
+
+log = Logger()
+
+
+recordTypeToVCardKindMap = {
+   RecordType.user: &quot;individual&quot;,
+   RecordType.group: &quot;group&quot;,
+   CalRecordType.location: &quot;location&quot;,
+   CalRecordType.resource: &quot;device&quot;,
+}
+
+
+# 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;,), },
+}
+
+
+vCardConstantProperties = {
+    #===================================================================
+    # 3.6 EXPLANATORY TYPES http://tools.ietf.org/html/rfc2426#section-3.6
+    #===================================================================
+    # 3.6.3 PRODID
+    &quot;PRODID&quot;: vCardProductID,
+    # 3.6.9 VERSION
+    &quot;VERSION&quot;: &quot;3.0&quot;,
+}
+
+
+def vCardFromRecord(record, kind=None, addProps=None, parentURI=None):
+
+    def isUniqueProperty(newProperty, ignoredParameters={}):
+        existingProperties = vcard.properties(newProperty.name())
+        for existingProperty in existingProperties:
+            if ignoredParameters:
+                existingProperty = existingProperty.duplicate()
+                for paramName, paramValues in ignoredParameters.iteritems():
+                    for paramValue in paramValues:
+                        existingProperty.removeParameterValue(paramName, paramValue)
+            if existingProperty == newProperty:
+                return False
+        return True
+
+
+    def addUniqueProperty(newProperty, ignoredParameters=None):
+        if isUniqueProperty(newProperty, ignoredParameters):
+            vcard.addProperty(newProperty)
+        else:
+            log.info(
+                &quot;Ignoring property {prop!r} it is a duplicate&quot;,
+                prop=newProperty
+            )
+
+    #=======================================================================
+    # start
+    #=======================================================================
+
+    log.debug(&quot;vCardFromRecord: record={record}, kind={kind}, addProps={addProps}, parentURI={parentURI}&quot;,
+                   record=record, kind=kind, addProps=addProps, parentURI=parentURI)
+
+    if kind is None:
+        kind = recordTypeToVCardKindMap.get(record.recordType, &quot;individual&quot;)
+
+    constantProperties = vCardConstantProperties.copy()
+    if addProps:
+        for key, value in addProps.iteritems():
+            if key not in constantProperties:
+                constantProperties[key] = value
+
+    # create vCard
+    vcard = Component(&quot;VCARD&quot;)
+
+    # add constant properties
+    for key, value in constantProperties.items():
+        vcard.addProperty(Property(key, value))
+
+    #===========================================================================
+    # 2.1 Predefined Type Usage
+    #===========================================================================
+    # 2.1.4 SOURCE Type http://tools.ietf.org/html/rfc2426#section-2.1.4
+    if parentURI:
+        uri = joinURL(parentURI, record.fields[FieldName.uid].encode(&quot;utf-8&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))
+
+    #===================================================================
+    # 3.1 IDENTIFICATION TYPES http://tools.ietf.org/html/rfc2426#section-3.1
+    #===================================================================
+    # 3.1.1 FN
+    vcard.addProperty(Property(&quot;FN&quot;, record.fields[FieldName.fullNames][0].encode(&quot;utf-8&quot;)))
+
+    # 3.1.2 N
+    # TODO: Better parsing
+    fullNameParts = record.fields[FieldName.fullNames][0].split()
+    first = fullNameParts[0] if len(fullNameParts) &gt;= 2 else None
+    last = fullNameParts[len(fullNameParts) - 1]
+    middle = fullNameParts[1] if len(fullNameParts) == 3 else None
+    prefix = None
+    suffix = None
+
+    nameObject = N(
+        first=first.encode(&quot;utf-8&quot;) if first else None,
+        last=last.encode(&quot;utf-8&quot;) if last else None,
+        middle=middle.encode(&quot;utf-8&quot;) if middle else None,
+        prefix=prefix.encode(&quot;utf-8&quot;) if prefix else None,
+        suffix=suffix.encode(&quot;utf-8&quot;) if suffix else None,
+    )
+    vcard.addProperty(Property(&quot;N&quot;, nameObject))
+
+    # 3.1.3 NICKNAME
+    nickname = record.fields.get(CalFieldName.abbreviatedName)
+    if nickname:
+        vcard.addProperty(Property(&quot;NICKNAME&quot;, nickname.encode(&quot;utf-8&quot;)))
+
+    # UNIMPLEMENTED
+    #     3.1.4 PHOTO
+    #     3.1.5 BDAY
+
+    #===========================================================================
+    # 3.2 Delivery Addressing Types http://tools.ietf.org/html/rfc2426#section-3.2
+    #===========================================================================
+    # 3.2.1 ADR
+
+    extended = record.fields.get(CalFieldName.floor)
+
+    #TODO: parse !
+    street = record.fields.get(CalFieldName.streetAddress)
+    city = None
+    region = None
+    postalcode = None
+    country = None
+
+    if extended or street or city or region or postalcode or country:
+        vcard.addProperty(
+            Property(
+                &quot;ADR&quot;, Adr(
+                    #pobox = box,
+                    extended=extended.encode(&quot;utf-8&quot;) if extended else None,
+                    street=street.encode(&quot;utf-8&quot;) if street else None,
+                    locality=city.encode(&quot;utf-8&quot;) if city else None,
+                    region=region.encode(&quot;utf-8&quot;) if region else None,
+                    postalcode=postalcode.encode(&quot;utf-8&quot;) if postalcode else None,
+                    country=country.encode(&quot;utf-8&quot;) if country else None,
+                ),
+                params={&quot;TYPE&quot;: (&quot;WORK&quot;, &quot;PREF&quot;, &quot;POSTAL&quot;, &quot;PARCEL&quot;,), }
+            )
+        )
+
+    # UNIMPLEMENTED
+    #     3.2.2 LABEL
+
+    #===================================================================
+    # 3.3 TELECOMMUNICATIONS ADDRESSING TYPES http://tools.ietf.org/html/rfc2426#section-3.3
+    #===================================================================
+    #
+    # UNIMPLEMENTED
+    #     3.3.1 TEL
+
+    # 3.3.2 EMAIL
+    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 record.fields.get(FieldName.emailAddresses, []):
+        addUniqueProperty(Property(&quot;EMAIL&quot;, emailAddress.encode(&quot;utf-8&quot;), params=params), ignoredParameters={&quot;TYPE&quot;: (&quot;PREF&quot;,)})
+        params = workParams
+
+    # UNIMPLEMENTED:
+    #     3.3.3 MAILER
+    #
+    #===================================================================
+    # 3.4 GEOGRAPHICAL TYPES http://tools.ietf.org/html/rfc2426#section-3.4
+    #===================================================================
+    #
+    # UNIMPLEMENTED:
+    #     3.4.1 TZ
+    #     3.4.2 GEO
+    #
+    #===================================================================
+    # 3.5 ORGANIZATIONAL TYPES http://tools.ietf.org/html/rfc2426#section-3.5
+    #===================================================================
+    #
+    # UNIMPLEMENTED:
+    #     3.5.1 TITLE
+    #     3.5.2 ROLE
+    #     3.5.3 LOGO
+    #     3.5.4 AGENT
+    #     3.5.5 ORG
+    #
+    #===================================================================
+    # 3.6 EXPLANATORY TYPES http://tools.ietf.org/html/rfc2426#section-3.6
+    #===================================================================
+    #
+    # UNIMPLEMENTED:
+    #     3.6.1 CATEGORIES
+    #     3.6.2 NOTE
+    #
+    # ADDED WITH CONTSTANT PROPERTIES:
+    #     3.6.3 PRODID
+    #
+    # UNIMPLEMENTED:
+    #     3.6.5 SORT-STRING
+    #     3.6.6 SOUND
+
+    # 3.6.7 UID
+    vcard.addProperty(Property(&quot;UID&quot;, record.fields[FieldName.uid].encode(&quot;utf-8&quot;)))
+
+    # UNIMPLEMENTED:
+    #     3.6.8 URL
+
+    # ADDED WITH CONTSTANT PROPERTIES:
+    #     3.6.9 VERSION
+
+    #===================================================================
+    # 3.7 SECURITY TYPES http://tools.ietf.org/html/rfc2426#section-3.7
+    #===================================================================
+    # UNIMPLEMENTED:
+    #     3.7.1 CLASS
+    #     3.7.2 KEY
+
+    #===================================================================
+    # X Properties
+    #===================================================================
+    # UNIMPLEMENTED:
+    #    X-&lt;instant messaging type&gt; such as:
+    #        &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;,
+    #    X-MAIDENNAME
+    #    X-PHONETIC-FIRST-NAME
+    #    X-PHONETIC-MIDDLE-NAME
+    #    X-PHONETIC-LAST-NAME
+    #    X-ABRELATEDNAMES
+
+    # X-GEOLOCATION
+    # TODO: Just made this up.  Check for standard.  Perhaps GEO can be pulled out
+    geographicLocation = record.fields.get(CalFieldName.geographicLocation)
+    if geographicLocation:
+        vcard.addProperty(Property(&quot;X-GEOLOCATION&quot;, geographicLocation.encode(&quot;utf-8&quot;)))
+
+    # X-ADDRESSBOOKSERVER-KIND
+    if kind == &quot;group&quot;:
+        vcard.addProperty(Property(&quot;X-ADDRESSBOOKSERVER-KIND&quot;, kind))
+
+    # add members
+    # FIXME:  members() is a deferred.  I really want non-deferred FieldName.memberUIDs
+    if record.members():
+        pass
+        '''
+        for memberRecord in (yield record.members()):
+            vcard.addProperty(Property(&quot;X-ADDRESSBOOKSERVER-MEMBER&quot;, &quot;urn:uuid:&quot; + memberRecord.fields[FieldName.uid].encode(&quot;utf-8&quot;)))
+        '''
+
+    #===================================================================
+    # vCard 4.0  http://tools.ietf.org/html/rfc6350
+    #===================================================================
+    # UNIMPLEMENTED:
+    #     6.4.3 IMPP http://tools.ietf.org/html/rfc6350#section-6.4.3
+    #
+    # 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;, kind))
+
+    # one more X- related to kind
+    if kind == &quot;org&quot;:
+        vcard.addProperty(Property(&quot;X-ABShowAs&quot;, &quot;COMPANY&quot;))
+
+    log.debug(&quot;vCardFromRecord: vcard=\n{vcard}&quot;, vcard=vcard)
+    return vcard
</ins></span></pre>
</div>
</div>

</body>
</html>