Module vcard
[hide private]
[frames] | no frames]

Source Code for Module vcard

   1  ### BITPIM 
   2  ### 
   3  ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com> 
   4  ### 
   5  ### This program is free software; you can redistribute it and/or modify 
   6  ### it under the terms of the BitPim license as detailed in the LICENSE file. 
   7  ### 
   8  ### $Id: vcard.py 4369 2007-08-20 01:59:33Z djpham $ 
   9   
  10  """Code for reading and writing Vcard 
  11   
  12  VCARD is defined in RFC 2425 and 2426 
  13  """ 
  14  from __future__ import with_statement 
  15  import sys 
  16  import quopri 
  17  import base64 
  18  import codecs 
  19  import cStringIO 
  20   
  21  import common 
  22  import nameparser 
  23  import phonenumber 
  24   
25 -class VFileException(Exception):
26 pass
27
28 -class VFile:
29 _charset_aliases={ 30 'MACINTOSH': 'MAC_ROMAN' 31 }
32 - def __init__(self, source):
33 self.source=source 34 35 36 self.saved=None
37
38 - def __iter__(self):
39 return self
40
41 - def next(self):
42 # Get the next non-blank line 43 while True: # python desperately needs do-while 44 line=self._getnextline() 45 if line is None: 46 raise StopIteration() 47 if len(line)!=0: 48 break 49 50 # Hack for evolution. If ENCODING is QUOTED-PRINTABLE then it doesn't 51 # offset the next line, so we look to see what the first char is 52 normalcontinuations=True 53 colon=line.find(':') 54 if colon>0: 55 s=line[:colon].lower().split(";") 56 if "quoted-printable" in s or 'encoding=quoted-printable' in s: 57 normalcontinuations=False 58 while line[-1]=="=" or line[-2]=='=': 59 if line[-1]=='=': 60 i=-1 61 else: 62 i=-2 63 nextl=self._getnextline() 64 if nextl[0] in ("\t", " "): nextl=nextl[1:] 65 line=line[:i]+nextl 66 67 while normalcontinuations: 68 nextline=self._lookahead() 69 if nextline is None: 70 break 71 if len(nextline)==0: 72 break 73 if nextline[0]!=' ' and nextline[0]!='\t': 74 break 75 line+=self._getnextline()[1:] 76 77 colon=line.find(':') 78 if colon<1: 79 # some evolution vcards don't even have colons 80 # raise VFileException("Invalid property: "+line) 81 if __debug__: 82 print "Fixing up bad line",line 83 colon=len(line) 84 line+=":" 85 86 b4=line[:colon] 87 line=line[colon+1:].strip() 88 89 # upper case and split on semicolons 90 items=b4.upper().split(";") 91 92 newitems=[] 93 if isinstance(line, unicode): 94 charset=None 95 else: 96 charset="LATIN-1" 97 for i in items: 98 # ::TODO:: probably delete anything preceding a '.' 99 # (see 5.8.2 in rfc 2425) 100 # look for charset parameter 101 if i.startswith("CHARSET="): 102 charset = i[8:] or "LATIN-1" 103 continue 104 # unencode anything that needs it 105 if not i.startswith("ENCODING=") and not i=="QUOTED-PRINTABLE": # evolution doesn't bother with "ENCODING=" 106 # ::TODO:: deal with backslashes, being especially careful with ones quoting semicolons 107 newitems.append(i) 108 continue 109 try: 110 if i=='QUOTED-PRINTABLE' or i=="ENCODING=QUOTED-PRINTABLE": 111 # technically quoted printable is ascii only but we decode anyway since not all vcards comply 112 line=quopri.decodestring(line) 113 elif i=='ENCODING=B': 114 line=base64.decodestring(line) 115 charset=None 116 else: 117 raise VFileException("unknown encoding: "+i) 118 except Exception,e: 119 if isinstance(e,VFileException): 120 raise e 121 raise VFileException("Exception %s while processing encoding %s on data '%s'" % (str(e), i, line)) 122 # ::TODO:: repeat above shenanigans looking for a VALUE= thingy and 123 # convert line as in 5.8.4 of rfc 2425 124 if len(newitems)==0: 125 raise VFileException("Line contains no property: %s" % (line,)) 126 # charset frigging 127 if charset is not None: 128 try: 129 decoder=codecs.getdecoder(self._charset_aliases.get(charset, charset)) 130 line,_=decoder(line) 131 except LookupError: 132 raise VFileException("unknown character set '%s' in parameters %s" % (charset, b4)) 133 if newitems==["BEGIN"] or newitems==["END"]: 134 line=line.upper() 135 return newitems,line
136
137 - def _getnextline(self):
138 if self.saved is not None: 139 line=self.saved 140 self.saved=None 141 return line 142 else: 143 return self._readandstripline()
144
145 - def _readandstripline(self):
146 line=self.source.readline() 147 if line is not None: 148 if len(line)==0: 149 return None 150 elif line[-2:]=="\r\n": 151 152 153 return line[:-2] 154 elif line[-1]=='\r' or line[-1]=='\n': 155 return line[:-1] 156 return line
157
158 - def _lookahead(self):
159 assert self.saved is None 160 self.saved=self._readandstripline() 161 return self.saved
162
163 -class VCards:
164 "Understands vcards in a vfile" 165
166 - def __init__(self, vfile):
167 self.vfile=vfile
168
169 - def __iter__(self):
170 return self
171
172 - def next(self):
173 # find vcard start 174 field=value=None 175 for field,value in self.vfile: 176 if (field,value)!=(["BEGIN"], "VCARD"): 177 continue 178 found=True 179 break 180 if (field,value)!=(["BEGIN"], "VCARD"): 181 # hit eof without any BEGIN:vcard 182 raise StopIteration() 183 # suck up lines 184 lines=[] 185 for field,value in self.vfile: 186 if (field,value)!=(["END"], "VCARD"): 187 lines.append( (field,value) ) 188 continue 189 break 190 if (field,value)!=(["END"], "VCARD"): 191 raise VFileException("There is a BEGIN:VCARD but no END:VCARD") 192 return VCard(lines)
193
194 -class VCard:
195 "A single vcard" 196
197 - def __init__(self, lines):
198 self._version=(2,0) # which version of the vcard spec the card conforms to 199 self._origin=None # which program exported the vcard 200 self._data={} 201 self._groups={} 202 self.lines=[] 203 # extract version field 204 for f,v in lines: 205 assert len(f) 206 if f==["X-EVOLUTION-FILE-AS"]: # all evolution cards have this 207 self._origin="evolution" 208 if f[0].startswith("ITEM") and (f[0].endswith(".X-ABADR") or f[0].endswith(".X-ABLABEL")): 209 self._origin="apple" 210 if len(v) and v[0].find(">!$_") > v[0].find("_$!<") >=0: 211 self.origin="apple" 212 if f==["VERSION"]: 213 ver=v.split(".") 214 try: 215 ver=[int(xx) for xx in ver] 216 except ValueError: 217 raise VFileException(v+" is not a valid vcard version") 218 self._version=ver 219 continue 220 # convert {home,work}.{tel,label} to {tel,label};{home,work} 221 # this probably dates from *very* early vcards 222 if f[0]=="HOME.TEL": f[0:1]=["TEL", "HOME"] 223 elif f[0]=="HOME.LABEL": f[0:1]=["LABEL", "HOME"] 224 elif f[0]=="WORK.TEL": f[0:1]=["TEL", "WORK"] 225 elif f[0]=="WORK.LABEL": f[0:1]=["LABEL", "WORK"] 226 self.lines.append( (f,v) ) 227 self._parse(self.lines, self._data) 228 self._update_groups(self._data)
229
230 - def getdata(self):
231 "Returns a dict of the data parsed out of the vcard" 232 return self._data
233
234 - def _getfieldname(self, name, dict):
235 """Returns the fieldname to use in the dict. 236 237 For example, if name is "email" and there is no "email" field 238 in dict, then "email" is returned. If there is already an "email" 239 field then "email2" is returned, etc""" 240 if name not in dict: 241 return name 242 for i in xrange(2,99999): 243 if name+`i` not in dict: 244 return name+`i`
245
246 - def _parse(self, lines, result):
247 for field,value in lines: 248 if len(value.strip())==0: # ignore blank values 249 continue 250 if '.' in field[0]: 251 f=field[0][field[0].find('.')+1:] 252 else: f=field[0] 253 t=f.replace("-", "_") 254 func=getattr(self, "_field_"+t, self._default_field) 255 func(field, value, result)
256
257 - def _update_groups(self, result):
258 """Update the groups info """ 259 for k,e in self._groups.items(): 260 self._setvalue(result, *e)
261 262 # fields we ignore 263
264 - def _field_ignore(self, field, value, result):
265 pass
266 267 _field_LABEL=_field_ignore # we use the ADR field instead 268 _field_BDAY=_field_ignore # not stored in bitpim 269 _field_ROLE=_field_ignore # not stored in bitpim 270 _field_CALURI=_field_ignore # not stored in bitpim 271 _field_CALADRURI=_field_ignore # variant of above 272 _field_FBURL=_field_ignore # not stored in bitpim 273 _field_REV=_field_ignore # not stored in bitpim 274 _field_KEY=_field_ignore # not stored in bitpim 275 _field_SOURCE=_field_ignore # not stored in bitpim (although arguably part of serials) 276 _field_PHOTO=_field_ignore # contained either binary image, or external URL, not used by BitPim 277 278 # simple fields 279
280 - def _field_FN(self, field, value, result):
281 result[self._getfieldname("name", result)]=self.unquote(value)
282
283 - def _field_TITLE(self, field, value, result):
284 result[self._getfieldname("title", result)]=self.unquote(value)
285
286 - def _field_NICKNAME(self, field, value, result):
287 # ::TODO:: technically this is a comma seperated list .. 288 result[self._getfieldname("nickname", result)]=self.unquote(value)
289
290 - def _field_NOTE(self, field, value, result):
291 result[self._getfieldname("notes", result)]=self.unquote(value)
292
293 - def _field_UID(self, field, value, result):
294 result["uid"]=self.unquote(value) # note that we only store one UID (the "U" does stand for unique)
295 296 # 297 # Complex fields 298 # 299
300 - def _field_N(self, field, value, result):
301 value=self.splitandunquote(value) 302 familyname=givenname=additionalnames=honorificprefixes=honorificsuffixes=None 303 try: 304 familyname=value[0] 305 givenname=value[1] 306 additionalnames=value[2] 307 honorificprefixes=value[3] 308 honorificsuffixes=value[4] 309 except IndexError: 310 pass 311 if familyname is not None and len(familyname): 312 result[self._getfieldname("last name", result)]=familyname 313 if givenname is not None and len(givenname): 314 result[self._getfieldname("first name", result)]=givenname 315 if additionalnames is not None and len(additionalnames): 316 result[self._getfieldname("middle name", result)]=additionalnames 317 if honorificprefixes is not None and len(honorificprefixes): 318 result[self._getfieldname("prefix", result)]=honorificprefixes 319 if honorificsuffixes is not None and len(honorificsuffixes): 320 result[self._getfieldname("suffix", result)]=honorificsuffixes
321 322 _field_NAME=_field_N # early versions of vcard did this 323
324 - def _field_ORG(self, field, value, result):
325 value=self.splitandunquote(value) 326 if len(value): 327 result[self._getfieldname("organisation", result)]=value[0] 328 for f in value[1:]: 329 result[self._getfieldname("organisational unit", result)]=f
330 331 _field_O=_field_ORG # early versions of vcard did this 332
333 - def _field_EMAIL(self, field, value, result):
334 value=self.unquote(value) 335 # work out the types 336 types=[] 337 for f in field[1:]: 338 if f.startswith("TYPE="): 339 ff=f[len("TYPE="):].split(",") 340 else: ff=[f] 341 types.extend(ff) 342 343 # the standard doesn't specify types of "home" and "work" but 344 # does allow for random user defined types, so we look for them 345 type=None 346 for t in types: 347 if t=="HOME": type="home" 348 if t=="WORK": type="business" 349 if t=="X400": return # we don't want no steenking X.400 350 351 preferred="PREF" in types 352 353 if type is None: 354 self._setvalue(result, "email", value, preferred) 355 else: 356 addr={'email': value, 'type': type} 357 self._setvalue(result, "email", addr, preferred)
358
359 - def _field_URL(self, field, value, result):
360 # the standard doesn't specify url types or a pref type, 361 # but we implement it anyway 362 value=self.unquote(value) 363 # work out the types 364 types=[] 365 for f in field[1:]: 366 if f.startswith("TYPE="): 367 ff=f[len("TYPE="):].split(",") 368 else: ff=[f] 369 types.extend(ff) 370 371 type=None 372 for t in types: 373 if t=="HOME": type="home" 374 if t=="WORK": type="business" 375 376 preferred="PREF" in types 377 378 if type is None: 379 self._setvalue(result, "url", value, preferred) 380 else: 381 addr={'url': value, 'type': type} 382 self._setvalue(result, "url", addr, preferred)
383
384 - def _field_X_SPEEDDIAL(self, field, value, result):
385 if '.' in field[0]: 386 group=field[0][:field[0].find('.')] 387 else: 388 group=None 389 if group is None: 390 # this has to belong to a group!! 391 print 'speedial has no group' 392 else: 393 self._setgroupvalue(result, 'phone', { 'speeddial': int(value) }, 394 group, False)
395
396 - def _field_TEL(self, field, value, result):
397 value=self.unquote(value) 398 # see if this is part of a group 399 if '.' in field[0]: 400 group=field[0][:field[0].find('.')] 401 else: 402 group=None 403 404 # work out the types 405 types=[] 406 for f in field[1:]: 407 if f.startswith("TYPE="): 408 ff=f[len("TYPE="):].split(",") 409 else: ff=[f] 410 types.extend(ff) 411 412 # type munging - we map vcard types to simpler ones 413 munge={ "BBS": "DATA", "MODEM": "DATA", "ISDN": "DATA", "CAR": "CELL", "PCS": "CELL" } 414 types=[munge.get(t, t) for t in types] 415 416 # reduce types to home, work, msg, pref, voice, fax, cell, video, pager, data 417 types=[t for t in types if t in ("HOME", "WORK", "MSG", "PREF", "VOICE", "FAX", "CELL", "VIDEO", "PAGER", "DATA")] 418 419 # if type is in this list and voice not explicitly mentioned then it is not a voice type 420 antivoice=["FAX", "PAGER", "DATA"] 421 if "VOICE" in types: 422 voice=True 423 else: 424 voice=True # default is voice 425 for f in antivoice: 426 if f in types: 427 voice=False 428 break 429 430 preferred="PREF" in types 431 432 # vcard allows numbers to be multiple things at the same time, such as home voice, home fax 433 # and work fax so we have to test for all variations 434 435 # if neither work or home is specified, then no default (otherwise things get really complicated) 436 iswork=False 437 ishome=False 438 if "WORK" in types: iswork=True 439 if "HOME" in types: ishome=True 440 441 if len(types)==0 or types==["PREF"]: iswork=True # special case when nothing else is specified 442 443 444 value=phonenumber.normalise(value) 445 if iswork and voice: 446 self._setgroupvalue(result, 447 "phone", {"type": "business", "number": value}, 448 group, preferred) 449 if ishome and voice: 450 self._setgroupvalue(result, 451 "phone", {"type": "home", "number": value}, 452 group, preferred) 453 if not iswork and not ishome and "FAX" in types: 454 # fax without explicit work or home 455 self._setgroupvalue(result, 456 "phone", {"type": "fax", "number": value}, 457 group, preferred) 458 else: 459 if iswork and "FAX" in types: 460 self._setgroupvalue(result, "phone", 461 {"type": "business fax", "number": value}, 462 group, preferred) 463 if ishome and "FAX" in types: 464 self._setgroupvalue(result, "phone", 465 {"type": "home fax", "number": value}, 466 group, preferred) 467 if "CELL" in types: 468 self._setgroupvalue(result, 469 "phone", {"type": "cell", "number": value}, 470 group, preferred) 471 if "PAGER" in types: 472 self._setgroupvalue(result, 473 "phone", {"type": "pager", "number": value}, 474 group, preferred) 475 if "DATA" in types: 476 self._setgroupvalue(result, 477 "phone", {"type": "data", "number": value}, 478 group, preferred)
479
480 - def _setgroupvalue(self, result, type, value, group, preferred=False):
481 """ Set value of an item of a group 482 """ 483 if group is None: 484 # no groups specified 485 return self._setvalue(result, type, value, preferred) 486 group_type=self._groups.get(group, None) 487 if group_type is None: 488 # 1st one of the group 489 self._groups[group]=[type, value, preferred] 490 else: 491 if type!=group_type[0]: 492 print 'Group',group,'has different types:',type,groups_type[0] 493 if preferred: 494 group_type[2]=True 495 group_type[1].update(value)
496
497 - def _setvalue(self, result, type, value, preferred=False):
498 if type not in result: 499 result[type]=value 500 return 501 if not preferred: 502 result[self._getfieldname(type, result)]=value 503 return 504 # we need to insert our value at the begining 505 values=[value] 506 for suffix in [""]+range(2,99): 507 if type+str(suffix) in result: 508 values.append(result[type+str(suffix)]) 509 else: 510 break 511 suffixes=[""]+range(2,len(values)+1) 512 for l in range(len(suffixes)): 513 result[type+str(suffixes[l])]=values[l]
514
515 - def _field_CATEGORIES(self, field, value, result):
516 # comma seperated just for fun 517 values=self.splitandunquote(value, seperator=",") 518 values=[v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field 519 values=[v for v in values if len(v)] 520 v=result.get('categories', None) 521 if v: 522 result['categories']=';'.join([v, ";".join(values)]) 523 else: 524 result['categories']=';'.join(values)
525
526 - def _field_SOUND(self, field, value, result):
527 # comma seperated just for fun 528 values=self.splitandunquote(value, seperator=",") 529 values=[v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field 530 values=[v for v in values if len(v)] 531 result[self._getfieldname("ringtones", result)]=";".join(values)
532 533 _field_CATEGORY=_field_CATEGORIES # apple use "category" which is not in the spec 534
535 - def _field_ADR(self, field, value, result):
536 # work out the type 537 preferred=False 538 type="business" 539 for f in field[1:]: 540 if f.startswith("TYPE="): 541 ff=f[len("TYPE="):].split(",") 542 else: ff=[f] 543 for x in ff: 544 if x=="HOME": 545 type="home" 546 if x=="PREF": 547 preferred=True 548 549 value=self.splitandunquote(value) 550 pobox=extendedaddress=streetaddress=locality=region=postalcode=country=None 551 try: 552 pobox=value[0] 553 extendedaddress=value[1] 554 streetaddress=value[2] 555 locality=value[3] 556 region=value[4] 557 postalcode=value[5] 558 country=value[6] 559 except IndexError: 560 pass 561 addr={} 562 if pobox is not None and len(pobox): 563 addr["pobox"]=pobox 564 if extendedaddress is not None and len(extendedaddress): 565 addr["street2"]=extendedaddress 566 if streetaddress is not None and len(streetaddress): 567 addr["street"]=streetaddress 568 if locality is not None and len(locality): 569 addr["city"]=locality 570 if region is not None and len(region): 571 addr["state"]=region 572 if postalcode is not None and len(postalcode): 573 addr["postalcode"]=postalcode 574 if country is not None and len(country): 575 addr["country"]=country 576 if len(addr): 577 addr["type"]=type 578 self._setvalue(result, "address", addr, preferred)
579
580 - def _field_X_PALM(self, field, value, result):
581 # handle a few PALM custom fields 582 ff=field[0].split(".") 583 f0=ff[0] 584 f1=ff[1] if len(ff)>1 else '' 585 if f0.startswith('X-PALM-CATEGORY') or f1.startswith('X-PALM-CATEGORY'): 586 self._field_CATEGORIES(['CATEGORIES'], value, result) 587 elif f0=='X-PALM-NICKNAME' or f1=='X-PALM-NICKNAME': 588 self._field_NICKNAME(['NICKNAME'], value, result) 589 else: 590 if __debug__: 591 print 'ignoring PALM custom field',field
592
593 - def _default_field(self, field, value, result):
594 ff=field[0].split(".") 595 f0=ff[0] 596 f1=ff[1] if len(ff)>1 else '' 597 if f0.startswith('X-PALM-') or f1.startswith('X-PALM-'): 598 self._field_X_PALM(field, value, result) 599 return 600 elif f0.startswith("X-") or f1.startswith("X-"): 601 if __debug__: 602 print "ignoring custom field",field 603 return 604 if __debug__: 605 print "no idea what do with" 606 print "field",field 607 print "value",value[:80]
608
609 - def unquote(self, value):
610 # ::TODO:: do this properly (deal with all backslashes) 611 return value.replace(r"\;", ";") \ 612 .replace(r"\,", ",") \ 613 .replace(r"\n", "\n") \ 614 .replace(r"\r\n", "\r\n") \ 615 .replace("\r\n", "\n") \ 616 .replace("\r", "\n")
617
618 - def splitandunquote(self, value, seperator=";"):
619 # also need a splitandsplitandunquote since some ; delimited fields are then comma delimited 620 621 # short cut for normal case - no quoted seperators 622 if value.find("\\"+seperator)<0: 623 return [self.unquote(v) for v in value.split(seperator)] 624 625 # funky quoting, do it the slow hard way 626 res=[] 627 build="" 628 v=0 629 while v<len(value): 630 if value[v]==seperator: 631 res.append(build) 632 build="" 633 v+=1 634 continue 635 636 637 if value[v]=="\\": 638 build+=value[v:v+2] 639 v+=2 640 continue 641 build+=value[v] 642 v+=1 643 if len(build): 644 res.append(build) 645 646 return [self.unquote(v) for v in res]
647
648 - def version(self):
649 "Best guess as to vcard version" 650 return self._version
651
652 - def origin(self):
653 "Best guess as to what program wrote the vcard" 654 return self._origin
655
656 - def __repr__(self):
657 str="Version: %s\n" % (`self.version()`) 658 str+="Origin: %s\n" % (`self.origin()`) 659 str+=common.prettyprintdict(self._data) 660 # str+=`self.lines` 661 return str+"\n"
662 663 ### 664 ### Outputting functions 665 ### 666 667 # The formatters return a string 668
669 -def myqpencodestring(value):
670 """My own routine to do qouted printable since the builtin one doesn't encode CR or NL!""" 671 return quopri.encodestring(value).replace("\r", "=0D").replace("\n", "=0A")
672
673 -def format_stringv2(value):
674 """Return a vCard v2 string. Any embedded commas or semi-colons are removed.""" 675 return value.replace("\\", "").replace(",", "").replace(";", "")
676
677 -def format_stringv3(value):
678 """Return a vCard v3 string. Embedded commas and semi-colons are backslash quoted""" 679 return value.replace("\\", "").replace(",", r"\,").replace(";", r"\;")
680 681 _string_formatters=(format_stringv2, format_stringv3) 682
683 -def format_binary(value):
684 """Return base 64 encoded string""" 685 # encodestring always adds a newline so we have to strip it off 686 return base64.encodestring(value).rstrip()
687
688 -def _is_sequence(v):
689 """Determine if v is a sequence such as passed to value in out_line. 690 Note that a sequence of chars is not a sequence for our purposes.""" 691 return isinstance(v, (type( () ), type([])))
692
693 -def out_line(name, attributes, value, formatter, join_char=";"):
694 """Returns a single field correctly formatted and encoded (including trailing newline) 695 696 @param name: The field name 697 @param attributes: A list of string attributes (eg "TYPE=intl,post" ). Usually 698 empty except for TEL and ADR. You can also pass in None. 699 @param value: The field value. You can also pass in a list of components which will be 700 joined with join_char such as the 6 components of N 701 @param formatter: The function that formats the value/components. See the 702 various format_ functions. They will automatically ensure that 703 ENCODING=foo attributes are added if appropriate""" 704 705 if attributes is None: attributes=[] # ensure it is a list 706 else: attributes=list(attributes[:]) # ensure we work with a copy 707 708 if formatter in _string_formatters: 709 if _is_sequence(value): 710 qp=False 711 for f in value: 712 f=formatter(f) 713 if myqpencodestring(f)!=f: 714 qp=True 715 break 716 if qp: 717 attributes.append("ENCODING=QUOTED-PRINTABLE") 718 value=[myqpencodestring(f) for f in value] 719 720 value=join_char.join(value) 721 else: 722 value=formatter(value) 723 # do the qp test 724 qp= myqpencodestring(value)!=value 725 if qp: 726 value=myqpencodestring(value) 727 attributes.append("ENCODING=QUOTED-PRINTABLE") 728 else: 729 assert not _is_sequence(value) 730 if formatter is not None: 731 value=formatter(value) # ::TODO:: deal with binary and other formatters and their encoding types 732 733 res=";".join([name]+attributes)+":" 734 res+=_line_reformat(value, 70, 70-len(res)) 735 assert res[-1]!="\n" 736 737 return res+"\n"
738
739 -def _line_reformat(line, width=70, firstlinewidth=0):
740 """Takes line string and inserts newlines 741 and spaces on following continuation lines 742 so it all fits in width characters 743 744 @param width: how many characters to fit it in 745 @param firstlinewidth: if >0 then first line is this width. 746 if equal to zero then first line is same width as rest. 747 if <0 then first line will go immediately to continuation. 748 """ 749 if firstlinewidth==0: firstlinewidth=width 750 if len(line)<firstlinewidth: 751 return line 752 res="" 753 if firstlinewidth>0: 754 res+=line[:firstlinewidth] 755 line=line[firstlinewidth:] 756 while len(line): 757 res+="\n "+line[:width] 758 if len(line)<width: break 759 line=line[width:] 760 return res
761
762 -def out_names(vals, formatter, limit=1):
763 res="" 764 for v in vals[:limit]: 765 # full name 766 res+=out_line("FN", None, nameparser.formatsimplename(v), formatter) 767 # name parts 768 f,m,l=nameparser.getparts(v) 769 res+=out_line("N", None, (l,f,m,"",""), formatter) 770 # nickname 771 nn=v.get("nickname", "") 772 if len(nn): 773 res+=out_line("NICKNAME", None, nn, formatter) 774 return res
775 776 # Apple uses wrong field name so we do some futzing ...
777 -def out_categories(vals, formatter, field="CATEGORIES"):
778 cats=[v.get("category") for v in vals] 779 if len(cats): 780 return out_line(field, None, cats, formatter, join_char=",") 781 return ""
782
783 -def out_categories_apple(vals, formatter):
784 return out_categories(vals, formatter, field="CATEGORY")
785 786 # Used for both email and urls. we don't put any limits on how many are output
787 -def out_eu(vals, formatter, field, bpkey):
788 res="" 789 first=True 790 for v in vals: 791 val=v.get(bpkey) 792 type=v.get("type", "") 793 if len(type): 794 if type=="business": type="work" # vcard uses different name 795 type=type.upper() 796 if first: 797 type=type+",PREF" 798 elif first: 799 type="PREF" 800 if len(type): 801 type=["TYPE="+type+["",",INTERNET"][field=="EMAIL"]] # email also has "INTERNET" 802 else: 803 type=None 804 res+=out_line(field, type, val, formatter) 805 first=False 806 return res
807
808 -def out_emails(vals, formatter):
809 return out_eu(vals, formatter, "EMAIL", "email")
810
811 -def out_urls(vals, formatter):
812 return out_eu(vals, formatter, "URL", "url")
813 814 # fun fun fun 815 _out_tel_mapping={ 'home': 'HOME', 816 'office': 'WORK', 817 'cell': 'CELL', 818 'fax': 'FAX', 819 'pager': 'PAGER', 820 'data': 'MODEM', 821 'none': 'VOICE' 822 }
823 -def out_tel(vals, formatter):
824 # ::TODO:: limit to one type of each number 825 phones=['phone'+str(x) for x in ['']+range(2,len(vals)+1)] 826 res="" 827 first=True 828 idx=0 829 for v in vals: 830 sp=v.get('speeddial', None) 831 if sp is None: 832 # no speed dial 833 res+=out_line("TEL", 834 ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])], 835 phonenumber.format(v['number']), formatter) 836 else: 837 res+=out_line(phones[idx]+".TEL", 838 ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])], 839 phonenumber.format(v['number']), formatter) 840 res+=out_line(phones[idx]+".X-SPEEDDIAL", None, str(sp), formatter) 841 idx+=1 842 first=False 843 return res
844 845 # and addresses
846 -def out_adr(vals, formatter):
847 # ::TODO:: limit to one type of each address, and only one org 848 res="" 849 first=True 850 for v in vals: 851 o=v.get("company", "") 852 if len(o): 853 res+=out_line("ORG", None, o, formatter) 854 if v.get("type")=="home": type="HOME" 855 else: type="WORK" 856 type="TYPE="+type+("", ",PREF")[first] 857 res+=out_line("ADR", [type], [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")], formatter) 858 first=False 859 return res
860
861 -def out_note(vals, formatter, limit=1):
862 return "".join([out_line("NOTE", None, v["memo"], formatter) for v in vals[:limit]])
863 864 # Sany SCP-6600 (Katana) support
865 -def out_tel_scp6600(vals, formatter):
866 res="" 867 _pref=len(vals)>1 868 for v in vals: 869 res+=out_line("TEL", 870 ["TYPE=%s%s" % ("PREF," if _pref else "", 871 _out_tel_mapping[v['type']])], 872 phonenumber.format(v['number']), formatter) 873 _pref=False 874 return res
875 -def out_email_scp6600(vals, formatter):
876 res='' 877 for _idx in range(min(len(vals), 2)): 878 v=vals[_idx] 879 if v.get('email', None): 880 res+=out_line('EMAIL', ['TYPE=INTERNET'], 881 v['email'], formatter) 882 return res
883 -def out_url_scp660(vals, formatter):
884 if vals and vals[0].get('url', None): 885 return out_line('URL', None, vals[0]['url'], formatter) 886 return ''
887 -def out_adr_scp6600(vals, formatter):
888 for v in vals: 889 if v.get('type', None)=='home': 890 _type='HOME' 891 else: 892 _type='WORK' 893 return out_line("ADR", ['TYPE=%s'%_type], 894 [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")], 895 formatter) 896 return ''
897 898 # This is the order we write things out to the vcard. Although 899 # vCard doesn't require an ordering, it looks nicer if it 900 # is (eg name first) 901 _field_order=("names", "wallpapers", "addresses", "numbers", "categories", "emails", "urls", "ringtones", "flags", "memos", "serials") 902
903 -def output_entry(entry, profile, limit_fields=None):
904 905 # debug build assertion that limit_fields only contains fields we know about 906 if __debug__ and limit_fields is not None: 907 assert len([f for f in limit_fields if f not in _field_order])==0 908 909 fmt=profile["_formatter"] 910 io=cStringIO.StringIO() 911 io.write(out_line("BEGIN", None, "VCARD", None)) 912 io.write(out_line("VERSION", None, profile["_version"], None)) 913 914 if limit_fields is None: 915 fields=_field_order 916 else: 917 fields=[f for f in _field_order if f in limit_fields] 918 919 for f in fields: 920 if f in entry and f in profile: 921 func=profile[f] 922 # does it have a limit? (nice scary introspection :-) 923 if "limit" in func.func_code.co_varnames[:func.func_code.co_argcount]: 924 lines=func(entry[f], fmt, limit=profile["_limit"]) 925 else: 926 lines=func(entry[f], fmt) 927 if len(lines): 928 io.write(lines) 929 930 io.write(out_line("END", None, "VCARD", fmt)) 931 return io.getvalue()
932 933 profile_vcard2={ 934 '_formatter': format_stringv2, 935 '_limit': 1, 936 '_version': "2.1", 937 'names': out_names, 938 'categories': out_categories, 939 'emails': out_emails, 940 'urls': out_urls, 941 'numbers': out_tel, 942 'addresses': out_adr, 943 'memos': out_note, 944 } 945 946 profile_vcard3=profile_vcard2.copy() 947 profile_vcard3['_formatter']=format_stringv3 948 profile_vcard3['_version']="3.0" 949 950 profile_apple=profile_vcard3.copy() 951 profile_apple['categories']=out_categories_apple 952 953 profile_full=profile_vcard3.copy() 954 profile_full['_limit']=99999 955 956 profile_scp6600=profile_full.copy() 957 del profile_scp6600['categories'] 958 profile_scp6600.update( 959 { 'numbers': out_tel_scp6600, 960 'emails': out_email_scp6600, 961 'urls': out_url_scp660, 962 'addresses': out_adr_scp6600, 963 }) 964 965 profiles={ 966 'vcard2': { 'description': "vCard v2.1", 'profile': profile_vcard2 }, 967 'vcard3': { 'description': "vCard v3.0", 'profile': profile_vcard3 }, 968 'apple': { 'description': "Apple", 'profile': profile_apple }, 969 'fullv3': { 'description': "Full vCard v3.0", 'profile': profile_full}, 970 'scp6600': { 'description': "Sanyo SCP-6600 (Katana)", 971 'profile': profile_scp6600 }, 972 } 973 974 975 if __name__=='__main__': 976
977 - def _wrap(func):
978 try: return func() 979 except: 980 print common.formatexception() 981 sys.exit(1)
982
983 - def dump_vcards():
984 for vcard in VCards(VFile(common.opentextfile(sys.argv[1]))): 985 # pass 986 print vcard
987
988 - def turn_around():
989 p="fullv3" 990 if len(sys.argv)==4: p=sys.argv[4] 991 print "Using profile", profiles[p]['description'] 992 profile=profiles[p]['profile'] 993 994 d={'result': {}} 995 try: 996 execfile(sys.argv[1], d,d) 997 except UnicodeError: 998 common.unicode_execfile(sys.argv[1], d,d) 999 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