Skip to content

Instantly share code, notes, and snippets.

@ijstokes
Last active September 23, 2025 14:14
Show Gist options
  • Select an option

  • Save ijstokes/612483dc4988b93e8d149bd93ff31420 to your computer and use it in GitHub Desktop.

Select an option

Save ijstokes/612483dc4988b93e8d149bd93ff31420 to your computer and use it in GitHub Desktop.
Use vobject to convert VCF file exported from Apple Contacts into CSV format ready for Outlook import
'''
Couldn't believe there wasn't a (working) tool to do this already.
This was quickly thrown together for my own purposes, did the job.
It has some pieces that are quite specific to my circumstance.
(for example, I worked at BCG and have lots of contacts @bcg.com
where email addresses are last.first)
YMMV, but hopefully this helps someone else out.
I put this in the Public Domain.
Ian Stokes-Rees, 2025
'''
import csv
import sys
import vobject
def vcf_to_csv(vcf_file, csv_file):
# open input VCF file
with open(vcf_file, 'r') as vcf:
# open output CSV file
with open(csv_file, 'w', newline='') as csvfile:
# "Just the basics" for the output
fieldnames = ['Last Name', 'First Name', 'Email', 'Phone', 'Address', 'Org']
# Add the headers
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
# Get a generator object to iterate over from the VCF
vcards = vobject.readComponents(vcf)
for card in vcards:
# Try to get the lastname & firstname from the N entry
try:
last_name = str(card.n.value).split()[-1] if hasattr(card, 'n') else ''
first_name = str(card.n.value).split()[0] if hasattr(card, 'n') else ''
# If that fails, assume it is a business and use the FN entry
except IndexError as e:
print(f'RETRY: card\n{card.prettyPrint()}\n-------------------')
last_name = card.fn.value if hasattr(card, 'fn') else ''
first_name = ''
email = card.email.value if hasattr(card, 'email') else ''
phone = card.tel.value if hasattr(card, 'tel') else ''
address = card.adr.value if hasattr(card, 'adr') else ''
org = card.org.value[0] if hasattr(card, 'org') else ''
# Some entries are backwards, signaled by a comma in the firstname entry, so remove & swap
if ',' in first_name:
new_last = first_name.replace(',','')
first_name = last_name
last_name = new_last
# Some entries have email as the lastname, so try to extract a name from the email address
if '@' in last_name:
username, host = last_name.split('@')
print(f'DEBUG: host {host} org {org}')
if '.' in username:
if 'bcg' in host.lower():
last_name = username.split('.')[0]
first_name = username.split('.')[1]
else:
first_name = username.split('.')[0]
last_name = username.split('.')[1]
# If there's no ORG but there is an email address, try to figure out org name, excluding the GMails of the world
if not org and email:
try:
username, domain = email.split('@')
company = domain.split('.')[-2]
# Stupid way to try and deal with 2-part TLDs
if company.lower() in ['org', 'ac', 'uk', 'gov']:
company = domain.split('.')[-3]
except:
print(f'DEBUG: invalid email {email}')
continue
# skip the GMails of the world
if company.lower() not in ['gmail', 'yahoo', 'outlook', 'hotmail', 'googlemail', 'verizon', 'me', 'icloud']:
# Heuristic that 3-5 character names are acronyms, so use UPPER, anything Title
if len(company) < 6:
org = company.upper()
else:
org = company.title()
print(f'UPDATED: org {org} from domain {domain}')
# output a single CSV row entry for the VCard
writer.writerow({'Last Name': last_name.title(),
'First Name': first_name.title(),
'Email': email,
'Phone': phone,
'Address': address,
'Org': org})
# good 'ol sys.argv
vcf_to_csv(sys.argv[1], sys.argv[2])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment