Created
August 10, 2018 22:08
-
-
Save samr7/7541db77ba36742f269f672ff33c80e8 to your computer and use it in GitHub Desktop.
iOS backup contact list extractor and VCF converter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
import sys | |
import sqlite3 | |
import datetime | |
import base64 | |
def convert_ab_time(tmx): | |
epoch = datetime.datetime(2001, 1, 1) | |
return epoch + datetime.timedelta(seconds=float(tmx)) | |
def extract_contacts(ab, abi): | |
cursor = ab.cursor() | |
xcursor = ab.cursor() | |
xqcursor = ab.cursor() | |
icursor = abi.cursor() | |
xcursor.execute('SELECT ROWID, value from ABMultiValueLabel') | |
mvlabelids = dict() | |
for row in xcursor: | |
value = row[1] | |
if value.startswith('_$!<') and value.endswith('>!$_'): | |
value = value[4:-4] | |
mvlabelids[row[0]] = value | |
xcursor.execute('SELECT ROWID, value from ABMultiValueEntryKey') | |
mvelabelids = dict() | |
for row in xcursor: | |
value = row[1] | |
mvelabelids[row[0]] = value | |
cursor.execute('SELECT ROWID, First, Middle, Last, Prefix, Suffix, Organization, Note, Birthday FROM ABPerson') | |
for row in cursor: | |
print('BEGIN:VCARD') | |
print('VERSION:3.0') | |
empty = lambda v: '' if (v is None) else str(v) | |
first = empty(row[1]) | |
middle = empty(row[2]) | |
last = empty(row[3]) | |
prefix = empty(row[4]) | |
suffix = empty(row[5]) | |
org = empty(row[6]) | |
nonempty = lambda v: (v + ' ') if (len(v) > 0) else '' | |
if last or first: | |
print('FN:{0}{1}{2}{3}'.format(nonempty(prefix),nonempty(first),nonempty(middle),nonempty(last),nonempty(suffix))) | |
print('N:{0};{1};{2};{3};{4}'.format(last,first,middle,prefix,suffix)) | |
elif org: | |
print('FN:{0}'.format(org)) | |
if org: | |
print('ORG:{0}'.format(row[6])) | |
if row[7]: | |
print('NOTE:{0}'.format(row[7])) | |
if row[8]: | |
bday = convert_ab_time(row[8]) | |
print('BDAY:{0}'.format(bday.strftime('%Y%m%d'))) | |
xcursor.execute('SELECT ROWID, property, label, value FROM ABMultiValue WHERE record_id = ?', (row[0],)) | |
for xrow in xcursor: | |
ttype = '' | |
if xrow[2] is not None: | |
ttype = ';TYPE={0}'.format(mvlabelids[xrow[2]]) | |
if xrow[1] == 3: | |
print('TEL{0}:{1}'.format(ttype, xrow[3])) | |
elif xrow[1] == 4: | |
print('EMAIL{0}:{1}'.format(ttype, xrow[3])) | |
elif xrow[1] == 5: | |
blank = '' | |
addr = dict() | |
xqcursor.execute('SELECT key, value FROM ABMultiValueEntry WHERE parent_id = ?', (xrow[0],)) | |
for xqrow in xqcursor: | |
addr[mvelabelids[xqrow[0]]] = xqrow[1] | |
acomp = lambda key: addr[key] if key in addr else '' | |
print('ADR{0}:{1};{2};{3};{4};{5};{6};{7}'.format(ttype,blank,blank,acomp('Street'),acomp('City'),acomp('State'),acomp('ZIP'),acomp('Country'))) | |
elif xrow[1] == 12: | |
anniv = convert_ab_time(row[8]) | |
print('X-ANNIVERSARY:{0}'.format(anniv.strftime('%Y%m%d'))) | |
elif xrow[1] == 13: | |
addr = dict() | |
xqcursor.execute('SELECT key, value FROM ABMultiValueEntry WHERE parent_id = ?', (xrow[0],)) | |
for xqrow in xqcursor: | |
addr[mvelabelids[xqrow[0]]] = xqrow[1] | |
acomp = lambda key: addr[key] if key in addr else '' | |
if 'username' in addr: | |
print('IMPP:{0}:{1}'.format(acomp('service'), acomp('username'))) | |
elif xrow[1] == 22: | |
print('URL{0}:{1}'.format(ttype, xrow[3])) | |
elif xrow[1] == 23: | |
print('RELATED{0}:{1}'.format(ttype, xrow[3])) | |
else: | |
xqcursor.execute('SELECT key, value FROM ABMultiValueEntry WHERE parent_id = ?', (xrow[0],)) | |
va = '' | |
for xqrow in xqcursor: | |
va = va + ';{0}={1}'.format(mvelabelids[xqrow[0]], xqrow[1]) | |
print('UNK{0}:prop={1};val={2}{3}'.format(ttype,xrow[1],xrow[3],va)) | |
icursor.execute('SELECT format, data FROM ABThumbnailImage WHERE record_id = ?', (row[0],)) | |
for xrow in icursor: | |
if xrow[1] is None: continue | |
if xrow[0] != 0: continue | |
encoded = base64.b64encode(xrow[1]) | |
print('PHOTO;ENCODING=BASE64;TYPE=JPEG:' + bytes.decode(encoded)) | |
print('END:VCARD') | |
print('') | |
def extract_backup(path): | |
# AddressBook.sqlitedb | |
xab = sqlite3.connect('file:' + path + '/31/31bb7ba8914766d4ba40d6dfb6113c8b614be442?immutable=1', uri=True) | |
# AddressBookImages.sqlitedb | |
xabi = sqlite3.connect('file:' + path + '/cd/cd6702cea29fe89cf280a76794405adb17f9a0ee?immutable=1', uri=True) | |
extract_contacts(xab, xabi) | |
if len(sys.argv) == 1: | |
print('Usage: {0} <path-to-backup>'.format(sys.argv[0])) | |
sys.exit(1) | |
extract_backup(sys.argv[1]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment