#!/usr/bin/python3 # coding=utf-8 # Doc # Script to extract Contacts from Sailfish Contact SQLite DB located at # # # Links # Projects Page: https://wiki.siningsoft.de/doku.php?id=sailfishos:projects:sailfish_contacts_rescue # FileFormatdescription: https://docs.fileformat.com/email/vcf/#vcf-30-example # Pytho vobject: http://eventable.github.io/vobject/ # Version version=0.4 # ChangeLog # 2021-08-03 - 0.1 - multiple E-Mails with different types are working correctly # 2021-08-09 - 0.2 - Phonenumbers with parameters, Addresses with parameters, E-Mail-Addresses with marameters # 2021-08-10 - 0.3 - load Avatars into VCards # 2021-08-28 - 0.4 - bugfixing after full exporting my contacts import sqlite3 import vobject import uuid import argparse import os #class vobject2(vobject): def DEBUG(debug,msg): if debug is True: print("..DEBUG: " + msg) # will return a default value, when var is None def setDefault(var, default): if var is None: return default else: return var parser = argparse.ArgumentParser(description='Restore SailfishOS 3 Contacts', epilog='This script was written to restore SailfishOS 3 contacts as VCF files. To see additional information, visit: https://wiki.siningsoft.de/doku.php?id=sailfishos:projects:sailfish_contacts_rescue' ) parser.add_argument('--db','-d', required=True, help="Sqlite3 Database file usually /home/{nemo,defaultuser}/.local/share/system/Contacts/qtcontacts-sqlite/contacts.db") parser.add_argument('--output','-o',required=True, help="Output directory for vcf files") parser.add_argument('--avatars','-a',required=False, help='Avatar directory. If present otherwise we skip this block of avatars, means, no avatars at all') parser.add_argument('--debug',action="store_true",help="debugging output to identify problems") parser.add_argument('--version', action='version', version='%(prog)s ' + str(version)) args = parser.parse_args() SQLconn = sqlite3.connect(args.db) try: SQLContCur = SQLconn.cursor() for row in SQLContCur.execute('SELECT * FROM Contacts'): # contactID abfragen contactID=setDefault(row[0],"NA") familyN=setDefault(row[6],"NA") givenN=setDefault(row[4],"NA") fullN=setDefault(row[1],"NA") cardfile=args.output + "/" + fullN.replace(" ","_").replace("(","").replace(")","").replace(".","").replace(",","") + ".vcf" # wir erstellen das Objekt vcf = vobject.vCard() vcf.add('uid').value = str(uuid.uuid4()) #vcf.add('uid').value = "Testdaten" vcf.add('n').value = vobject.vcard.Name( family=familyN, given=givenN ) vcf.add('fn').value =fullN print("exporting " + fullN + " to file " + cardfile) DEBUG(args.debug,"Contact " + str(fullN) + " family-name=" + str(familyN) + " given-name=" + str(givenN)) # abfrage der Adressdaten SQLADRCur = SQLconn.cursor() for ADRrow in SQLADRCur.execute('SELECT * FROM Addresses JOIN Details on Details.detailId = Addresses.detailId where Addresses.contactId = ' + str(contactID)): if ADRrow[2] is not None: ADRstr=str(ADRrow[2]) else: ADRstr="NA" if ADRrow[5] is not None: ADRcit=str(ADRrow[5]) #.replace(" ","\ ").replace("\\", "\\\\").replace(";", "\;").replace(",", "\,") else: ADRcit="NA" if ADRrow[4] is not None: ADRreg=str(ADRrow[4]) else: ADRreg="NA" if ADRrow[6] is not None: ADRcod=str(ADRrow[6]) else: ADRcod="NA" if ADRrow[7] is not None: ADRcou=str(ADRrow[7]) else: ADRcou="NA" # we need to know the context we've read here if ADRrow[14] is not None: addADR="ADR;TYPE="+str(ADRrow[14]) else: addADR="ADR" vcf.add("ADR").value = vobject.vcard.Address(street=ADRstr, city=ADRcit, region=ADRreg, code=ADRcod,country=ADRcou) #adr = vcf.add(addADR).value = vobject.vcard.Address() DEBUG(args.debug,"Addressdata: street=" + ADRstr + " city=" + ADRcit + " region=" + ADRreg + " code=" + ADRcod + " country=" + ADRcou + " context=" + str(ADRrow[14])) ## Abfragen Organisation SQLORGCur = SQLconn.cursor() for ORGrow in SQLORGCur.execute('SELECT * from Organizations where contactId = ' + str(contactID)): org = vcf.add('ORG').value = [str(ORGrow[2]), str(ORGrow[6])] if ORGrow[4] is not None: title = vcf.add('TITLE').value = str(ORGrow[4]) if ORGrow[3] is not None: role = vcf.add('ROLE').value = str(ORGrow[3]) # Also parameters are possible. Could be read out # | columnID | column | # ---------------------------- # | 0 | detailId | # | 1 | contactId | # | 2 | name | # | 3 | role | # | 4 | title | # | 5 | location | # | 6 | department | # | 7 | logoUrl | # | 8 | assistantName | ## Abfragen E-Mail-Adressen SQLEmailCur = SQLconn.cursor() for Emailrow in SQLEmailCur.execute('SELECT * from EmailAddresses JOIN Details on Details.detailId= EmailAddresses.detailId where EmailAddresses.contactId = ' + str(contactID)): # debug ausgabe DEBUG(args.debug,str(Emailrow[2]) + " at " + str(Emailrow[9])) email = vcf.add('email') email.value = str(Emailrow[2]) # nur den Typ einpflegen, wenn das hier nicht none ist if Emailrow[9] != None: email.type_param = str(Emailrow[9]) SQLPhoneCur = SQLconn.cursor() ## Abfragen Telefonnummer, Fax, SMS - Nummern kommen aus der gleichen Tabelle for Phonerow in SQLPhoneCur.execute('SELECT * from PhoneNumbers JOIN Details on Details.detailId = PhoneNumbers.detailId where PhoneNumbers.contactId = ' + str(contactID)): # wir müssen die SubTypen unterscheiden #Null voice #1 cell #2 fax #3 pager #6 video #10 Assistent # None is a normal phone Number if Phonerow[3] == "1": phcat='cell' elif Phonerow[3] == "2": phcat='fax' elif Phonerow[3] == "3": phcat='pager' elif Phonerow[3] == "6": phcat='video' elif Phonerow[3] == "10": phcat='assistent' elif Phonerow[3] is None: phcat='voice' # debug ausgabe DEBUG(args.debug,str(Phonerow[2]) + " at " + str(Phonerow[10]) + " as subtype=" + str(Phonerow[3]) + "=" + phcat) phone = vcf.add(phcat).value = str(Phonerow[2]) # nur den Typ einpflegen, wenn das hier nicht none ist if Phonerow[10] != None: try: phone.type_param = str(Phonerow[10]) except AttributeError: continue if args.avatars is not None: DEBUG(args.debug,"Avatar Argument given") SQLAVTRCur = SQLconn.cursor() ## get Avatar Filelink from DB for AVTRrow in SQLAVTRCur.execute('SELECT imageURL from Avatars where contactId = ' + str(contactID)): DEBUG(args.debug,"found PHOTO entry") if AVTRrow[0] is not None: avatarfile=os.path.split(AVTRrow[0])[1] # pre-checks # - is it a file # - is it jpg import mimetypes afile=args.avatars + "/" + avatarfile DEBUG(args.debug,"Avatar File: " + afile + " mimetype: " + str(mimetypes.guess_type(afile))) if os.path.isfile(afile) and mimetypes.guess_type(afile)[0] == "image/jpeg": DEBUG(args.debug,"found file " + afile) import base64 # actions # - encode base24 to variable # - add to vcard fileopen=open(afile,'rb') bfile=base64.b64encode(fileopen.read()) fileopen.close() # https://stackoverflow.com/a/61532783 photo = vcf.add('PHOTO;ENCODING=b;TYPE=image/jpeg') photo.value = str(bfile.decode('utf-8')) else: print("file " + afile + " not found or no JPG") # Output to file f = open(cardfile,'w') f.write(vcf.serialize()) f.close() # hier brauchen wir einige eception handles -> wie bekommen wir die einzelnen exceptions heruas ? #except: #print("Error in executing SQL") except AttributeError: print("Datatype mismatch") raise # das generöse Except am Ende except: print("unhandled error") raise