0001 ### BITPIM 0002 ### 0003 ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com> 0004 ### 0005 ### This program is free software; you can redistribute it and/or modify 0006 ### it under the terms of the BitPim license as detailed in the LICENSE file. 0007 ### 0008 ### $Id: vcard.py 4369 2007-08-20 01:59:33Z djpham $ 0009 0010 """Code for reading and writing Vcard 0011 0012 VCARD is defined in RFC 2425 and 2426 0013 """ 0014 from __future__ import with_statement 0015 import sys 0016 import quopri 0017 import base64 0018 import codecs 0019 import cStringIO 0020 0021 import common 0022 import nameparser 0023 import phonenumber 0024 0025 class VFileException(Exception): 0026 pass 0027 0028 class VFile: 0029 _charset_aliases={ 0030 'MACINTOSH': 'MAC_ROMAN' 0031 } 0032 def __init__(self, source): 0033 self.source=source 0034 0035 0036 self.saved=None 0037 0038 def __iter__(self): 0039 return self 0040 0041 def next(self): 0042 # Get the next non-blank line 0043 while True: # python desperately needs do-while 0044 line=self._getnextline() 0045 if line is None: 0046 raise StopIteration() 0047 if len(line)!=0: 0048 break 0049 0050 # Hack for evolution. If ENCODING is QUOTED-PRINTABLE then it doesn't 0051 # offset the next line, so we look to see what the first char is 0052 normalcontinuations=True 0053 colon=line.find(':') 0054 if colon>0: 0055 s=line[:colon].lower().split(";") 0056 if "quoted-printable" in s or 'encoding=quoted-printable' in s: 0057 normalcontinuations=False 0058 while line[-1]=="=" or line[-2]=='=': 0059 if line[-1]=='=': 0060 i=-1 0061 else: 0062 i=-2 0063 nextl=self._getnextline() 0064 if nextl[0] in ("\t", " "): nextl=nextl[1:] 0065 line=line[:i]+nextl 0066 0067 while normalcontinuations: 0068 nextline=self._lookahead() 0069 if nextline is None: 0070 break 0071 if len(nextline)==0: 0072 break 0073 if nextline[0]!=' ' and nextline[0]!='\t': 0074 break 0075 line+=self._getnextline()[1:] 0076 0077 colon=line.find(':') 0078 if colon<1: 0079 # some evolution vcards don't even have colons 0080 # raise VFileException("Invalid property: "+line) 0081 if __debug__: 0082 print "Fixing up bad line",line 0083 colon=len(line) 0084 line+=":" 0085 0086 b4=line[:colon] 0087 line=line[colon+1:].strip() 0088 0089 # upper case and split on semicolons 0090 items=b4.upper().split(";") 0091 0092 newitems=[] 0093 if isinstance(line, unicode): 0094 charset=None 0095 else: 0096 charset="LATIN-1" 0097 for i in items: 0098 # ::TODO:: probably delete anything preceding a '.' 0099 # (see 5.8.2 in rfc 2425) 0100 # look for charset parameter 0101 if i.startswith("CHARSET="): 0102 charset = i[8:] or "LATIN-1" 0103 continue 0104 # unencode anything that needs it 0105 if not i.startswith("ENCODING=") and not i=="QUOTED-PRINTABLE": # evolution doesn't bother with "ENCODING=" 0106 # ::TODO:: deal with backslashes, being especially careful with ones quoting semicolons 0107 newitems.append(i) 0108 continue 0109 try: 0110 if i=='QUOTED-PRINTABLE' or i=="ENCODING=QUOTED-PRINTABLE": 0111 # technically quoted printable is ascii only but we decode anyway since not all vcards comply 0112 line=quopri.decodestring(line) 0113 elif i=='ENCODING=B': 0114 line=base64.decodestring(line) 0115 charset=None 0116 else: 0117 raise VFileException("unknown encoding: "+i) 0118 except Exception,e: 0119 if isinstance(e,VFileException): 0120 raise e 0121 raise VFileException("Exception %s while processing encoding %s on data '%s'" % (str(e), i, line)) 0122 # ::TODO:: repeat above shenanigans looking for a VALUE= thingy and 0123 # convert line as in 5.8.4 of rfc 2425 0124 if len(newitems)==0: 0125 raise VFileException("Line contains no property: %s" % (line,)) 0126 # charset frigging 0127 if charset is not None: 0128 try: 0129 decoder=codecs.getdecoder(self._charset_aliases.get(charset, charset)) 0130 line,_=decoder(line) 0131 except LookupError: 0132 raise VFileException("unknown character set '%s' in parameters %s" % (charset, b4)) 0133 if newitems==["BEGIN"] or newitems==["END"]: 0134 line=line.upper() 0135 return newitems,line 0136 0137 def _getnextline(self): 0138 if self.saved is not None: 0139 line=self.saved 0140 self.saved=None 0141 return line 0142 else: 0143 return self._readandstripline() 0144 0145 def _readandstripline(self): 0146 line=self.source.readline() 0147 if line is not None: 0148 if len(line)==0: 0149 return None 0150 elif line[-2:]=="\r\n": 0151 0152 0153 return line[:-2] 0154 elif line[-1]=='\r' or line[-1]=='\n': 0155 return line[:-1] 0156 return line 0157 0158 def _lookahead(self): 0159 assert self.saved is None 0160 self.saved=self._readandstripline() 0161 return self.saved 0162 0163 class VCards: 0164 "Understands vcards in a vfile" 0165 0166 def __init__(self, vfile): 0167 self.vfile=vfile 0168 0169 def __iter__(self): 0170 return self 0171 0172 def next(self): 0173 # find vcard start 0174 field=value=None 0175 for field,value in self.vfile: 0176 if (field,value)!=(["BEGIN"], "VCARD"): 0177 continue 0178 found=True 0179 break 0180 if (field,value)!=(["BEGIN"], "VCARD"): 0181 # hit eof without any BEGIN:vcard 0182 raise StopIteration() 0183 # suck up lines 0184 lines=[] 0185 for field,value in self.vfile: 0186 if (field,value)!=(["END"], "VCARD"): 0187 lines.append( (field,value) ) 0188 continue 0189 break 0190 if (field,value)!=(["END"], "VCARD"): 0191 raise VFileException("There is a BEGIN:VCARD but no END:VCARD") 0192 return VCard(lines) 0193 0194 class VCard: 0195 "A single vcard" 0196 0197 def __init__(self, lines): 0198 self._version=(2,0) # which version of the vcard spec the card conforms to 0199 self._origin=None # which program exported the vcard 0200 self._data={} 0201 self._groups={} 0202 self.lines=[] 0203 # extract version field 0204 for f,v in lines: 0205 assert len(f) 0206 if f==["X-EVOLUTION-FILE-AS"]: # all evolution cards have this 0207 self._origin="evolution" 0208 if f[0].startswith("ITEM") and (f[0].endswith(".X-ABADR") or f[0].endswith(".X-ABLABEL")): 0209 self._origin="apple" 0210 if len(v) and v[0].find(">!$_") > v[0].find("_$!<") >=0: 0211 self.origin="apple" 0212 if f==["VERSION"]: 0213 ver=v.split(".") 0214 try: 0215 ver=[int(xx) for xx in ver] 0216 except ValueError: 0217 raise VFileException(v+" is not a valid vcard version") 0218 self._version=ver 0219 continue 0220 # convert {home,work}.{tel,label} to {tel,label};{home,work} 0221 # this probably dates from *very* early vcards 0222 if f[0]=="HOME.TEL": f[0:1]=["TEL", "HOME"] 0223 elif f[0]=="HOME.LABEL": f[0:1]=["LABEL", "HOME"] 0224 elif f[0]=="WORK.TEL": f[0:1]=["TEL", "WORK"] 0225 elif f[0]=="WORK.LABEL": f[0:1]=["LABEL", "WORK"] 0226 self.lines.append( (f,v) ) 0227 self._parse(self.lines, self._data) 0228 self._update_groups(self._data) 0229 0230 def getdata(self): 0231 "Returns a dict of the data parsed out of the vcard" 0232 return self._data 0233 0234 def _getfieldname(self, name, dict): 0235 """Returns the fieldname to use in the dict. 0236 0237 For example, if name is "email" and there is no "email" field 0238 in dict, then "email" is returned. If there is already an "email" 0239 field then "email2" is returned, etc""" 0240 if name not in dict: 0241 return name 0242 for i in xrange(2,99999): 0243 if name+`i` not in dict: 0244 return name+`i` 0245 0246 def _parse(self, lines, result): 0247 for field,value in lines: 0248 if len(value.strip())==0: # ignore blank values 0249 continue 0250 if '.' in field[0]: 0251 f=field[0][field[0].find('.')+1:] 0252 else: f=field[0] 0253 t=f.replace("-", "_") 0254 func=getattr(self, "_field_"+t, self._default_field) 0255 func(field, value, result) 0256 0257 def _update_groups(self, result): 0258 """Update the groups info """ 0259 for k,e in self._groups.items(): 0260 self._setvalue(result, *e) 0261 0262 # fields we ignore 0263 0264 def _field_ignore(self, field, value, result): 0265 pass 0266 0267 _field_LABEL=_field_ignore # we use the ADR field instead 0268 _field_BDAY=_field_ignore # not stored in bitpim 0269 _field_ROLE=_field_ignore # not stored in bitpim 0270 _field_CALURI=_field_ignore # not stored in bitpim 0271 _field_CALADRURI=_field_ignore # variant of above 0272 _field_FBURL=_field_ignore # not stored in bitpim 0273 _field_REV=_field_ignore # not stored in bitpim 0274 _field_KEY=_field_ignore # not stored in bitpim 0275 _field_SOURCE=_field_ignore # not stored in bitpim (although arguably part of serials) 0276 _field_PHOTO=_field_ignore # contained either binary image, or external URL, not used by BitPim 0277 0278 # simple fields 0279 0280 def _field_FN(self, field, value, result): 0281 result[self._getfieldname("name", result)]=self.unquote(value) 0282 0283 def _field_TITLE(self, field, value, result): 0284 result[self._getfieldname("title", result)]=self.unquote(value) 0285 0286 def _field_NICKNAME(self, field, value, result): 0287 # ::TODO:: technically this is a comma seperated list .. 0288 result[self._getfieldname("nickname", result)]=self.unquote(value) 0289 0290 def _field_NOTE(self, field, value, result): 0291 result[self._getfieldname("notes", result)]=self.unquote(value) 0292 0293 def _field_UID(self, field, value, result): 0294 result["uid"]=self.unquote(value) # note that we only store one UID (the "U" does stand for unique) 0295 0296 # 0297 # Complex fields 0298 # 0299 0300 def _field_N(self, field, value, result): 0301 value=self.splitandunquote(value) 0302 familyname=givenname=additionalnames=honorificprefixes=honorificsuffixes=None 0303 try: 0304 familyname=value[0] 0305 givenname=value[1] 0306 additionalnames=value[2] 0307 honorificprefixes=value[3] 0308 honorificsuffixes=value[4] 0309 except IndexError: 0310 pass 0311 if familyname is not None and len(familyname): 0312 result[self._getfieldname("last name", result)]=familyname 0313 if givenname is not None and len(givenname): 0314 result[self._getfieldname("first name", result)]=givenname 0315 if additionalnames is not None and len(additionalnames): 0316 result[self._getfieldname("middle name", result)]=additionalnames 0317 if honorificprefixes is not None and len(honorificprefixes): 0318 result[self._getfieldname("prefix", result)]=honorificprefixes 0319 if honorificsuffixes is not None and len(honorificsuffixes): 0320 result[self._getfieldname("suffix", result)]=honorificsuffixes 0321 0322 _field_NAME=_field_N # early versions of vcard did this 0323 0324 def _field_ORG(self, field, value, result): 0325 value=self.splitandunquote(value) 0326 if len(value): 0327 result[self._getfieldname("organisation", result)]=value[0] 0328 for f in value[1:]: 0329 result[self._getfieldname("organisational unit", result)]=f 0330 0331 _field_O=_field_ORG # early versions of vcard did this 0332 0333 def _field_EMAIL(self, field, value, result): 0334 value=self.unquote(value) 0335 # work out the types 0336 types=[] 0337 for f in field[1:]: 0338 if f.startswith("TYPE="): 0339 ff=f[len("TYPE="):].split(",") 0340 else: ff=[f] 0341 types.extend(ff) 0342 0343 # the standard doesn't specify types of "home" and "work" but 0344 # does allow for random user defined types, so we look for them 0345 type=None 0346 for t in types: 0347 if t=="HOME": type="home" 0348 if t=="WORK": type="business" 0349 if t=="X400": return # we don't want no steenking X.400 0350 0351 preferred="PREF" in types 0352 0353 if type is None: 0354 self._setvalue(result, "email", value, preferred) 0355 else: 0356 addr={'email': value, 'type': type} 0357 self._setvalue(result, "email", addr, preferred) 0358 0359 def _field_URL(self, field, value, result): 0360 # the standard doesn't specify url types or a pref type, 0361 # but we implement it anyway 0362 value=self.unquote(value) 0363 # work out the types 0364 types=[] 0365 for f in field[1:]: 0366 if f.startswith("TYPE="): 0367 ff=f[len("TYPE="):].split(",") 0368 else: ff=[f] 0369 types.extend(ff) 0370 0371 type=None 0372 for t in types: 0373 if t=="HOME": type="home" 0374 if t=="WORK": type="business" 0375 0376 preferred="PREF" in types 0377 0378 if type is None: 0379 self._setvalue(result, "url", value, preferred) 0380 else: 0381 addr={'url': value, 'type': type} 0382 self._setvalue(result, "url", addr, preferred) 0383 0384 def _field_X_SPEEDDIAL(self, field, value, result): 0385 if '.' in field[0]: 0386 group=field[0][:field[0].find('.')] 0387 else: 0388 group=None 0389 if group is None: 0390 # this has to belong to a group!! 0391 print 'speedial has no group' 0392 else: 0393 self._setgroupvalue(result, 'phone', { 'speeddial': int(value) }, 0394 group, False) 0395 0396 def _field_TEL(self, field, value, result): 0397 value=self.unquote(value) 0398 # see if this is part of a group 0399 if '.' in field[0]: 0400 group=field[0][:field[0].find('.')] 0401 else: 0402 group=None 0403 0404 # work out the types 0405 types=[] 0406 for f in field[1:]: 0407 if f.startswith("TYPE="): 0408 ff=f[len("TYPE="):].split(",") 0409 else: ff=[f] 0410 types.extend(ff) 0411 0412 # type munging - we map vcard types to simpler ones 0413 munge={ "BBS": "DATA", "MODEM": "DATA", "ISDN": "DATA", "CAR": "CELL", "PCS": "CELL" } 0414 types=[munge.get(t, t) for t in types] 0415 0416 # reduce types to home, work, msg, pref, voice, fax, cell, video, pager, data 0417 types=[t for t in types if t in ("HOME", "WORK", "MSG", "PREF", "VOICE", "FAX", "CELL", "VIDEO", "PAGER", "DATA")] 0418 0419 # if type is in this list and voice not explicitly mentioned then it is not a voice type 0420 antivoice=["FAX", "PAGER", "DATA"] 0421 if "VOICE" in types: 0422 voice=True 0423 else: 0424 voice=True # default is voice 0425 for f in antivoice: 0426 if f in types: 0427 voice=False 0428 break 0429 0430 preferred="PREF" in types 0431 0432 # vcard allows numbers to be multiple things at the same time, such as home voice, home fax 0433 # and work fax so we have to test for all variations 0434 0435 # if neither work or home is specified, then no default (otherwise things get really complicated) 0436 iswork=False 0437 ishome=False 0438 if "WORK" in types: iswork=True 0439 if "HOME" in types: ishome=True 0440 0441 if len(types)==0 or types==["PREF"]: iswork=True # special case when nothing else is specified 0442 0443 0444 value=phonenumber.normalise(value) 0445 if iswork and voice: 0446 self._setgroupvalue(result, 0447 "phone", {"type": "business", "number": value}, 0448 group, preferred) 0449 if ishome and voice: 0450 self._setgroupvalue(result, 0451 "phone", {"type": "home", "number": value}, 0452 group, preferred) 0453 if not iswork and not ishome and "FAX" in types: 0454 # fax without explicit work or home 0455 self._setgroupvalue(result, 0456 "phone", {"type": "fax", "number": value}, 0457 group, preferred) 0458 else: 0459 if iswork and "FAX" in types: 0460 self._setgroupvalue(result, "phone", 0461 {"type": "business fax", "number": value}, 0462 group, preferred) 0463 if ishome and "FAX" in types: 0464 self._setgroupvalue(result, "phone", 0465 {"type": "home fax", "number": value}, 0466 group, preferred) 0467 if "CELL" in types: 0468 self._setgroupvalue(result, 0469 "phone", {"type": "cell", "number": value}, 0470 group, preferred) 0471 if "PAGER" in types: 0472 self._setgroupvalue(result, 0473 "phone", {"type": "pager", "number": value}, 0474 group, preferred) 0475 if "DATA" in types: 0476 self._setgroupvalue(result, 0477 "phone", {"type": "data", "number": value}, 0478 group, preferred) 0479 0480 def _setgroupvalue(self, result, type, value, group, preferred=False): 0481 """ Set value of an item of a group 0482 """ 0483 if group is None: 0484 # no groups specified 0485 return self._setvalue(result, type, value, preferred) 0486 group_type=self._groups.get(group, None) 0487 if group_type is None: 0488 # 1st one of the group 0489 self._groups[group]=[type, value, preferred] 0490 else: 0491 if type!=group_type[0]: 0492 print 'Group',group,'has different types:',type,groups_type[0] 0493 if preferred: 0494 group_type[2]=True 0495 group_type[1].update(value) 0496 0497 def _setvalue(self, result, type, value, preferred=False): 0498 if type not in result: 0499 result[type]=value 0500 return 0501 if not preferred: 0502 result[self._getfieldname(type, result)]=value 0503 return 0504 # we need to insert our value at the begining 0505 values=[value] 0506 for suffix in [""]+range(2,99): 0507 if type+str(suffix) in result: 0508 values.append(result[type+str(suffix)]) 0509 else: 0510 break 0511 suffixes=[""]+range(2,len(values)+1) 0512 for l in range(len(suffixes)): 0513 result[type+str(suffixes[l])]=values[l] 0514 0515 def _field_CATEGORIES(self, field, value, result): 0516 # comma seperated just for fun 0517 values=self.splitandunquote(value, seperator=",") 0518 values=[v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field 0519 values=[v for v in values if len(v)] 0520 v=result.get('categories', None) 0521 if v: 0522 result['categories']=';'.join([v, ";".join(values)]) 0523 else: 0524 result['categories']=';'.join(values) 0525 0526 def _field_SOUND(self, field, value, result): 0527 # comma seperated just for fun 0528 values=self.splitandunquote(value, seperator=",") 0529 values=[v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field 0530 values=[v for v in values if len(v)] 0531 result[self._getfieldname("ringtones", result)]=";".join(values) 0532 0533 _field_CATEGORY=_field_CATEGORIES # apple use "category" which is not in the spec 0534 0535 def _field_ADR(self, field, value, result): 0536 # work out the type 0537 preferred=False 0538 type="business" 0539 for f in field[1:]: 0540 if f.startswith("TYPE="): 0541 ff=f[len("TYPE="):].split(",") 0542 else: ff=[f] 0543 for x in ff: 0544 if x=="HOME": 0545 type="home" 0546 if x=="PREF": 0547 preferred=True 0548 0549 value=self.splitandunquote(value) 0550 pobox=extendedaddress=streetaddress=locality=region=postalcode=country=None 0551 try: 0552 pobox=value[0] 0553 extendedaddress=value[1] 0554 streetaddress=value[2] 0555 locality=value[3] 0556 region=value[4] 0557 postalcode=value[5] 0558 country=value[6] 0559 except IndexError: 0560 pass 0561 addr={} 0562 if pobox is not None and len(pobox): 0563 addr["pobox"]=pobox 0564 if extendedaddress is not None and len(extendedaddress): 0565 addr["street2"]=extendedaddress 0566 if streetaddress is not None and len(streetaddress): 0567 addr["street"]=streetaddress 0568 if locality is not None and len(locality): 0569 addr["city"]=locality 0570 if region is not None and len(region): 0571 addr["state"]=region 0572 if postalcode is not None and len(postalcode): 0573 addr["postalcode"]=postalcode 0574 if country is not None and len(country): 0575 addr["country"]=country 0576 if len(addr): 0577 addr["type"]=type 0578 self._setvalue(result, "address", addr, preferred) 0579 0580 def _field_X_PALM(self, field, value, result): 0581 # handle a few PALM custom fields 0582 ff=field[0].split(".") 0583 f0=ff[0] 0584 f1=ff[1] if len(ff)>1 else '' 0585 if f0.startswith('X-PALM-CATEGORY') or f1.startswith('X-PALM-CATEGORY'): 0586 self._field_CATEGORIES(['CATEGORIES'], value, result) 0587 elif f0=='X-PALM-NICKNAME' or f1=='X-PALM-NICKNAME': 0588 self._field_NICKNAME(['NICKNAME'], value, result) 0589 else: 0590 if __debug__: 0591 print 'ignoring PALM custom field',field 0592 0593 def _default_field(self, field, value, result): 0594 ff=field[0].split(".") 0595 f0=ff[0] 0596 f1=ff[1] if len(ff)>1 else '' 0597 if f0.startswith('X-PALM-') or f1.startswith('X-PALM-'): 0598 self._field_X_PALM(field, value, result) 0599 return 0600 elif f0.startswith("X-") or f1.startswith("X-"): 0601 if __debug__: 0602 print "ignoring custom field",field 0603 return 0604 if __debug__: 0605 print "no idea what do with" 0606 print "field",field 0607 print "value",value[:80] 0608 0609 def unquote(self, value): 0610 # ::TODO:: do this properly (deal with all backslashes) 0611 return value.replace(r"\;", ";") \ 0612 .replace(r"\,", ",") \ 0613 .replace(r"\n", "\n") \ 0614 .replace(r"\r\n", "\r\n") \ 0615 .replace("\r\n", "\n") \ 0616 .replace("\r", "\n") 0617 0618 def splitandunquote(self, value, seperator=";"): 0619 # also need a splitandsplitandunquote since some ; delimited fields are then comma delimited 0620 0621 # short cut for normal case - no quoted seperators 0622 if value.find("\\"+seperator)<0: 0623 return [self.unquote(v) for v in value.split(seperator)] 0624 0625 # funky quoting, do it the slow hard way 0626 res=[] 0627 build="" 0628 v=0 0629 while v<len(value): 0630 if value[v]==seperator: 0631 res.append(build) 0632 build="" 0633 v+=1 0634 continue 0635 0636 0637 if value[v]=="\\": 0638 build+=value[v:v+2] 0639 v+=2 0640 continue 0641 build+=value[v] 0642 v+=1 0643 if len(build): 0644 res.append(build) 0645 0646 return [self.unquote(v) for v in res] 0647 0648 def version(self): 0649 "Best guess as to vcard version" 0650 return self._version 0651 0652 def origin(self): 0653 "Best guess as to what program wrote the vcard" 0654 return self._origin 0655 0656 def __repr__(self): 0657 str="Version: %s\n" % (`self.version()`) 0658 str+="Origin: %s\n" % (`self.origin()`) 0659 str+=common.prettyprintdict(self._data) 0660 # str+=`self.lines` 0661 return str+"\n" 0662 0663 ### 0664 ### Outputting functions 0665 ### 0666 0667 # The formatters return a string 0668 0669 def myqpencodestring(value): 0670 """My own routine to do qouted printable since the builtin one doesn't encode CR or NL!""" 0671 return quopri.encodestring(value).replace("\r", "=0D").replace("\n", "=0A") 0672 0673 def format_stringv2(value): 0674 """Return a vCard v2 string. Any embedded commas or semi-colons are removed.""" 0675 return value.replace("\\", "").replace(",", "").replace(";", "") 0676 0677 def format_stringv3(value): 0678 """Return a vCard v3 string. Embedded commas and semi-colons are backslash quoted""" 0679 return value.replace("\\", "").replace(",", r"\,").replace(";", r"\;") 0680 0681 _string_formatters=(format_stringv2, format_stringv3) 0682 0683 def format_binary(value): 0684 """Return base 64 encoded string""" 0685 # encodestring always adds a newline so we have to strip it off 0686 return base64.encodestring(value).rstrip() 0687 0688 def _is_sequence(v): 0689 """Determine if v is a sequence such as passed to value in out_line. 0690 Note that a sequence of chars is not a sequence for our purposes.""" 0691 return isinstance(v, (type( () ), type([]))) 0692 0693 def out_line(name, attributes, value, formatter, join_char=";"): 0694 """Returns a single field correctly formatted and encoded (including trailing newline) 0695 0696 @param name: The field name 0697 @param attributes: A list of string attributes (eg "TYPE=intl,post" ). Usually 0698 empty except for TEL and ADR. You can also pass in None. 0699 @param value: The field value. You can also pass in a list of components which will be 0700 joined with join_char such as the 6 components of N 0701 @param formatter: The function that formats the value/components. See the 0702 various format_ functions. They will automatically ensure that 0703 ENCODING=foo attributes are added if appropriate""" 0704 0705 if attributes is None: attributes=[] # ensure it is a list 0706 else: attributes=list(attributes[:]) # ensure we work with a copy 0707 0708 if formatter in _string_formatters: 0709 if _is_sequence(value): 0710 qp=False 0711 for f in value: 0712 f=formatter(f) 0713 if myqpencodestring(f)!=f: 0714 qp=True 0715 break 0716 if qp: 0717 attributes.append("ENCODING=QUOTED-PRINTABLE") 0718 value=[myqpencodestring(f) for f in value] 0719 0720 value=join_char.join(value) 0721 else: 0722 value=formatter(value) 0723 # do the qp test 0724 qp= myqpencodestring(value)!=value 0725 if qp: 0726 value=myqpencodestring(value) 0727 attributes.append("ENCODING=QUOTED-PRINTABLE") 0728 else: 0729 assert not _is_sequence(value) 0730 if formatter is not None: 0731 value=formatter(value) # ::TODO:: deal with binary and other formatters and their encoding types 0732 0733 res=";".join([name]+attributes)+":" 0734 res+=_line_reformat(value, 70, 70-len(res)) 0735 assert res[-1]!="\n" 0736 0737 return res+"\n" 0738 0739 def _line_reformat(line, width=70, firstlinewidth=0): 0740 """Takes line string and inserts newlines 0741 and spaces on following continuation lines 0742 so it all fits in width characters 0743 0744 @param width: how many characters to fit it in 0745 @param firstlinewidth: if >0 then first line is this width. 0746 if equal to zero then first line is same width as rest. 0747 if <0 then first line will go immediately to continuation. 0748 """ 0749 if firstlinewidth==0: firstlinewidth=width 0750 if len(line)<firstlinewidth: 0751 return line 0752 res="" 0753 if firstlinewidth>0: 0754 res+=line[:firstlinewidth] 0755 line=line[firstlinewidth:] 0756 while len(line): 0757 res+="\n "+line[:width] 0758 if len(line)<width: break 0759 line=line[width:] 0760 return res 0761 0762 def out_names(vals, formatter, limit=1): 0763 res="" 0764 for v in vals[:limit]: 0765 # full name 0766 res+=out_line("FN", None, nameparser.formatsimplename(v), formatter) 0767 # name parts 0768 f,m,l=nameparser.getparts(v) 0769 res+=out_line("N", None, (l,f,m,"",""), formatter) 0770 # nickname 0771 nn=v.get("nickname", "") 0772 if len(nn): 0773 res+=out_line("NICKNAME", None, nn, formatter) 0774 return res 0775 0776 # Apple uses wrong field name so we do some futzing ... 0777 def out_categories(vals, formatter, field="CATEGORIES"): 0778 cats=[v.get("category") for v in vals] 0779 if len(cats): 0780 return out_line(field, None, cats, formatter, join_char=",") 0781 return "" 0782 0783 def out_categories_apple(vals, formatter): 0784 return out_categories(vals, formatter, field="CATEGORY") 0785 0786 # Used for both email and urls. we don't put any limits on how many are output 0787 def out_eu(vals, formatter, field, bpkey): 0788 res="" 0789 first=True 0790 for v in vals: 0791 val=v.get(bpkey) 0792 type=v.get("type", "") 0793 if len(type): 0794 if type=="business": type="work" # vcard uses different name 0795 type=type.upper() 0796 if first: 0797 type=type+",PREF" 0798 elif first: 0799 type="PREF" 0800 if len(type): 0801 type=["TYPE="+type+["",",INTERNET"][field=="EMAIL"]] # email also has "INTERNET" 0802 else: 0803 type=None 0804 res+=out_line(field, type, val, formatter) 0805 first=False 0806 return res 0807 0808 def out_emails(vals, formatter): 0809 return out_eu(vals, formatter, "EMAIL", "email") 0810 0811 def out_urls(vals, formatter): 0812 return out_eu(vals, formatter, "URL", "url") 0813 0814 # fun fun fun 0815 _out_tel_mapping={ 'home': 'HOME', 0816 'office': 'WORK', 0817 'cell': 'CELL', 0818 'fax': 'FAX', 0819 'pager': 'PAGER', 0820 'data': 'MODEM', 0821 'none': 'VOICE' 0822 } 0823 def out_tel(vals, formatter): 0824 # ::TODO:: limit to one type of each number 0825 phones=['phone'+str(x) for x in ['']+range(2,len(vals)+1)] 0826 res="" 0827 first=True 0828 idx=0 0829 for v in vals: 0830 sp=v.get('speeddial', None) 0831 if sp is None: 0832 # no speed dial 0833 res+=out_line("TEL", 0834 ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])], 0835 phonenumber.format(v['number']), formatter) 0836 else: 0837 res+=out_line(phones[idx]+".TEL", 0838 ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])], 0839 phonenumber.format(v['number']), formatter) 0840 res+=out_line(phones[idx]+".X-SPEEDDIAL", None, str(sp), formatter) 0841 idx+=1 0842 first=False 0843 return res 0844 0845 # and addresses 0846 def out_adr(vals, formatter): 0847 # ::TODO:: limit to one type of each address, and only one org 0848 res="" 0849 first=True 0850 for v in vals: 0851 o=v.get("company", "") 0852 if len(o): 0853 res+=out_line("ORG", None, o, formatter) 0854 if v.get("type")=="home": type="HOME" 0855 else: type="WORK" 0856 type="TYPE="+type+("", ",PREF")[first] 0857 res+=out_line("ADR", [type], [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")], formatter) 0858 first=False 0859 return res 0860 0861 def out_note(vals, formatter, limit=1): 0862 return "".join([out_line("NOTE", None, v["memo"], formatter) for v in vals[:limit]]) 0863 0864 # Sany SCP-6600 (Katana) support 0865 def out_tel_scp6600(vals, formatter): 0866 res="" 0867 _pref=len(vals)>1 0868 for v in vals: 0869 res+=out_line("TEL", 0870 ["TYPE=%s%s" % ("PREF," if _pref else "", 0871 _out_tel_mapping[v['type']])], 0872 phonenumber.format(v['number']), formatter) 0873 _pref=False 0874 return res 0875 def out_email_scp6600(vals, formatter): 0876 res='' 0877 for _idx in range(min(len(vals), 2)): 0878 v=vals[_idx] 0879 if v.get('email', None): 0880 res+=out_line('EMAIL', ['TYPE=INTERNET'], 0881 v['email'], formatter) 0882 return res 0883 def out_url_scp660(vals, formatter): 0884 if vals and vals[0].get('url', None): 0885 return out_line('URL', None, vals[0]['url'], formatter) 0886 return '' 0887 def out_adr_scp6600(vals, formatter): 0888 for v in vals: 0889 if v.get('type', None)=='home': 0890 _type='HOME' 0891 else: 0892 _type='WORK' 0893 return out_line("ADR", ['TYPE=%s'%_type], 0894 [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")], 0895 formatter) 0896 return '' 0897 0898 # This is the order we write things out to the vcard. Although 0899 # vCard doesn't require an ordering, it looks nicer if it 0900 # is (eg name first) 0901 _field_order=("names", "wallpapers", "addresses", "numbers", "categories", "emails", "urls", "ringtones", "flags", "memos", "serials") 0902 0903 def output_entry(entry, profile, limit_fields=None): 0904 0905 # debug build assertion that limit_fields only contains fields we know about 0906 if __debug__ and limit_fields is not None: 0907 assert len([f for f in limit_fields if f not in _field_order])==0 0908 0909 fmt=profile["_formatter"] 0910 io=cStringIO.StringIO() 0911 io.write(out_line("BEGIN", None, "VCARD", None)) 0912 io.write(out_line("VERSION", None, profile["_version"], None)) 0913 0914 if limit_fields is None: 0915 fields=_field_order 0916 else: 0917 fields=[f for f in _field_order if f in limit_fields] 0918 0919 for f in fields: 0920 if f in entry and f in profile: 0921 func=profile[f] 0922 # does it have a limit? (nice scary introspection :-) 0923 if "limit" in func.func_code.co_varnames[:func.func_code.co_argcount]: 0924 lines=func(entry[f], fmt, limit=profile["_limit"]) 0925 else: 0926 lines=func(entry[f], fmt) 0927 if len(lines): 0928 io.write(lines) 0929 0930 io.write(out_line("END", None, "VCARD", fmt)) 0931 return io.getvalue() 0932 0933 profile_vcard2={ 0934 '_formatter': format_stringv2, 0935 '_limit': 1, 0936 '_version': "2.1", 0937 'names': out_names, 0938 'categories': out_categories, 0939 'emails': out_emails, 0940 'urls': out_urls, 0941 'numbers': out_tel, 0942 'addresses': out_adr, 0943 'memos': out_note, 0944 } 0945 0946 profile_vcard3=profile_vcard2.copy() 0947 profile_vcard3['_formatter']=format_stringv3 0948 profile_vcard3['_version']="3.0" 0949 0950 profile_apple=profile_vcard3.copy() 0951 profile_apple['categories']=out_categories_apple 0952 0953 profile_full=profile_vcard3.copy() 0954 profile_full['_limit']=99999 0955 0956 profile_scp6600=profile_full.copy() 0957 del profile_scp6600['categories'] 0958 profile_scp6600.update( 0959 { 'numbers': out_tel_scp6600, 0960 'emails': out_email_scp6600, 0961 'urls': out_url_scp660, 0962 'addresses': out_adr_scp6600, 0963 }) 0964 0965 profiles={ 0966 'vcard2': { 'description': "vCard v2.1", 'profile': profile_vcard2 }, 0967 'vcard3': { 'description': "vCard v3.0", 'profile': profile_vcard3 }, 0968 'apple': { 'description': "Apple", 'profile': profile_apple }, 0969 'fullv3': { 'description': "Full vCard v3.0", 'profile': profile_full}, 0970 'scp6600': { 'description': "Sanyo SCP-6600 (Katana)", 0971 'profile': profile_scp6600 }, 0972 } 0973 0974 0975 if __name__=='__main__': 0976 0977 def _wrap(func): 0978 try: return func() 0979 except: 0980 print common.formatexception() 0981 sys.exit(1) 0982 0983 def dump_vcards(): 0984 for vcard in VCards(VFile(common.opentextfile(sys.argv[1]))): 0985 # pass 0986 print vcard 0987 0988 def turn_around(): 0989 p="fullv3" 0990 if len(sys.argv)==4: p=sys.argv[4] 0991 print "Using profile", profiles[p]['description'] 0992 profile=profiles[p]['profile'] 0993 0994 d={'result': {}} 0995 try: 0996 execfile(sys.argv[1], d,d) 0997 except UnicodeError: 0998 common.unicode_execfile(sys.argv[1], d,d) 0999 1000 with file(sys.argv[2], "wt") as f: 1001 for k in d['result']['phonebook']: 1002 print >>f, output_entry(d['result']['phonebook'][k], profile) 1003 1004 if len(sys.argv)==2: 1005 # import bp 1006 # bp.profile("vcard.prof", "dump_vcards()") 1007 _wrap(dump_vcards) 1008 elif len(sys.argv)==3 or len(sys.argv)==4: 1009 _wrap(turn_around) 1010 else: 1011 print """one arg: import the named vcard file 1012 two args: first arg is phonebook/index.idx file, write back out to arg2 in vcard format 1013 three args: same as two but last arg is profile to use. 1014 profiles are""", profiles.keys() 1015
Generated by PyXR 0.9.4