| 1 |
#!/usr/bin/env python |
| 2 |
# -*- coding: utf-8 -*- |
| 3 |
u""" |
| 4 |
GMail contacts to VCF |
| 5 |
--------------------- |
| 6 |
|
| 7 |
Exports Gmail contacts to a vcard (VCF) file. This can be done through the |
| 8 |
Gmail web interface, but this script is more complete (more fields are |
| 9 |
exported) and can be run automatically and periodically, for example as a |
| 10 |
backup system. |
| 11 |
|
| 12 |
|
| 13 |
.. :Authors: |
| 14 |
Aurélien Bompard <aurelien@bompard.org> <http://aurelien.bompard.org> |
| 15 |
|
| 16 |
.. :License: |
| 17 |
GNU GPL v3 or later |
| 18 |
|
| 19 |
""" |
| 20 |
|
| 21 |
from __future__ import with_statement # compat python 2.5 |
| 22 |
|
| 23 |
import os |
| 24 |
import sys |
| 25 |
import getpass |
| 26 |
import base64 |
| 27 |
from urlparse import urlparse |
| 28 |
from cStringIO import StringIO |
| 29 |
from optparse import OptionParser |
| 30 |
|
| 31 |
import atom |
| 32 |
import gdata.contacts |
| 33 |
import gdata.contacts.service |
| 34 |
import gdata.contacts.client |
| 35 |
import vobject |
| 36 |
|
| 37 |
|
| 38 |
|
| 39 |
class Contacts(object): |
| 40 |
|
| 41 |
def __init__(self, email, password, picsdir=None): |
| 42 |
""" |
| 43 |
Takes an email and password corresponding to a gmail account to |
| 44 |
connect to the Contacts feed. |
| 45 |
|
| 46 |
:param email: The e-mail address of the account to use. |
| 47 |
:param password: The password corresponding to the account specified by |
| 48 |
the email parameter. |
| 49 |
""" |
| 50 |
self.gd_client = gdata.contacts.client.ContactsClient() |
| 51 |
self.gd_client.source = os.path.basename(sys.argv[0]) |
| 52 |
self.gd_client.ClientLogin(email, password, self.gd_client.source) |
| 53 |
self.groups = {} |
| 54 |
self.maingroup = None |
| 55 |
self.picsdir = picsdir |
| 56 |
if self.picsdir and not os.path.exists(self.picsdir): |
| 57 |
os.makedirs(self.picsdir) |
| 58 |
|
| 59 |
|
| 60 |
def dump(self, filename): |
| 61 |
self.list_groups() |
| 62 |
query = gdata.contacts.client.ContactsQuery() |
| 63 |
query.max_results = 999 |
| 64 |
feed = self.gd_client.GetContacts(q=query) |
| 65 |
|
| 66 |
with open(filename, "w") as vcf_file: |
| 67 |
for i, entry in enumerate(feed.entry): |
| 68 |
if entry.title.text is None: |
| 69 |
continue # likely to be a collected address |
| 70 |
all_group_ids = [ g.href for g in entry.group_membership_info ] |
| 71 |
if self.maingroup not in all_group_ids: |
| 72 |
# Don't store this contact, it's a collected address |
| 73 |
continue |
| 74 |
print i+1, entry.title.text.encode("utf8") |
| 75 |
#print entry |
| 76 |
contact = self._make_contact(entry) |
| 77 |
if contact is None: |
| 78 |
continue |
| 79 |
vcf_file.write(contact.serialize()) |
| 80 |
vcf_file.write("\r\n") |
| 81 |
|
| 82 |
|
| 83 |
def _make_contact(self, entry): |
| 84 |
"""Builds a VCard entry from a Google Atom entry and returns it""" |
| 85 |
|
| 86 |
# Name |
| 87 |
contact = vobject.vCard() |
| 88 |
contact.add("n") |
| 89 |
contact.n.value = vobject.vcard.Name() |
| 90 |
if entry.name.given_name: |
| 91 |
contact.n.value.given = entry.name.given_name.text |
| 92 |
if entry.name.family_name: |
| 93 |
contact.n.value.family = entry.name.family_name.text |
| 94 |
if entry.name.name_prefix: |
| 95 |
contact.n.value.prefix = entry.name.name_prefix.text |
| 96 |
if entry.name.additional_name: |
| 97 |
contact.n.value.additional = entry.name.additional_name.text |
| 98 |
contact.add("fn") |
| 99 |
contact.fn.value = entry.name.full_name.text |
| 100 |
contact.add("name").value = entry.title.text |
| 101 |
|
| 102 |
# Email addresses |
| 103 |
for email in entry.email: |
| 104 |
c_email = contact.add("email") |
| 105 |
c_email.value = email.address |
| 106 |
if email.primary and email.primary == 'true': |
| 107 |
c_email.type_param = "PREF" |
| 108 |
|
| 109 |
# Note |
| 110 |
if entry.content: |
| 111 |
contact.add("note").value = entry.content.text |
| 112 |
|
| 113 |
# Groups |
| 114 |
groups = [] |
| 115 |
for group in entry.group_membership_info: |
| 116 |
if group.href not in self.groups: |
| 117 |
continue |
| 118 |
groups.append(self.groups[group.href]) |
| 119 |
if groups: |
| 120 |
contact.add("categories").value = groups |
| 121 |
|
| 122 |
# Modification time |
| 123 |
contact.add("rev") |
| 124 |
contact.rev.value = entry.updated.text |
| 125 |
|
| 126 |
# Phone |
| 127 |
for phone in entry.phone_number: |
| 128 |
phone_type = urlparse(phone.rel).fragment |
| 129 |
if phone_type == "mobile": |
| 130 |
phone_type = "cell" |
| 131 |
elif phone_type == "work_fax": |
| 132 |
phone_type = "fax" |
| 133 |
tel = contact.add("tel") |
| 134 |
tel.value = phone.text |
| 135 |
tel.type_param = phone_type.upper() |
| 136 |
|
| 137 |
# Organization |
| 138 |
if entry.organization: |
| 139 |
contact.add("org").value = [entry.organization.name.text] |
| 140 |
|
| 141 |
# Birthday |
| 142 |
if entry.birthday: |
| 143 |
contact.add("bday").value = entry.birthday.when |
| 144 |
|
| 145 |
# Address |
| 146 |
for address in entry.structured_postal_address: |
| 147 |
adr = contact.add("adr") |
| 148 |
adr.value = vobject.vcard.Address() |
| 149 |
if address.street: |
| 150 |
adr.value.street = address.street.text, |
| 151 |
if address.city: |
| 152 |
adr.value.city = address.city.text, |
| 153 |
if address.region: |
| 154 |
adr.value.region = address.region.text, |
| 155 |
if address.neighborhood: |
| 156 |
adr.value.code = address.neighborhood.text, |
| 157 |
if address.postcode: |
| 158 |
adr.value.code = address.postcode.text, |
| 159 |
if address.country: |
| 160 |
adr.value.country = address.country.text, |
| 161 |
if address.po_box: |
| 162 |
adr.value.box = address.po_box.text, |
| 163 |
adr_type = urlparse(address.rel).fragment |
| 164 |
adr.type_param = adr_type.upper() |
| 165 |
|
| 166 |
# Photo |
| 167 |
for link in entry.link: |
| 168 |
if link.rel != "http://schemas.google.com/contacts/2008/rel#photo": |
| 169 |
continue |
| 170 |
if "{http://schemas.google.com/g/2005}etag" not in link._other_attributes: |
| 171 |
continue |
| 172 |
hosted_image_binary = self.gd_client.GetPhoto(entry) |
| 173 |
if hosted_image_binary: |
| 174 |
contact.add("photo") |
| 175 |
contact.photo.value = hosted_image_binary |
| 176 |
contact.photo.encoding_param = "b" |
| 177 |
contact.photo.type_param = "image/jpeg" |
| 178 |
if self.picsdir: |
| 179 |
with open(os.path.join(self.picsdir, |
| 180 |
"%s.jpg" % entry.title.text), "w") as img: |
| 181 |
img.write(hosted_image_binary) |
| 182 |
|
| 183 |
# IM |
| 184 |
im_addrs = [] |
| 185 |
for im in entry.im: |
| 186 |
proto = urlparse(im.protocol).fragment |
| 187 |
im_addrs.append( (proto, im.address) ) |
| 188 |
if im_addrs: |
| 189 |
c_im = contact.add("x-kaddressbook-x-imaddress") |
| 190 |
c_im.value = " ".join("(%s)%s" % addr for addr in im_addrs) |
| 191 |
|
| 192 |
# Website |
| 193 |
for website in entry.website: |
| 194 |
contact.add("url").value = website.href |
| 195 |
|
| 196 |
# Display extended properties. |
| 197 |
for extended_property in entry.extended_property: |
| 198 |
if extended_property.value: |
| 199 |
value = extended_property.value |
| 200 |
else: |
| 201 |
value = extended_property.GetXmlBlob() |
| 202 |
print ' Extended Property - %s: %s' % (extended_property.name, value) |
| 203 |
|
| 204 |
return contact |
| 205 |
|
| 206 |
|
| 207 |
def list_groups(self): |
| 208 |
""" |
| 209 |
Lists all Google contact groups and stores them in self.groups. |
| 210 |
The main "My Contacts" group is stored in self.maingroup to filter contacts. |
| 211 |
""" |
| 212 |
query = gdata.service.Query(feed='/m8/feeds/groups/default/full') |
| 213 |
query.max_results = 2 |
| 214 |
feed = self.gd_client.GetGroups() |
| 215 |
for entry in feed.entry: |
| 216 |
if entry.system_group is not None: |
| 217 |
if entry.system_group.id == "Contacts": |
| 218 |
self.maingroup = entry.id.text |
| 219 |
continue |
| 220 |
self.groups[entry.id.text] = entry.title.text |
| 221 |
|
| 222 |
|
| 223 |
|
| 224 |
def parse_opts(): |
| 225 |
usage = "%prog [--user email_address] [--password password] [--filename vcard_file]" |
| 226 |
parser = OptionParser(usage) |
| 227 |
parser.add_option("-u", "--user", help="full email address") |
| 228 |
parser.add_option("-p", "--password") |
| 229 |
parser.add_option("-f", "--filename", help="VCard file to write to") |
| 230 |
parser.add_option("--pics", help="dump contact pictures in this folder") |
| 231 |
opts, args = parser.parse_args() |
| 232 |
while not opts.user: |
| 233 |
opts.user = raw_input("Please enter your username: ") |
| 234 |
while not opts.password: |
| 235 |
print "Please enter your password: ", |
| 236 |
opts.password = getpass.getpass() |
| 237 |
if not opts.password: |
| 238 |
print "Password cannot be blank." |
| 239 |
while not opts.filename: |
| 240 |
opts.filename = raw_input("Please enter the VCard file: ") |
| 241 |
return opts |
| 242 |
|
| 243 |
|
| 244 |
def main(): |
| 245 |
opts = parse_opts() |
| 246 |
|
| 247 |
try: |
| 248 |
contacts = Contacts(opts.user, opts.password, opts.pics) |
| 249 |
except gdata.service.BadAuthentication: |
| 250 |
print 'Invalid user credentials given.' |
| 251 |
return |
| 252 |
|
| 253 |
contacts.dump(opts.filename) |
| 254 |
|
| 255 |
|
| 256 |
|
| 257 |
if __name__ == '__main__': |
| 258 |
main() |