PyXR

c:\projects\bitpim\src \ vcard.py



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