#!/usr/bin/python -u """ evolution --> thunderbird addressbook migration script Known to work with Ximian Evolution 1.2 and Mozilla Thunderbird 0.6. Or right about 2004-06-05. Both products are mail clients for Linux and other platforms. Both rock. This utility was written because Ximian Evolution (1.2) does not export its address book very well to other programs in general, and Mozilla Thunderbird (0.6) in particular. Both programs are at fault I think. I googled for a solution and got a half-baked python & perl script combo that did some of the job. Since I was not satisfied with that, I decided to spend a bit of time and write a more robust utility myself. I.e., it was one of those one-off programs that ended up expanding and taking way too much of my time! USAGE: ./evol2tbird-addressbook.py --db [path to evolution's addressbook.db] or ./evol2tbird-addressbook.py --vcard [path to evolution's exported vcards] (if no options are used the default is --db ~/evolution/local/Contacts/addressbook.db) How it works: o I iterate over the addressbook records in Evolution's bsddb Contacts database. -or- I iterate over the VCard records in a text file that Evolution exported. o as I pull in data, I translate that data into strings that are comma deliminated - Comma Seperated Values, or CSV. o this cvs output is *exactly* formated to what the Thunderbird mail client expects. What would be better: o Evolution needs to be able to convert the data to a more robust portable (and *sigh* more complex) format such as LDAP Data Interchange Format (LDIF) that anyone can parse. I am certain it is on their todo list. o Thunderbird needs to be able to import Evolution's exported VCards natively. Evolution is extremely popular. It only makes sense. o This utility would be better if it translated Evolution's VCard format to LDIF. Maybe I will do that one of these days. Don't hold your breath though. ;) License: CC-GNU LGPL v2.1 Copyright (C) 2004 Todd Warner All rights reserved. Author: Todd Warner See also: Ximian (Novell) Evolution: http://www.novell.com/products/evolution/ Mozilla Thunderbird: http://www.mozilla.org/projects/thunderbird/ CC-GNU LGPL v2.1: http://creativecommons.org/licenses/LGPL/2.1/ LDIF: http://whatis.techtarget.com/definition/0,,sid9_gci549219,00.html VCard: http://www.free-definition.com/VCard.html Update (2005-05-11): Arsen Kostenko pointed out a flaw s/EMAIL;INTERNET/EMAIL;TYPE=INTERNET It may be an Evolution v2.* thing. I don't know. Hope it works fine. :) """ #------------------------------------------------------------------------------ # $Id: evol2tbird-addressbook.py,v 1.6 2005/05/12 02:50:18 taw Exp $ import os import re import sys import bsddb import string def cleanupAbsPath(path): """ take ~taw/../some/path/$MOUNT_POINT/blah and make it sensible. Path return is absolute. NOTE: python 2.2 fixes a number of bugs with this and eliminates the need for os.path.expanduser """ if path is None: return None return os.path.abspath( os.path.expanduser( os.path.expandvars(path))) DB_PATH = "~/evolution/local/Contacts/addressbook.db" class DB: def __init__(self, dbpath): self.dbpath = dbpath self.open() self._bsddb = bsddb # a hack for python 1.5.2's gc so that # the __del__ method works w/o fail. def __del__(self): self.close() def open(self): self.db = bsddb.hashopen(self.dbpath, 'r') def close(self): try: self.db.close() except: pass def findAndStrip(s, key): s = string.strip(s) #print 'findAndStrip', string.find(s, key), s, key if string.find(s, key) == 0: return string.replace(s, key, '', 1) raise KeyError("not found") # Format of a single csv entry: # "first,last,full,nick,email1,email2,workph,homeph,fax,pager,\ # cell,hadd1,hadd2,hcity,hstate,hzip,hcountry,wadd1,wadd2,wcity,\ # wstate,wzip,wcountry,title,org-unit,org,wurl,hurl,birthY,birthM,\ # birthD,custom1,custom2,custom3,custom4,notes1," # # Note the 3 ?'s that I don't know what are for. # I believe there needs to be at least 36 items in that list (36 commas) # Possible keys in the vcard (order is important!) _EVOL_KEYS = ['N:', 'FN:', 'NICKNAME:', 'EMAIL;TYPE=INTERNET:', 'TEL;WORK;VOICE:', 'TEL;HOME:', 'TEL;WORK;FAX:', 'TEL;PAGER:', 'TEL;CELL:', 'LABEL;QUOTED-PRINTABLE;HOME:', 'LABEL;QUOTED-PRINTABLE;WORK;PREF:', 'TITLE:', 'ORG:', 'URL:', # The next three are not exported by evolution # but are used by the imported and must be in here. 'birth-year', 'birth-month', 'birth-day', 'NOTE;QUOTED-PRINTABLE:'] def balanceQuotes(s): listYN = type(s) == type([]) if not listYN: s = [s] for i in range(len(s)): s[i] = string.strip(s[i]) if s[i] and len(s[i]) > 1: if s[i][0] == '"' and s[i][-1] != '"': s[i] = s[i] + '"' elif s[i][0] != '"' and s[i][-1] == '"': s[i] = '"' + s[i] if not listYN: return s[0] return s def splitStrip(s, spl=''): s = string.split(s, spl) for i in range(len(s)): s[i] = string.strip(s[i]) return s def splitStripBalance(s, slp=''): return balanceQuotes(string.split(s, slp)) def figureFirstLast(n): """ ["warner;todd"] --> "todd,warner" """ if not n: return ',' n = splitStripBalance(n[0], ';') if not n: return ',' if len(n) == 1: return n[0]+',' return n[1]+','+n[0] def figureEmail(e): """ [email, email] --> "email,email" """ e = balanceQuotes(e) if not e: return ',' elif len(e) == 1: return e[0]+',' # return at most two email addresses return e[0] + ',' + e[1] def figureAddress(a): """ ["line1=0Aline2"] --> "line1,line2,,,," """ if not a: a = [''] a = splitStripBalance(a[0], '=0A') a = a + ['']*(6-len(a)) # 6 fields a = a[:6] a = string.join(a, ',') return a def figureURL(u): """ ["URL","URL"] --> "URL,URL,,," """ # only 2 URLs allowed u = u + ['']*(2-len(u)) u = u[:2] return string.join(balanceQuotes(u), ',') def figureOrg(o): """ ["ORG","ORG-UNIT"] --> "ORG-UNIT,ORG" """ if not o: return ',' o = string.split(o[0], ';') # vcard has them flip-flopped (org,org-unit) if len(o) > 1: return o[1] + ',' + o[0] else: return o[0] + ',' return ',' def figureNotes(n): """ "cnote1=0Acnote2=0Acnote3=0Acnote4=0Anote" --> "cnote1,cnote2,cnote3,cnote4,Anote Anote-continued" You can have 4 custom notes and 1 general note. The general note is space deliminated. """ cust = [] note = [] for each in n: each = splitStripBalance(each, '=0A') l = max(4-len(cust), 0) cust = cust + each[:l] note = note + each[l:] cust = cust + ['']*(4-len(cust)) # must have at least 4 cust = string.join(cust, ',') note = string.join(note, ' ') or '' return cust + ',' + note def figureOther(o): """ ["xxx","yyy"] --> "xxx,yyy" """ if not o: return '' o = balanceQuotes(o) return string.join(o, ',') def processVCard(vcard): d = {} mutexish = 0 for line in vcard: # begin check - are we in the vcard yet? # is this a vcard? if not mutexish: try: findAndStrip(line, 'BEGIN:VCARD') mutexish = 1 continue except KeyError: continue # end check - if you found the end, break the loop if string.find(line, 'END:VCARD') == 0: break # key processing for key in _EVOL_KEYS: if string.split(line, ':')[0]+':' not in _EVOL_KEYS: break try: x = findAndStrip(line, key) # if we find a comma, we need to quote the whole string. if string.count(x, ','): if x[0] != '"': x = '"' + x if x[-1] != '"': x = x + '"' # more than one! Append! if d.has_key(key): d[key].append(x) else: d[key] = [x] break except KeyError: pass return d def cvs(d): """ cvs-ify a parsed vcard """ s = '' for k in _EVOL_KEYS: if k == 'N:': s = s + figureFirstLast(d.get(k, [])) + ',' #elif k == 'EMAIL;INTERNET:': # s = s + figureEmail(d.get(k, [])) + ',' elif k == 'EMAIL;TYPE=INTERNET:': s = s + figureEmail(d.get(k, [])) + ',' elif k in ('LABEL;QUOTED-PRINTABLE;HOME:', 'LABEL;QUOTED-PRINTABLE;WORK;PREF:'): s = s + figureAddress(d.get(k, [])) + ',' elif k == 'URL:': s = s + figureURL(d.get(k, [])) + ',' elif k == 'birth-year': # not exported by evolution s = s + ',' elif k == 'birth-month': # not exported by evolution s = s + ',' elif k == 'birth-day': # not exported by evolution s = s + ',' elif k == 'ORG:': s = s + figureOrg(d.get(k, [])) + ',' elif k == 'NOTE;QUOTED-PRINTABLE:': s = s + figureNotes(d.get(k, [])) + ',' else: # everything else s = s + figureOther(d.get(k, [])) + ',' return s def export(db): db = db.db for k in db.keys(): print cvs(processVCard(db[k].split('\n'))) #sys.exit(999) def usage(): executable = os.path.basename(sys.argv[0]) print """\ USAGE: %s --db [path to addressbook.db] or %s --vcard [path to an evolution exported *.vcf file] NOTE: if no options are used the default option and value is: --db %s """ % (executable, executable, DB_PATH) sys.exit(-1) def processCommandLine(argv): """ chose to *not* import optik or getopt just to be simple """ if len(argv) > 3 or '-h' in argv or '--help' in argv: usage() if len(argv) == 2: usage() if len(argv) == 1: argv = argv + ['--db', DB_PATH] if argv[1] not in ['--db', '--vcard']: usage() dbpath = None vcardpath = None if argv[1] == '--db': dbpath = cleanupAbsPath(argv[2]) elif argv[1] == '--vcard': vcardpath = cleanupAbsPath(argv[2]) if not os.path.exists(dbpath or vcardpath): sys.stderr.write("ERROR: file doesn't exist: %s\n" % (dbpath or vcardpath)) sys.exit(-1) if not os.path.isfile(dbpath or vcardpath): sys.stderr.write("ERROR: not a file: %s\n" % (dbpath or vcardpath)) sys.exit(-1) return dbpath, vcardpath def dosStrip(s): if not s: return s if s[-1] == '\n': s = s[:-1] if not s: return s if s[-1] == '\r': s = s[:-1] s = string.strip(s) return s #------------------------------------------------------------------------------ def main(argv=sys.argv): dbpath, vcardpath = processCommandLine(argv) if dbpath: db = DB(dbpath) for k in db.db.keys(): print cvs(processVCard(db.db[k].split('\n'))) else: fo = open(vcardpath, 'rb') line = fo.readline() while line: vcard = [] if string.find(line, 'BEGIN:VCARD') == 0: while 1: vcard.append(dosStrip(line)) if string.find(line, 'END:VCARD') == 0: break line = fo.readline() print cvs(processVCard(vcard)) line = fo.readline() # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if __name__ == '__main__': sys.exit(main() or 0) #==============================================================================