Created
January 22, 2014 02:45
-
-
Save mislav/8552653 to your computer and use it in GitHub Desktop.
iPhone OS 4.1 Address Book to .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
# Usage: addressbook2vcard /path/to/AddressBook.sqlitedb | |
# | |
# Reads contact info from iPhone OS 4.1 Address Book database and outputs it as | |
# VCard 3.0 for easy import in other programs. | |
require 'sqlite3' | |
String.class_eval do | |
def present? | |
!strip.empty? | |
end | |
end | |
NilClass.class_eval do | |
def present?() false end | |
end | |
db = SQLite3::Database.new(ARGV[0]) | |
EPOCH = Time.utc(2001, 1, 1) | |
labels = db.execute("SELECT * FROM ABMultiValueLabel") | |
LABELS = Hash[*labels.map.with_index { |row, i| [i+1, row[0]] }.flatten] | |
LABEL_MOBILE = '_$!<Mobile>!$_' | |
LABEL_IPHONE = 'iPhone' | |
LABEL_WORK = "_$!<Work>!$_" | |
LABEL_MAIN = "_$!<Main>!$_" | |
LABEL_HOME = "_$!<Home>!$_" | |
keys = db.execute("SELECT * FROM ABMultiValueEntryKey") | |
KEYS = Hash[*keys.map.with_index { |row, i| [i+1, row[0].to_sym] }.flatten] | |
FIELDS = [ | |
:ROWID, | |
:First, | |
:Last, | |
:Middle, | |
:Organization, | |
:Department, | |
:Note, | |
:Birthday, | |
:JobTitle, | |
:Nickname, | |
] | |
Person = Struct.new(*FIELDS.map(&:downcase)) do | |
attr_reader :telephones, :emails, :address | |
def initialize(*) | |
super | |
@telephones = {} | |
@emails = {} | |
@address = {} | |
@first_address = true | |
end | |
def id() rowid.to_i end | |
def full_name | |
[first, middle, last].select(&:present?).join(' ') | |
end | |
def born_at | |
EPOCH + (12 * 60 * 60) + birthday.to_i | |
end | |
def inspect | |
%(#<Person: %p (%d)>) % [ | |
full_name, | |
id, | |
] | |
end | |
def add_telephone(value, label) | |
telephones[label] = value if value.present? | |
end | |
def add_email(value, label) | |
emails[label] = value if value.present? | |
end | |
def add_address_part(key, value) | |
@first_address = false if address.has_key?(key) | |
return unless @first_address | |
address[key] = value if value.present? | |
end | |
end | |
VCardFormatter = Struct.new(:person) do | |
def write(io) | |
io.puts "BEGIN:VCARD" | |
io.puts "N:%s;%s;%s;;" % [ | |
escape(person.last), | |
escape(person.first), | |
escape(person.middle), | |
] | |
io.puts "FN:%s" % [ escape(person.full_name) ] if person.full_name.present? | |
if person.nickname.present? | |
io.puts "NICKNAME:%s" % [ escape(person.nickname) ] | |
end | |
if person.organization.present? | |
io.puts "ORG:%s;%s" % [ | |
escape(person.organization), | |
escape(person.department), | |
] | |
end | |
if person.jobtitle.present? | |
io.puts "TITLE:%s" % [ escape(person.jobtitle) ] | |
end | |
item_num = 0 | |
email_pref = tel_pref = ';type=pref' | |
for label, email in person.emails | |
type = case label | |
when LABEL_HOME, LABEL_MAIN then 'HOME' | |
when LABEL_WORK then 'WORK' | |
end | |
if type | |
io.puts "EMAIL;type=INTERNET;type=%s%s:%s" % [ | |
type, | |
email_pref, | |
escape(email), | |
] | |
else | |
item_num += 1 | |
io.puts "item%d.EMAIL;type=INTERNET%s:%s" % [ | |
item_num, | |
email_pref, | |
escape(email), | |
] | |
io.puts "item%d.X-ABLabel:%s" % [ | |
item_num, | |
escape(label), | |
] | |
end | |
email_pref = '' | |
end | |
for label, tel in person.telephones | |
type = case label | |
when LABEL_HOME, LABEL_MAIN then 'HOME' | |
when LABEL_WORK then 'WORK' | |
when LABEL_IPHONE then 'IPHONE;type=CELL' | |
when LABEL_MOBILE then 'CELL' | |
end | |
if type | |
io.puts "TEL;type=%s;type=VOICE%s:%s" % [ | |
type, | |
tel_pref, | |
escape(tel), | |
] | |
else | |
item_num += 1 | |
io.puts "item%d.TEL;type=VOICE%s:%s" % [ | |
item_num, | |
tel_pref, | |
escape(tel), | |
] | |
io.puts "item%d.X-ABLabel:%s" % [ | |
item_num, | |
escape(label), | |
] | |
end | |
tel_pref = '' | |
end | |
unless person.address.empty? | |
# TODO: :Street, :Country, :ZIP, :City, :CountryCode, :State | |
end | |
if person.birthday.present? | |
io.puts "BDAY:" + person.born_at.strftime('%Y-%m-%d') | |
end | |
if person.note.present? | |
note = person.note.dup | |
note.gsub!(/^X-.+$/) { | |
io.puts($&) | |
'' | |
} | |
note.strip! | |
io.puts "NOTE:%s" % [ escape(note) ] unless note.empty? | |
end | |
io.puts "END:VCARD" | |
end | |
def escape(str) | |
str.to_s.gsub(';', '\;').gsub(/\r?\n/, '\n') | |
end | |
end | |
people = {} | |
db.execute("SELECT #{FIELDS.join(',')} FROM ABPerson") do |row| | |
person = Person.new(*row) | |
people[person.id] = person | |
end | |
db.execute("SELECT record_id,property,label,value FROM ABMultiValue") do |row| | |
person_id, type, label_id, value = row | |
label = LABELS.fetch(label_id) | |
person = people.fetch(person_id) | |
case type | |
when 3 | |
person.add_telephone(value, label) | |
when 4 | |
person.add_email(value, label) | |
end | |
end | |
db.execute("SELECT parent_id,key,value FROM ABMultiValueEntry") do |row| | |
person_id, key_id, value = row | |
key = KEYS.fetch(key_id) | |
begin | |
person = people.fetch(person_id) | |
person.add_address_part(key, value) | |
rescue KeyError | |
warn "person not found: %d" % person_id | |
end | |
end | |
people.each do |_, person| | |
vcard = VCardFormatter.new(person) | |
vcard.write($stdout) | |
end |
Thank you very much!
I have exported contacts from my old broken iphone 4 and converted using your script.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
thx this saved me a couple of hours of work