Initial
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.DS_Store
|
||||||
374
ABCDDB.py
Executable file
374
ABCDDB.py
Executable file
@@ -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 == '_$!<Home>!$_':
|
||||||
|
typ = ';type=HOME'
|
||||||
|
elif label == '_$!<Work>!$_':
|
||||||
|
typ = ';type=WORK'
|
||||||
|
elif validOther and label == '_$!<Other>!$_':
|
||||||
|
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 = {
|
||||||
|
'_$!<Mobile>!$_': ';type=CELL;type=VOICE',
|
||||||
|
'iPhone': ';type=IPHONE;type=CELL;type=VOICE',
|
||||||
|
'_$!<Home>!$_': ';type=HOME;type=VOICE',
|
||||||
|
'_$!<Work>!$_': ';type=WORK;type=VOICE',
|
||||||
|
'_$!<Main>!$_': ';type=MAIN',
|
||||||
|
'_$!<HomeFAX>!$_': ';type=HOME;type=FAX',
|
||||||
|
'_$!<WorkFAX>!$_': ';type=WORK;type=FAX',
|
||||||
|
'_$!<OtherFAX>!$_': ';type=OTHER;type=FAX',
|
||||||
|
'_$!<Pager>!$_': ';type=PAGER',
|
||||||
|
'_$!<Other>!$_': ';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)
|
||||||
7
LICENSE
Normal file
7
LICENSE
Normal file
@@ -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.
|
||||||
44
README.md
Normal file
44
README.md
Normal file
@@ -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.
|
||||||
21
abcddb2vcard.py
Executable file
21
abcddb2vcard.py
Executable file
@@ -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.')
|
||||||
166
test.vcf
Normal file
166
test.vcf
Normal file
@@ -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:_$!<Other>!$_
|
||||||
|
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:_$!<HomePage>!$_
|
||||||
|
item6.URL:!"§$%&/()=?¡“¶¢[]|{}≠¿'´-–—_:.<>\,\;
|
||||||
|
item6.X-ABLabel:ss
|
||||||
|
item7.URL:url other
|
||||||
|
item7.X-ABLabel:_$!<Other>!$_
|
||||||
|
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:_$!<Other>!$_
|
||||||
|
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:_$!<Other>!$_
|
||||||
|
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:_$!<Other>!$_
|
||||||
|
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:_$!<Other>!$_
|
||||||
|
item14.IMPP;X-SERVICE-TYPE=Yahoo:ymsgr:\,yahoo\;
|
||||||
|
item14.X-ABLabel:_$!<Other>!$_
|
||||||
|
IMPP;X-SERVICE-TYPE=ICQ;type=HOME:aim:.<>\,\;
|
||||||
|
item15.IMPP;X-SERVICE-TYPE=QQ:x-apple:\,qq\;
|
||||||
|
item15.X-ABLabel:_$!<Other>!$_
|
||||||
|
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
|
||||||
|
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
|
||||||
|
ARDwP/AD7l+B//2Q==
|
||||||
|
X-ABShowAs:COMPANY
|
||||||
|
END:VCARD
|
||||||
54
vcard2image.py
Executable file
54
vcard2image.py
Executable file
@@ -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.')
|
||||||
Reference in New Issue
Block a user