mirror of
https://github.com/ssb22/wm6-utils.git
synced 2022-12-13 08:35:29 +00:00
140 lines
6.5 KiB
Python
140 lines
6.5 KiB
Python
#!/usr/bin/env python2
|
|
|
|
# PIMBackup CSC to VCF VCards conversion
|
|
# (for copying contacts from Windows Mobile to S60, Android etc)
|
|
# Silas S. Brown 2014, 2016 - v1.3 - public domain - no warranty
|
|
|
|
# Android (at least v4.4) built-in Contacts import: if
|
|
# non-identical contacts already exist, importing an
|
|
# update is likely to create new ones and Link them.
|
|
# Not sure there's any way to say "delete all old versions of all these" apart from delete all & start again (which would clear non-phone contacts)
|
|
|
|
# Where to find history:
|
|
# on GitHub at https://github.com/ssb22/wm6-utils
|
|
# and on GitLab at https://gitlab.com/ssb22/wm6-utils
|
|
# and on BitBucket https://bitbucket.org/ssb22/wm6-utils
|
|
# and at https://gitlab.developers.cam.ac.uk/ssb22/wm6-utils
|
|
# and in China: https://gitee.com/ssb22/wm6-utils
|
|
|
|
infile = "contacts.csc"
|
|
outfile = "contacts.vcf"
|
|
|
|
import csv, base64, sys, time, re
|
|
o = open(outfile,'wb')
|
|
class Dialect(csv.Dialect):
|
|
delimiter=';'
|
|
# the rest is not really needed, but some versions of
|
|
# csv won't run without them:
|
|
quoting = csv.QUOTE_MINIMAL
|
|
quotechar = '"'
|
|
lineterminator = '\n'
|
|
csv.register_dialect('csc',Dialect)
|
|
|
|
cscMap = { "Name":"FN", # = "File as" in WM6 Contacts
|
|
"Title":('N',4,3),
|
|
"First Name":('N',4,1),
|
|
"Middle Name":('N',4,2),
|
|
"Last Name":('N',4,0),
|
|
# NickName
|
|
# Suffix
|
|
"Display Name":None, # = "Name" (NOT "File as"!) in WM6 Contacts
|
|
"Picture":"PHOTO;JPEG;ENCODING=BASE64",
|
|
"Job Title":"TITLE",
|
|
"Department":("ORG",2,1),
|
|
"Company":("ORG",2,0),
|
|
"Business Phone":"TEL;TYPE=work",
|
|
# Business Fax
|
|
"Business Street":("ADR;TYPE=work",7,2),
|
|
"Business City":("ADR;TYPE=work",7,3),
|
|
"Business State":("ADR;TYPE=work",7,4),
|
|
"Business Postal Code":("ADR;TYPE=work",7,5),
|
|
"Business Country":("ADR;TYPE=work",7,6),
|
|
"IM":"IMPP","IM2":"IMPP","IM3":"IMPP", # ? (requires VCard 4.0; propsed in 3.0)
|
|
"E-mail Address":"EMAIL",
|
|
"Mobile Phone":"TEL;TYPE=cell",
|
|
# Ring Tone (usually a Windows pathname)
|
|
"Web Page":"URL",
|
|
"Office Location":"ADR;TYPE=work", # ?
|
|
"Home Phone":"TEL;TYPE=home",
|
|
"Home Street":("ADR;TYPE=home",7,2),
|
|
"Home City":("ADR;TYPE=home",7,3),
|
|
"Home State":("ADR;TYPE=home",7,4),
|
|
"Home Postal Code":("ADR;TYPE=home",7,5),
|
|
"Home Country":("ADR;TYPE=home",7,6),
|
|
"Categories":"CATEGORIES", # doesn't seem to have any effect on Android 4.4 (isn't translated to Groups)
|
|
"Other Street":("ADR;TYPE=other",7,2),
|
|
"Other City":("ADR;TYPE=other",7,3),
|
|
"Other State":("ADR;TYPE=other",7,4),
|
|
"Other Postal Code":("ADR;TYPE=other",7,5),
|
|
"Other Country":("ADR;TYPE=other",7,6),
|
|
"Pager":"TEL;TYPE=cell", # usually used as an 'overspill' if person has too many numbers
|
|
"Car Phone":"TEL;TYPE=cell", # ditto
|
|
# Home Fax
|
|
"Company Main Phone":"TEL;TYPE=work",
|
|
"Business Phone 2":"TEL;TYPE=work",
|
|
"Home Phone 2":"TEL;TYPE=home",
|
|
"Radio Phone":"TEL;TYPE=cell", # ditto
|
|
# IM2, IM3 - handled above
|
|
"E-mail 2 Address":"EMAIL",
|
|
"E-mail 3 Address":"EMAIL",
|
|
"Assistant's Name":"X-ASSISTANT",
|
|
"Assistant's Phone":"X-ASSISTANT-TEL",
|
|
# "Manager's Name":"X-MANAGER",
|
|
"Manager's Name":"Note:Manager",
|
|
# Government ID Number
|
|
# Account
|
|
# Customer ID Number
|
|
#"Spouse":"X-SPOUSE", # won't display in Android 4.1
|
|
"Spouse":"NOTE:Spouse",
|
|
"Children":"NOTE:Children",
|
|
"Anniversary":"ANNIVERSARY", # requires VCard 4.0 (and doesn't seem to have any effect in Android 4.4), TODO: put in a NOTE instead?
|
|
"Birthday":"BDAY", # in case date of birth needed for medical purposes?
|
|
"Notes":"NOTE" }
|
|
|
|
lines = open(infile).read().decode('utf-16').encode('utf-8').replace('\r','').split('\n')
|
|
fields = lines[0].split(';')
|
|
del lines[0]
|
|
for l in csv.reader(lines,'csc'):
|
|
if not l: continue
|
|
assert len(l) == len(fields), len(l)
|
|
o.write("BEGIN:VCARD\r\nVERSION:2.1\r\n")
|
|
warning_id = ""
|
|
extra = {}
|
|
for f,v in zip(fields,l):
|
|
if not v: continue
|
|
try: vd = v.replace("0xfe,0xff,","").replace(",0x00","").replace("0x","\\x").replace(",","").decode('string-escape')
|
|
except: vd = None
|
|
if vd=="<HTCData><!-- Please do not modify -->\r\n<Facebook></Facebook>\r\n</HTCData>\r\n": continue
|
|
if f=="Name": warning_id = " in "+v
|
|
# if f=="Name": sys.stderr.write(v+"\n") # for Info: below
|
|
mapTo = cscMap.get(f,'')
|
|
if mapTo==None: continue
|
|
elif mapTo=='':
|
|
if len(v)<30: f += ': '+v # not e.g. Notes Ink
|
|
sys.stderr.write("Warning: omitting "+f+warning_id+'\n')
|
|
continue
|
|
elif type(mapTo)==tuple:
|
|
if not mapTo[0] in extra:
|
|
extra[mapTo[0]] = ['']*mapTo[1]
|
|
extra[mapTo[0]][mapTo[2]] = v
|
|
continue
|
|
if f=="Picture":
|
|
v=('\\'+v[1:].replace(',0','\\')).decode('string_escape')
|
|
# from PIL import Image; from cStringIO import StringIO
|
|
# sys.stderr.write("Info: picture size is %dx%d\r\n" % Image.open(StringIO(v)).size)
|
|
v=base64.b64encode(v)+'\r\n' # must leave an extra blank line for Android, otherwise get "File ended during parsing BASE64 binary" - see java/com/android/vcard/VCardParserImpl_V21.java
|
|
else:
|
|
if re.match("^(0x[0-9a-f][0-9a-f],?)*$",v):
|
|
v = v.replace(",","").replace("0x",r"\x").decode("string-escape").decode("utf-16").encode("utf-8")
|
|
if "\n" in v or "\r" in v:
|
|
v = v.replace("=","=3D").replace("\r","=0D").replace("\n","=0A")
|
|
if ':' in mapTo: mapTo=mapTo[:mapTo.index(':')]+";ENCODING=QUOTED-PRINTABLE"+mapTo[mapTo.index(':'):]
|
|
else: mapTo += ";ENCODING=QUOTED-PRINTABLE"
|
|
o.write(mapTo+':'+v+'\r\n')
|
|
for k,v in extra.items():
|
|
if ';' in ''.join(v):
|
|
sys.stderr.write("Warning: omitting "+k+" because don't know how to deal with the extra semicolons in it\n")
|
|
else: o.write(k+':'+';'.join(v)+'\r\n')
|
|
o.write("NOTE:Contact imported %d-%02d-%02d\r\n" % time.localtime()[:3]) # TODO: if there's already a NOTE, shouldn't we append this to the existing one instead of adding a new one?
|
|
o.write("END:VCARD\r\n")
|