ContactRestore.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. #!/usr/bin/python3
  2. # coding=utf-8
  3. # Doc
  4. # Script to extract Contacts from Sailfish Contact SQLite DB located at
  5. #
  6. #
  7. # Links
  8. # Projects Page: https://wiki.siningsoft.de/doku.php?id=sailfishos:projects:sailfish_contacts_rescue
  9. # FileFormatdescription: https://docs.fileformat.com/email/vcf/#vcf-30-example
  10. # Pytho vobject: http://eventable.github.io/vobject/
  11. # Version
  12. version=0.6
  13. # ChangeLog
  14. # 2021-08-03 - 0.1 - multiple E-Mails with different types are working correctly
  15. # 2021-08-09 - 0.2 - Phonenumbers with parameters, Addresses with parameters, E-Mail-Addresses with marameters
  16. # 2021-08-10 - 0.3 - load Avatars into VCards
  17. # 2021-08-28 - 0.4 - bugfixing after full exporting my contacts
  18. # - Address export fixing
  19. # 2021-09-18 - 0.5 - bugfixing wrong table column mapping
  20. # 2021-09-21 - 0.6 - changing table mapping, adding fix for phonenumber categories
  21. import sqlite3
  22. import vobject
  23. import uuid
  24. import argparse
  25. import os
  26. #class vobject2(vobject):
  27. def DEBUG(debug,msg):
  28. if debug is True:
  29. print("..DEBUG: " + msg)
  30. # will return a default value, when var is None
  31. def setDefault(var, default):
  32. if var is None:
  33. return default
  34. else:
  35. return var
  36. 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' )
  37. parser.add_argument('--db','-d', required=True, help="Sqlite3 Database file usually /home/{nemo,defaultuser}/.local/share/system/Contacts/qtcontacts-sqlite/contacts.db")
  38. parser.add_argument('--output','-o',required=True, help="Output directory for vcf files")
  39. parser.add_argument('--avatars','-a',required=False, help='Avatar directory. If present otherwise we skip this block of avatars, means, no avatars at all')
  40. parser.add_argument('--debug',action="store_true",help="debugging output to identify problems")
  41. parser.add_argument('--version', action='version', version='%(prog)s ' + str(version))
  42. args = parser.parse_args()
  43. SQLconn = sqlite3.connect(args.db)
  44. SQLconn.row_factory = sqlite3.Row #das macht den Unterschied und liefert die Werte
  45. try:
  46. SQLContCur = SQLconn.cursor()
  47. SQLContCur.execute('SELECT * FROM Contacts')
  48. rows = SQLContCur.fetchall()
  49. for row in rows:
  50. # contactID abfragen
  51. contactID=setDefault(row['contactId'],"NA")
  52. familyN=setDefault(row['lastName'],"NA")
  53. givenN=setDefault(row['firstName'],"NA")
  54. if familyN != None and givenN != None:
  55. fullN = givenN + " " + familyN
  56. #fullN=setDefault(row['displayLabel'],"NA")
  57. cardfile=args.output + "/" + fullN.replace(" ","_").replace("(","").replace(")","").replace(".","").replace(",","") + ".vcf"
  58. # wir erstellen das Objekt
  59. vcf = vobject.vCard()
  60. vcf.add('uid').value = str(uuid.uuid4())
  61. #vcf.add('uid').value = "Testdaten"
  62. vcf.add('n').value = vobject.vcard.Name( family=familyN, given=givenN )
  63. vcf.add('fn').value =fullN
  64. print("exporting " + fullN + " to file " + cardfile)
  65. DEBUG(args.debug,"Contact " + str(fullN) + " family-name=" + str(familyN) + " given-name=" + str(givenN))
  66. # abfrage der Adressdaten
  67. SQLADRCur = SQLconn.cursor()
  68. SQLADRCur.execute('SELECT * FROM Addresses JOIN Details on Addresses.detailId = Details.detailId where Addresses.contactId = ' + str(contactID))
  69. ADRrows = SQLADRCur.fetchall()
  70. for ADRrow in ADRrows:
  71. if ADRrow['street'] is not None:
  72. ADRstr=str(ADRrow['street'])
  73. else:
  74. ADRstr="NA"
  75. if ADRrow['locality'] is not None:
  76. ADRcit=str(ADRrow['locality'])
  77. else:
  78. ADRcit="NA"
  79. if ADRrow['region'] is not None:
  80. ADRreg=str(ADRrow['region'])
  81. else:
  82. ADRreg="NA"
  83. if ADRrow['postCode'] is not None:
  84. ADRcod=str(ADRrow['postCode'])
  85. else:
  86. ADRcod="NA"
  87. if ADRrow['country'] is not None:
  88. ADRcou=str(ADRrow['country'])
  89. else:
  90. ADRcou="NA"
  91. # we need to know the context we've read here
  92. # 2021-09-19 - it figured out, that this might cause a call of AddressBehaviour for Address.replace(***)
  93. #if ADRrow[14] is not None:
  94. # addADR="ADR;TYPE="+str(ADRrow[14])
  95. #else:
  96. # addADR="ADR"
  97. vcf.add("ADR").value = vobject.vcard.Address(street=ADRstr, city=ADRcit, region=ADRreg, code=ADRcod,country=ADRcou)
  98. #adr = vcf.add(addADR).value = vobject.vcard.Address()
  99. DEBUG(args.debug,"Addressdata: street=" + ADRstr + " city=" + ADRcit + " region=" + ADRreg + " code=" + ADRcod + " country=" + ADRcou + " context=" + str(ADRrow[14]))
  100. ## Abfragen Organisation
  101. SQLORGCur = SQLconn.cursor()
  102. SQLORGCur.execute('SELECT * from Organizations where contactId = ' + str(contactID))
  103. ORGrows = SQLContCur.fetchall()
  104. for ORGrow in ORGrows:
  105. org = vcf.add('ORG').value = [str(ORGrow['name']), str(ORGrow['department'])]
  106. if ORGrow[4] is not None:
  107. title = vcf.add('TITLE').value = str(ORGrow['title'])
  108. if ORGrow[3] is not None:
  109. role = vcf.add('ROLE').value = str(ORGrow['role'])
  110. # Also parameters are possible. Could be read out
  111. # | columnID | column |
  112. # ----------------------------
  113. # | 0 | detailId |
  114. # | 1 | contactId |
  115. # | 2 | name |
  116. # | 3 | role |
  117. # | 4 | title |
  118. # | 5 | location |
  119. # | 6 | department |
  120. # | 7 | logoUrl |
  121. # | 8 | assistantName |
  122. ## Abfragen E-Mail-Adressen
  123. SQLEmailCur = SQLconn.cursor()
  124. SQLEmailCur.execute('SELECT * from EmailAddresses JOIN Details on Details.detailId= EmailAddresses.detailId where EmailAddresses.contactId = ' + str(contactID))
  125. Emailrows = SQLEmailCur.fetchall()
  126. for Emailrow in Emailrows:
  127. # debug ausgabe
  128. DEBUG(args.debug,str(Emailrow[2]) + " at " + str(Emailrow[9]))
  129. email = vcf.add('email')
  130. email.value = str(Emailrow['emailAddress'])
  131. # nur den Typ einpflegen, wenn das hier nicht none ist
  132. if Emailrow['contexts'] != None:
  133. email.type_param = str(Emailrow['contexts'])
  134. SQLPhoneCur = SQLconn.cursor()
  135. SQLPhoneCur.execute('SELECT * from PhoneNumbers JOIN Details on Details.detailId = PhoneNumbers.detailId where PhoneNumbers.contactId = ' + str(contactID))
  136. Phonerows = SQLPhoneCur.fetchall()
  137. ## Abfragen Telefonnummer, Fax, SMS - Nummern kommen aus der gleichen Tabelle
  138. for Phonerow in Phonerows:
  139. # wir müssen die SubTypen unterscheiden
  140. #Null voice
  141. #1 cell
  142. #2 fax
  143. #3 pager
  144. #6 video
  145. #10 Assistent
  146. # None is a normal phone Number
  147. if Phonerow['subTypes'] == "1":
  148. phcat='cell'
  149. elif Phonerow['subTypes'] == "2":
  150. phcat='fax'
  151. elif Phonerow['subTypes'] == "3":
  152. phcat='pager'
  153. elif Phonerow['subTypes'] == "6":
  154. phcat='video'
  155. elif Phonerow['subTypes'] == "10":
  156. phcat='assistent'
  157. elif Phonerow['subTypes'] is None:
  158. phcat='voice'
  159. # debug ausgabe
  160. DEBUG(args.debug,str(Phonerow['phoneNumber']) + " at " + str(Phonerow['contexts']) + " as subtype=" + str(Phonerow['subTypes']) + "=" + phcat)
  161. phaddcat = 'TEL;TYPE='+phcat;
  162. # nur den Typ einpflegen, wenn das hier nicht none ist
  163. if Phonerow['contexts'] != None:
  164. phaddcat = phaddcat + "," + Phonerow['contexts']
  165. phone = vcf.add(phaddcat).value = str(Phonerow['phoneNumber'])
  166. # nur den Typ einpflegen, wenn das hier nicht none ist
  167. if Phonerow['contexts'] != None:
  168. try:
  169. phone.type_param = str(Phonerow['contexts'])
  170. except AttributeError:
  171. continue
  172. if args.avatars is not None:
  173. DEBUG(args.debug,"Avatar Argument given")
  174. SQLAVTRCur = SQLconn.cursor()
  175. SQLAVTRCur.execute('SELECT imageURL from Avatars where contactId = ' + str(contactID))
  176. AVTRrows = SQLAVTRCur.fetchall()
  177. ## get Avatar Filelink from DB
  178. for AVTRrow in AVTRrows:
  179. DEBUG(args.debug,"found PHOTO entry")
  180. if AVTRrow[0] is not None:
  181. avatarfile=os.path.split(AVTRrow[0])[1]
  182. # pre-checks
  183. # - is it a file
  184. # - is it jpg
  185. import mimetypes
  186. afile=args.avatars + "/" + avatarfile
  187. DEBUG(args.debug,"Avatar File: " + afile + " mimetype: " + str(mimetypes.guess_type(afile)))
  188. if os.path.isfile(afile) and mimetypes.guess_type(afile)[0] == "image/jpeg":
  189. DEBUG(args.debug,"found file " + afile)
  190. import base64
  191. # actions
  192. # - encode base24 to variable
  193. # - add to vcard
  194. fileopen=open(afile,'rb')
  195. bfile=base64.b64encode(fileopen.read())
  196. fileopen.close()
  197. # https://stackoverflow.com/a/61532783
  198. photo = vcf.add('PHOTO;ENCODING=b;TYPE=image/jpeg')
  199. photo.value = str(bfile.decode('utf-8'))
  200. else:
  201. print("file " + afile + " not found or no JPG")
  202. # Output to file
  203. f = open(cardfile,'w')
  204. f.write(vcf.serialize())
  205. f.close()
  206. # hier brauchen wir einige eception handles -> wie bekommen wir die einzelnen exceptions heruas ?
  207. #except:
  208. #print("Error in executing SQL")
  209. except AttributeError:
  210. print("Datatype mismatch")
  211. raise
  212. # das generöse Except am Ende
  213. except:
  214. print("unhandled error")
  215. raise