From 4d3af13996bbd26dcb07285a8460f04af345fa85 Mon Sep 17 00:00:00 2001 From: relikd Date: Sat, 5 Jun 2021 20:52:18 +0200 Subject: [PATCH] Initial --- .gitignore | 2 + ABCDDB.py | 374 ++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 7 + README.md | 44 ++++++ abcddb2vcard.py | 21 +++ test.jpg | Bin 0 -> 3059 bytes test.vcf | 166 +++++++++++++++++++++ vcard2image.py | 54 +++++++ 8 files changed, 668 insertions(+) create mode 100644 .gitignore create mode 100755 ABCDDB.py create mode 100644 LICENSE create mode 100644 README.md create mode 100755 abcddb2vcard.py create mode 100644 test.jpg create mode 100644 test.vcf create mode 100755 vcard2image.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5155e00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.DS_Store diff --git a/ABCDDB.py b/ABCDDB.py new file mode 100755 index 0000000..deed9cc --- /dev/null +++ b/ABCDDB.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +import sys +import sqlite3 +from base64 import b64encode +from urllib.parse import quote + +PROD_ID = '-//Apple Inc.//Mac OS X 10.15.7//EN' +ITEM_COUNTER = 0 + + +class ABCDDB(object): + @staticmethod + def load(db_path): + db = sqlite3.connect(db_path) + cur = db.cursor() + + records = Record.queryAll(cur) + + # query once, then distribute + for x in Email.queryAll(cur): + records[x.parent].email.append(x) + + for x in Phone.queryAll(cur): + records[x.parent].phone.append(x) + + for x in Address.queryAll(cur): + records[x.parent].address.append(x) + + for x in SocialProfile.queryAll(cur): + records[x.parent].socialprofile.append(x) + + for x in Note.queryAll(cur): + records[x.parent].note = x.text + + for x in URL.queryAll(cur): + records[x.parent].urls.append(x) + + for x in Service.queryAll(cur): + records[x.parent].service.append(x) + + db.close() + return records.values() + + +def incrItem(value, label): + global ITEM_COUNTER + ITEM_COUNTER += 1 + return 'item{0}.{1}\r\nitem{0}.X-ABLabel:{2}'.format( + ITEM_COUNTER, value, label) + + +def x520(val): + if not val: + return None + return val.replace(';', '\\;').replace(',', '\\,') + + +def buildLabel(prefix, label, isFirst, suffix, validOther=False): + typ = '' + if label == '_$!!$_': + typ = ';type=HOME' + elif label == '_$!!$_': + typ = ';type=WORK' + elif validOther and label == '_$!!$_': + typ = ';type=OTHER' + + value = prefix + typ + (';type=pref:' if isFirst else ':') + suffix + if typ: + return value + else: + return incrItem(value, label) + + +class Record(object): + @staticmethod + def queryAll(cursor): + return {x[0]: Record(x) for x in cursor.execute(''' + SELECT Z_PK, + ZFIRSTNAME, ZLASTNAME, ZMIDDLENAME, ZTITLE, ZSUFFIX, + ZNICKNAME, ZMAIDENNAME, + ZPHONETICFIRSTNAME, ZPHONETICMIDDLENAME, ZPHONETICLASTNAME, + ZPHONETICORGANIZATION, ZORGANIZATION, ZDEPARTMENT, ZJOBTITLE, + strftime('%Y-%m-%d', ZBIRTHDAY + 978307200, 'unixepoch'), + ZTHUMBNAILIMAGEDATA, ZDISPLAYFLAGS + FROM ZABCDRECORD + WHERE ZCONTAINER1 IS NOT NULL;''')} + + def __init__(self, row): + self.id = row[0] + self.firstname = x520(row[1]) or '' + self.lastname = x520(row[2]) or '' + self.middlename = x520(row[3]) or '' + self.nameprefix = x520(row[4]) or '' + self.namesuffix = x520(row[5]) or '' + self.nickname = x520(row[6]) + self.maidenname = x520(row[7]) + self.phonetic_firstname = x520(row[8]) + self.phonetic_middlename = x520(row[9]) + self.phonetic_lastname = x520(row[10]) + self.phonetic_org = x520(row[11]) + self.organization = x520(row[12]) or '' + self.department = x520(row[13]) or '' + self.jobtitle = x520(row[14]) + self.bday = row[15] + self.email = [] + self.phone = [] + self.address = [] + self.socialprofile = [] + self.note = None + self.urls = [] + self.service = [] + self.image = row[16] + self.iscompany = row[17] & 1 + + def __repr__(self): + return self.makeVCard() + + def makeVCard(self): + global ITEM_COUNTER + ITEM_COUNTER = 0 + t = 'BEGIN:VCARD\r\nVERSION:3.0' + t += '\r\nPRODID:' + PROD_ID + + def optional(key, value): + nonlocal t + if value: + t += '\r\n' + key + ':' + value + + def optionalArray(arr): + nonlocal t + isFirst = True + for x in arr: + t += '\r\n' + x.asStr(markPref=isFirst) + isFirst = False + + t += '\r\nN:' + ';'.join((self.lastname, self.firstname, + self.middlename, self.nameprefix, + self.namesuffix)) + if self.iscompany: + fullname = self.organization + else: + fullname = ' '.join(filter(None, [ + self.nameprefix, self.firstname, self.middlename, + self.lastname, self.namesuffix])) + + t += '\r\nFN:' + fullname + optional('NICKNAME', self.nickname) + optional('X-MAIDENNAME', self.maidenname) + optional('X-PHONETIC-FIRST-NAME', self.phonetic_firstname) + optional('X-PHONETIC-MIDDLE-NAME', self.phonetic_middlename) + optional('X-PHONETIC-LAST-NAME', self.phonetic_lastname) + + if self.organization or self.department: + t += '\r\nORG:' + self.organization + ';' + self.department + + optional('X-PHONETIC-ORG', self.phonetic_org) + optional('TITLE', self.jobtitle) + optionalArray(self.email) + optionalArray(self.phone) + optionalArray(self.address) + optionalArray(self.socialprofile) + optional('NOTE', self.note) + optionalArray(self.urls) + + if self.bday: + key = 'BDAY' + if self.bday.startswith('1604'): + key += ';X-APPLE-OMIT-YEAR=1604' + t += '\r\n' + key + ':' + self.bday + + for kind in ['Jabber', 'MSN', 'Yahoo', 'ICQ']: + isFirst = True + for x in self.service: + if x.service == kind: + t += '\r\n' + x.asSpecialStr(markPref=isFirst) + isFirst = False + optionalArray(self.service) + + if self.image: + try: + t += '\r\n' + self.imageAsBase64() + except NotImplementedError: + print('''Image format not supported. + Could not extract image for contact: {} + @: {}... + skipping.'''.format(fullname, self.image[:20]), file=sys.stderr) + if self.iscompany: + t += '\r\nX-ABShowAs:COMPANY' + return t + '\r\nEND:VCARD\r\n' + + def imageAsBase64(self): + if not self.image: + return + img = self.image[1:] # why does Apple prepend \x01 to all images?! + t = 'PHOTO;ENCODING=b;TYPE=' + if img[6:10] == b'JFIF': + t += 'JPEG:' + b64encode(img).decode('ascii') + else: + raise NotImplementedError( + 'Image types other than JPEG are not supported yet.') + # place 'P' manually for nice 75 char alignment + return t[0] + '\r\n '.join(t[i:i + 74] for i in range(1, len(t), 74)) + + +class Email(object): + @staticmethod + def queryAll(cursor): + return (Email(x) for x in cursor.execute(''' + SELECT ZOWNER, ZLABEL, ZADDRESS + FROM ZABCDEMAILADDRESS + ORDER BY ZOWNER, ZISPRIMARY DESC, ZORDERINGINDEX;''')) + + def __init__(self, row): + self.parent = row[0] + self.label = x520(row[1]) + self.email = x520(row[2]) + + def asStr(self, markPref): + return buildLabel('EMAIL;type=INTERNET', self.label, markPref, + self.email) + + +class Phone(object): + @staticmethod + def queryAll(cursor): + return (Phone(x) for x in cursor.execute(''' + SELECT ZOWNER, ZLABEL, ZFULLNUMBER + FROM ZABCDPHONENUMBER + ORDER BY ZOWNER, ZISPRIMARY DESC, ZORDERINGINDEX;''')) + + def __init__(self, row): + self.parent = row[0] + self.label = x520(row[1]) + self.number = x520(row[2]) + + def asStr(self, markPref): + mapping = { + '_$!!$_': ';type=CELL;type=VOICE', + 'iPhone': ';type=IPHONE;type=CELL;type=VOICE', + '_$!!$_': ';type=HOME;type=VOICE', + '_$!!$_': ';type=WORK;type=VOICE', + '_$!
!$_': ';type=MAIN', + '_$!!$_': ';type=HOME;type=FAX', + '_$!!$_': ';type=WORK;type=FAX', + '_$!!$_': ';type=OTHER;type=FAX', + '_$!!$_': ';type=PAGER', + '_$!!$_': ';type=OTHER;type=VOICE' + } + value = (';type=pref:' if markPref else ':') + self.number + if self.label in mapping.keys(): + return 'TEL' + mapping[self.label] + value + else: + return incrItem('TEL' + value, self.label) + + +class Address(object): + @staticmethod + def queryAll(cursor): + return (Address(x) for x in cursor.execute(''' + SELECT ZOWNER, ZLABEL, + ZSTREET, ZCITY, ZSTATE, ZZIPCODE, ZCOUNTRYNAME + FROM ZABCDPOSTALADDRESS + ORDER BY ZOWNER, ZISPRIMARY DESC, ZORDERINGINDEX;''')) + + def __init__(self, row): + self.parent = row[0] + self.label = x520(row[1]) + self.street = x520(row[2]) or '' + self.city = x520(row[3]) or '' + self.state = x520(row[4]) or '' + self.zip = x520(row[5]) or '' + self.country = x520(row[6]) or '' + + def asStr(self, markPref): + value = ';'.join((self.street, self.city, self.state, self.zip, + self.country)) + return buildLabel('ADR', self.label, markPref, ';;' + value, + validOther=True) + + +class SocialProfile(object): + @staticmethod + def queryAll(cursor): + return (SocialProfile(x) for x in cursor.execute(''' + SELECT ZOWNER, ZSERVICENAME, ZUSERNAME + FROM ZABCDSOCIALPROFILE;''')) + + def __init__(self, row): + self.parent = row[0] + self.service = row[1] + # no x520(); actually, Apple does that ... and it breaks on reimport + self.user = row[2] # Apple: x520() + + def asStr(self, markPref): + # Apple does some x-user, x-apple, and url stuff that is wrong + return 'X-SOCIALPROFILE;type=' + self.service.lower() + ':' + self.user + + +class Note(object): + @staticmethod + def queryAll(cursor): + return (Note(x) for x in cursor.execute(''' + SELECT ZCONTACT, ZTEXT + FROM ZABCDNOTE + WHERE ZTEXT IS NOT NULL;''')) + + def __init__(self, row): + self.parent = row[0] + self.text = x520(row[1]) + + +class URL(object): + @staticmethod + def queryAll(cursor): + return (URL(x) for x in cursor.execute(''' + SELECT ZOWNER, ZLABEL, ZURL + FROM ZABCDURLADDRESS + ORDER BY ZOWNER, ZISPRIMARY DESC, ZORDERINGINDEX;''')) + + def __init__(self, row): + self.parent = row[0] + self.label = x520(row[1]) + self.url = x520(row[2]) + + def asStr(self, markPref): + return buildLabel('URL', self.label, markPref, self.url) + + +class Service(object): + @staticmethod + def queryAll(cursor): + return (Service(x) for x in cursor.execute(''' + SELECT ZOWNER, ZSERVICENAME, ZLABEL, ZADDRESS + FROM ZABCDMESSAGINGADDRESS + INNER JOIN ZABCDSERVICE ON ZSERVICE = ZABCDSERVICE.Z_PK + ORDER BY ZOWNER, ZISPRIMARY DESC, ZORDERINGINDEX;''')) + + def __init__(self, row): + self.parent = row[0] + self.service = row[1] + if self.service.endswith('Instant'): + self.service = self.service[:-7] # drop suffix + self.label = x520(row[2]) + self.username = x520(row[3]) + + def isSpecial(self): + return self.service in ['Jabber', 'MSN', 'Yahoo', 'ICQ'] + + def asSpecialStr(self, markPref): + return buildLabel('X-' + self.service.upper(), self.label, markPref, + self.username) + + def asStr(self, markPref): + if self.service in ['Jabber', 'GoogleTalk', 'Facebook']: + typ = 'xmpp' + elif self.service in ['GaduGadu', 'QQ']: + typ = 'x-apple' + elif self.service == 'ICQ': + typ = 'aim' + elif self.service == 'MSN': + typ = 'msnim' + elif self.service == 'Skype': + typ = 'skype' + elif self.service == 'Yahoo': + typ = 'ymsgr' + else: + raise NotImplementedError('Unkown Service: ' + self.service) + + # Dear Apple, why do you do such weird shit, URL encoding? bah! + # Even worse, you break it so that reimport fails. + # user = quote(self.username, safe='!/()=_:.\'$&').replace('%2C', '\\,') + user = self.username + return buildLabel('IMPP;X-SERVICE-TYPE=' + self.service, self.label, + markPref, typ + ':' + user) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..724c90c --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2021 Oleg Geier + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b7a471 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# abcddb2vcard + +This python script reads an AddressBook database file (`AddressBook-v22.abcddb`) and export its content to a vCard file (`.vcf`). + +I created this script to automate my contacts backup procedure. +The output of this script should be exactly the same as dragging and dropping the “All Contacts” card. + + +### Usage + +``` +python3 abcddb2vcard.py backup/contacts_$(date +"%Y-%m-%d").vcf +``` + +> assuming db is located at "~/Library/Application Support/AddressBook/AddressBook-v22.abcddb" + +#### Extract contact images + +``` +python3 vcard2image.py AllContacts.vcf ./profile_pics/ +``` + + +### Supported data fields + +`firstname`, `lastname`, `middlename`, `nameprefix`, `namesuffix`, `nickname`, `maidenname`, `phonetic_firstname`, `phonetic_middlename`, `phonetic_lastname`, `phonetic_organization`, `organization`, `department`, `jobtitle`, `birthday`, `[email]`, `[phone]`, `[address]`, `[socialprofile]`, `note`, `[url]`, `[xmpp-service]`, `image`, `iscompany` + + +### Limitations + +The `image` field currently only supports JPG images. +I have honestly no idea where PNG images are stored. +For PNGs the database only stores a UUID instead of the file itself. +If you happen to know where I can find these, open an issue or pull request. + + +### Disclaimer + +You should check the output for yourself before using it in a production environment. +I have tested the script with many arbitrary fields, however there may be some edge cases missing. +Feel free to create an issue for missing or wrong field values. + +**Note:** The output of `diff` or `FileMerge.app` can be different to this output. +Apples does some weird transformations on vcf export that are not only unnecessary but in many cases break the re-import of the file. \ No newline at end of file diff --git a/abcddb2vcard.py b/abcddb2vcard.py new file mode 100755 index 0000000..db3b164 --- /dev/null +++ b/abcddb2vcard.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import sys +from ABCDDB import ABCDDB +from pathlib import Path + +if len(sys.argv) != 2: + print(' Usage:', Path(__file__).name, 'outfile.vcf') + exit(0) + +outfile = Path(sys.argv[1]) +if not outfile.parent.exists(): + print('Output directory does not exist.', file=sys.stderr) + exit(1) + +contacts = ABCDDB.load(Path.home().joinpath( + 'Library/Application Support/AddressBook/AddressBook-v22.abcddb')) +# contacts = [list(contacts)[-1]] # test on last imported contact +with open(outfile, 'w') as f: + for rec in contacts: + f.write(rec.makeVCard()) + print(len(contacts), 'contacts.') diff --git a/test.jpg b/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..56d3dfdad04d386386d932cbf8557109c535204e GIT binary patch literal 3059 zcmex=aDZ!z#NGcqs5(Hdr_a5xi%-_zaf$CTseMxowkvylPOH6fK4iC4a*>pELAPG!fr$e;9<&x(Jq?(kzb`?Z2He>bab_dkB# zf9Uc4U%Tx;REcxUTk>L`qV}6$nQ4!H#qb@i*|2Sob6^G22i6DN2gDon8R8iBF!->< zF>y9@AJBYP|9#^2txx|vlM}M_3aoJozp>0{yLcUY(!Pk&^C2uN9Zt?!_q^?nK*!nY zXmx#e_bb2Rmqa`5cD#7;tKIaMylfXrN^1R={&IHQA}jk3jS=DP!+z6yL4Qgtp1r@WPJi*N;ip8pk1^-9{aq5%3f^AX(!HeiI&UN6dEyrgpqsId8en1itW@amS@%*H3l6Qk}t_eL(B|&u?bm z_MD${;86R@DE$tv-z;unkKP@46MmX$L*BMM?AMg8_+EY7rg!$qoWfnQ$3nNO>BSkZ zZ;gK!$xtc8lCGQRxvIl+`@gwU4QAa<+*o^VW!ROCN9XEA+TU2E8>&<7dL{O<|Cx(V zzs@*!Cpx@YdFie_U1?>1FzR|>xwKQb zNpQmUW#SuD*LQN*#KjxlYCT!n9qH}mcc&}p(6#C(%b1R{X9`sd zCe2(`I6Ze&k!0}NSS@}gE=R9-i{`d^u~#!jTv=5xZ|cnHlcjdWR!T=m3r*FS9Z|k+ zdG)>ZI?Q$-xbA{3~qX0%=G>VtzXW0e<0WL~#sQkyz|=k+bMPKxPaGiO!a(Yxq+I%BqD(cMF>jrA4> zG7TnQdY4!g^|a$kXV~P;4I(P6%nt(08xACdtIoYtn))+ZvYh4AY~_ZgS7rS?tZiCM z^sbynKV6qdlpmo?wTw8uNo5XF~--Q-?)A2vzoWhL~XqT_c(>$P%_#sUIxmY zwO9H6?q}={G!NhPI@fG@Y*chE)9a`XF9(^FxlMX!C#-sz&E{bd?Q(eXI`zamE6&XR zXDXublgBOd(i*op8*WXXT2#E!|I+5He>WeQs(X7o9p4eQZNrr*uUro8P@TuRoWDUk zY0sq$jHe$a}KOkxSvD zw1&d`{W|O}|9XS&yf)rZR~xc?c8=KsM)M7)XB&B#2fY3_g>{|8!B1bp8FCDfK%p{oLJ$$yfGDA5pDsQnoIf4h|sElgm^ijDxv( zHyq9?ygH9z%3C$vkh{-gvU^`eOv?2*vrN+?{OnW44GIOir%O-U^{9EY#fJ+_T;0H= z<0#E>i(iI0+0DvvR#oCI(Zoxyrth6NJ95`L`978kV;DhScOpl(1l4fyHC5L=a!GrePs*oHOu zeA9l~I`@%OWAQ;o>*@)DNEJ@*b{3Z0cMgsQf)%V&|1;RS?Ylnn^Y(~Hhlf>$(ryo? z-u}=>#nvS0B+2KT4qK%?gG*`&=k#e_>o=roS#JC2cDX%fo}aw$ zvDq%JYrd{db=@NR^{BFW+wJ7WZ|hgXszdgLlOl*1fVkRaN>br`xQ`YtdJy zP4M(^^sxT4_Dku5OKR(Ldj7POu`V1ScEq=*=fAuj?2&^%QduQz? zu)K3+r23Q7&IOt`+aqT1)bm(O+3<_w^fj5An@TSnxjgUbE|y1c7Kenlzr6o7d%K_N zlb3ci?+>|E&)s={mu-BQx=()C^hPz7J99Scylb<+la-mb>gSFLE6duKOFekH^4Zrd zw-YUo)Y;ny-F7}b+s%4P$ZFp`-{5lh4L*TALeRa>z&+CMwd*0ubTfgG2#q`NbetzE1$$PB)U4QtMyg8mH zFV+0K-_rKj`n&t;SLQG`Iv39Uz4+=^>E%M7O6J>@KUCat?(fA{zozc0EL*XwR=n)A zp&s-4Cpu?zKb5c8duV2|#=jLWUi?XY`1Q%TuIKXe|NaQikUiC;HMRGs-sx9HZ`1f~ zmg$(bH!NNLxymqa;*aDj!Iro6-xSP*+20p?ei7J)oy(sYZk$>Wyd?9)Y=u|T{yaYI z5xPxgQK;k|$;-l$+LcF6e>qzGxi+ro)#IQ48May9zjg0F!$R5m<$vvxfA-(~d;R}S E05a`&FaQ7m literal 0 HcmV?d00001 diff --git a/test.vcf b/test.vcf new file mode 100644 index 0000000..9a283fc --- /dev/null +++ b/test.vcf @@ -0,0 +1,166 @@ +BEGIN:VCARD +VERSION:3.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +N:Lastname;Firstname\,apdx;middlename;prefixname;suffixname +FN:Company\;\,name:or-so–with_and$%&/)("§$!`? +NICKNAME:Nickname +X-MAIDENNAME:maidenname +X-PHONETIC-FIRST-NAME:phoneticfirst +X-PHONETIC-MIDDLE-NAME:phoneticmiddle +X-PHONETIC-LAST-NAME:phoneticlast +ORG:Company\;\,name:or-so–with_and$%&/)("§$!`?;department +X-PHONETIC-ORG:phoneticcompany +TITLE:jobtitle +EMAIL;type=INTERNET;type=HOME;type=pref:emailhome +item1.EMAIL;type=INTERNET:comma\,ma\;il +item1.X-ABLabel:hi\,ther\;e +item2.EMAIL;type=INTERNET:emailother +item2.X-ABLabel:_$!!$_ +EMAIL;type=INTERNET;type=WORK:emailwork +TEL;type=CELL;type=VOICE;type=pref:phonemobile +item3.TEL:otherphone +item3.X-ABLabel:cu\;stom\,s +TEL;type=HOME;type=VOICE:phonehome +TEL;type=HOME;type=FAX:phonefax +TEL;type=MAIN:phonemain +TEL;type=IPHONE;type=CELL;type=VOICE:phoneiphone +TEL;type=WORK;type=FAX:workfax +TEL;type=OTHER;type=FAX:otherfax +TEL;type=PAGER:pager +TEL;type=WORK;type=VOICE:phonework +TEL;type=OTHER;type=VOICE:phoneopther +ADR;type=HOME;type=pref:;;homestreet;homecity;state;homepostal;homecountry +item4.ADR:;;customstreet;customcity;;custompostal;customcountry +item4.X-ABLabel:dfdf +ADR;type=OTHER:;;otherstreet;othercity;;otherpostal;othercountry +ADR;type=WORK:;;workstreet;workcity;sta;workpostal;workcountry +X-SOCIALPROFILE;type=twitter:twit,t +X-SOCIALPROFILE;type=flickr:flic,kr +X-SOCIALPROFILE;type=myspace:my;spa,ce +X-SOCIALPROFILE;type=facebook:faceo,ok +X-SOCIALPROFILE;type=yelp:ye,lp +X-SOCIALPROFILE;type=tencentweibo:tr,ansent +X-SOCIALPROFILE;type=linkedin:lin;ked,in +X-SOCIALPROFILE;type=sinaweibo:s,ina +NOTE:notes +item5.URL;type=pref:urlhomepage +item5.X-ABLabel:_$!!$_ +item6.URL:!"§$%&/()=?¡“¶¢[]|{}≠¿'´-–—_:.<>\,\; +item6.X-ABLabel:ss +item7.URL:url other +item7.X-ABLabel:_$!!$_ +URL;type=WORK:url work +URL;type=HOME:url home +BDAY;X-APPLE-OMIT-YEAR=1604:1604-01-01 +X-JABBER;type=WORK;type=pref:jabwork +item8.X-JABBER:jabother +item8.X-ABLabel:_$!!$_ +X-JABBER;type=HOME:ja\,bb\;er +X-MSN;type=HOME;type=pref:m\,s\;n +item9.X-YAHOO;type=pref:\,yahoo\; +item9.X-ABLabel:_$!!$_ +X-ICQ;type=HOME;type=pref:i\,c\;q +X-ICQ;type=HOME:!"§$%&/()=?¡“¶¢[]|{}≠¿'´-–—_:.<>\,\; +item10.X-ICQ:v\,ie\;\;r +item10.X-ABLabel:lo\;l +X-ICQ;type=HOME:.<>\,\; +IMPP;X-SERVICE-TYPE=ICQ;type=HOME;type=pref:aim:i\,c\;q +IMPP;X-SERVICE-TYPE=ICQ;type=HOME:aim:!"§$%&/()=?¡“¶¢[]|{}≠¿'´-–—_:.<>\,\; +IMPP;X-SERVICE-TYPE=GaduGadu;type=HOME:x-apple:g\,ad\;z +item11.IMPP;X-SERVICE-TYPE=ICQ:aim:v\,ie\;\;r +item11.X-ABLabel:lo\;l +IMPP;X-SERVICE-TYPE=Jabber;type=WORK:xmpp:jabwork +item12.IMPP;X-SERVICE-TYPE=Skype:skype:sk\;yp\,e +item12.X-ABLabel:_$!!$_ +IMPP;X-SERVICE-TYPE=Facebook;type=HOME:xmpp:face\,boo\;k +IMPP;X-SERVICE-TYPE=GoogleTalk;type=HOME:xmpp:go\,go\;le +item13.IMPP;X-SERVICE-TYPE=Jabber:xmpp:jabother +item13.X-ABLabel:_$!!$_ +item14.IMPP;X-SERVICE-TYPE=Yahoo:ymsgr:\,yahoo\; +item14.X-ABLabel:_$!!$_ +IMPP;X-SERVICE-TYPE=ICQ;type=HOME:aim:.<>\,\; +item15.IMPP;X-SERVICE-TYPE=QQ:x-apple:\,qq\; +item15.X-ABLabel:_$!!$_ +IMPP;X-SERVICE-TYPE=Jabber;type=HOME:xmpp:ja\,bb\;er +IMPP;X-SERVICE-TYPE=MSN;type=HOME:msnim:m\,s\;n +PHOTO;ENCODING=b;TYPE=JPEG:/9j/4AAQSkZJRgABAQAASABIAAD/4QBMRXhpZgAATU0AKgAA + AAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAcqADAAQAAAABAA + AAXQAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCy + BOmACZjs+EJ+/8AAEQgAXQByAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQ + YHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEV + UtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dX + Z3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY + 2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC/ + /EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy + 0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eo + KDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj + 5OXm5+jp6vLz9PX29/j5+v/bAEMAAgICAgICAwICAwQDAwMEBQQEBAQFBwUFBQUFBwgHBwcHBw + cICAgICAgICAoKCgoKCgsLCwsLDQ0NDQ0NDQ0NDf/bAEMBAgICAwMDBgMDBg0JBwkNDQ0NDQ0N + DQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDf/dAAQACP/aAAwDAQ + ACEQMRAD8A/Tf9nH9nX4XeG/hZ4f1PUfD+m6vrGsaZa397e39tHcyNJcxrKUQyq21F3bcLjpk8 + 19A/8Kn+F3/Qn6D/AOCu1/8AjdV/hCf+LUeDD/1L2lf+ksVejbjW9arOc3KTPIyvK8HRwlOnTp + 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 + ARDwP/AD7l+B//2Q== +X-ABShowAs:COMPANY +END:VCARD diff --git a/vcard2image.py b/vcard2image.py new file mode 100755 index 0000000..96c1542 --- /dev/null +++ b/vcard2image.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import os +import re +import sys +from base64 import b64decode +from pathlib import Path + +if len(sys.argv) != 3: + print(' Usage:', Path(__file__).name, 'infile.vcf', 'outdir/') + exit(0) + +infile = Path(sys.argv[1]) +outdir = Path(sys.argv[2]) +if not infile.exists(): + print('Does not exist: ', infile, file=sys.stderr) + exit(1) +elif not outdir.exists(): + if not outdir.parent.exists(): + print('Output directory does not exist.', file=sys.stderr) + exit(1) + else: + os.mkdir(outdir) + +with open(infile, 'r') as f: + c1 = 0 + c2 = 0 + name = '' + img = None + collect = False + for line in f.readlines(): + line = line.rstrip() + if line == 'BEGIN:VCARD': + c1 += 1 + name = '' + img = None + collect = False + elif line.startswith('FN:'): + name = line.split(':', 1)[1] + elif line.startswith('PHOTO;'): + img = line.split(':', 1)[1] + collect = True + elif collect: + if line[0] == ' ': + img += line[1:] + else: + collect = False + if line == 'END:VCARD' and img: + c2 += 1 + name = name.replace('\\,', ',').replace('\\;', ';').replace( + '/', '-') + with open(outdir.joinpath(name + '.jpg'), 'wb') as fw: + fw.write(b64decode(img)) + + print(c1, 'contacts.', c2, 'images.')