Skip to content

Instantly share code, notes, and snippets.

@mislav
Created January 22, 2014 02:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mislav/8552653 to your computer and use it in GitHub Desktop.
Save mislav/8552653 to your computer and use it in GitHub Desktop.
iPhone OS 4.1 Address Book to .vcf converter
# 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
@ptrcarta
Copy link

thx this saved me a couple of hours of work

@zenden2k
Copy link

zenden2k commented Nov 4, 2019

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