| 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): |
|---|
| 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 | |
|---|
| 56 | |
|---|
| 57 | def dump(self, filename): |
|---|
| 58 | self.list_groups() |
|---|
| 59 | query = gdata.contacts.client.ContactsQuery() |
|---|
| 60 | query.max_results = 999 |
|---|
| 61 | feed = self.gd_client.GetContacts(q=query) |
|---|
| 62 | |
|---|
| 63 | with open(filename, "w") as vcf_file: |
|---|
| 64 | for i, entry in enumerate(feed.entry): |
|---|
| 65 | if entry.title.text is None: |
|---|
| 66 | continue # likely to be a collected address |
|---|
| 67 | all_group_ids = [ g.href for g in entry.group_membership_info ] |
|---|
| 68 | if self.maingroup not in all_group_ids: |
|---|
| 69 | # Don't store this contact, it's a collected address |
|---|
| 70 | continue |
|---|
| 71 | print i+1, entry.title.text.encode("utf8") |
|---|
| 72 | #print entry |
|---|
| 73 | contact = self._make_contact(entry) |
|---|
| 74 | if contact is None: |
|---|
| 75 | continue |
|---|
| 76 | vcf_file.write(contact.serialize()) |
|---|
| 77 | vcf_file.write("\r\n") |
|---|
| 78 | |
|---|
| 79 | |
|---|
| 80 | def _make_contact(self, entry): |
|---|
| 81 | """Builds a VCard entry from a Google Atom entry and returns it""" |
|---|
| 82 | |
|---|
| 83 | # Name |
|---|
| 84 | contact = vobject.vCard() |
|---|
| 85 | contact.add("n") |
|---|
| 86 | contact.n.value = vobject.vcard.Name() |
|---|
| 87 | if entry.name.given_name: |
|---|
| 88 | contact.n.value.given = entry.name.given_name.text |
|---|
| 89 | if entry.name.family_name: |
|---|
| 90 | contact.n.value.family = entry.name.family_name.text |
|---|
| 91 | if entry.name.name_prefix: |
|---|
| 92 | contact.n.value.prefix = entry.name.name_prefix.text |
|---|
| 93 | if entry.name.additional_name: |
|---|
| 94 | contact.n.value.additional = entry.name.additional_name.text |
|---|
| 95 | contact.add("fn") |
|---|
| 96 | contact.fn.value = entry.name.full_name.text |
|---|
| 97 | contact.add("name").value = entry.title.text |
|---|
| 98 | |
|---|
| 99 | # Email addresses |
|---|
| 100 | for email in entry.email: |
|---|
| 101 | c_email = contact.add("email") |
|---|
| 102 | c_email.value = email.address |
|---|
| 103 | if email.primary and email.primary == 'true': |
|---|
| 104 | c_email.type_param = "PREF" |
|---|
| 105 | |
|---|
| 106 | # Note |
|---|
| 107 | if entry.content: |
|---|
| 108 | contact.add("note").value = entry.content.text |
|---|
| 109 | |
|---|
| 110 | # Groups |
|---|
| 111 | groups = [] |
|---|
| 112 | for group in entry.group_membership_info: |
|---|
| 113 | if group.href not in self.groups: |
|---|
| 114 | continue |
|---|
| 115 | groups.append(self.groups[group.href]) |
|---|
| 116 | if groups: |
|---|
| 117 | contact.add("categories").value = groups |
|---|
| 118 | |
|---|
| 119 | # Modification time |
|---|
| 120 | contact.add("rev") |
|---|
| 121 | contact.rev.value = entry.updated.text |
|---|
| 122 | |
|---|
| 123 | # Phone |
|---|
| 124 | for phone in entry.phone_number: |
|---|
| 125 | phone_type = urlparse(phone.rel).fragment |
|---|
| 126 | if phone_type == "mobile": |
|---|
| 127 | phone_type = "cell" |
|---|
| 128 | elif phone_type == "work_fax": |
|---|
| 129 | phone_type = "fax" |
|---|
| 130 | tel = contact.add("tel") |
|---|
| 131 | tel.value = phone.text |
|---|
| 132 | tel.type_param = phone_type.upper() |
|---|
| 133 | |
|---|
| 134 | # Organization |
|---|
| 135 | if entry.organization: |
|---|
| 136 | contact.add("org").value = [entry.organization.name.text] |
|---|
| 137 | |
|---|
| 138 | # Birthday |
|---|
| 139 | if entry.birthday: |
|---|
| 140 | contact.add("bday").value = entry.birthday.when |
|---|
| 141 | |
|---|
| 142 | # Address |
|---|
| 143 | for address in entry.structured_postal_address: |
|---|
| 144 | adr = contact.add("adr") |
|---|
| 145 | adr.value = vobject.vcard.Address() |
|---|
| 146 | if address.street: |
|---|
| 147 | adr.value.street = address.street.text, |
|---|
| 148 | if address.city: |
|---|
| 149 | adr.value.city = address.city.text, |
|---|
| 150 | if address.region: |
|---|
| 151 | adr.value.region = address.region.text, |
|---|
| 152 | if address.neighborhood: |
|---|
| 153 | adr.value.code = address.neighborhood.text, |
|---|
| 154 | if address.postcode: |
|---|
| 155 | adr.value.code = address.postcode.text, |
|---|
| 156 | if address.country: |
|---|
| 157 | adr.value.country = address.country.text, |
|---|
| 158 | if address.po_box: |
|---|
| 159 | adr.value.box = address.po_box.text, |
|---|
| 160 | adr_type = urlparse(address.rel).fragment |
|---|
| 161 | adr.type_param = adr_type.upper() |
|---|
| 162 | |
|---|
| 163 | # Photo |
|---|
| 164 | for link in entry.link: |
|---|
| 165 | if link.rel != "http://schemas.google.com/contacts/2008/rel#photo": |
|---|
| 166 | continue |
|---|
| 167 | if "{http://schemas.google.com/g/2005}etag" not in link._other_attributes: |
|---|
| 168 | continue |
|---|
| 169 | hosted_image_binary = self.gd_client.GetPhoto(entry) |
|---|
| 170 | if hosted_image_binary: |
|---|
| 171 | contact.add("photo") |
|---|
| 172 | contact.photo.value = hosted_image_binary |
|---|
| 173 | contact.photo.encoding_param = "b" |
|---|
| 174 | contact.photo.type_param = "image/jpeg" |
|---|
| 175 | #with open("%s.jpg" % entry.title.text, "w") as img: |
|---|
| 176 | # img.write(hosted_image_binary) |
|---|
| 177 | |
|---|
| 178 | # IM |
|---|
| 179 | im_addrs = [] |
|---|
| 180 | for im in entry.im: |
|---|
| 181 | proto = urlparse(im.protocol).fragment |
|---|
| 182 | im_addrs.append( (proto, im.address) ) |
|---|
| 183 | if im_addrs: |
|---|
| 184 | c_im = contact.add("x-kaddressbook-x-imaddress") |
|---|
| 185 | c_im.value = " ".join("(%s)%s" % addr for addr in im_addrs) |
|---|
| 186 | |
|---|
| 187 | # Website |
|---|
| 188 | for website in entry.website: |
|---|
| 189 | contact.add("url").value = website.href |
|---|
| 190 | |
|---|
| 191 | # Display extended properties. |
|---|
| 192 | for extended_property in entry.extended_property: |
|---|
| 193 | if extended_property.value: |
|---|
| 194 | value = extended_property.value |
|---|
| 195 | else: |
|---|
| 196 | value = extended_property.GetXmlBlob() |
|---|
| 197 | print ' Extended Property - %s: %s' % (extended_property.name, value) |
|---|
| 198 | |
|---|
| 199 | return contact |
|---|
| 200 | |
|---|
| 201 | |
|---|
| 202 | def list_groups(self): |
|---|
| 203 | """ |
|---|
| 204 | Lists all Google contact groups and stores them in self.groups. |
|---|
| 205 | The main "My Contacts" group is stored in self.maingroup to filter contacts. |
|---|
| 206 | """ |
|---|
| 207 | query = gdata.service.Query(feed='/m8/feeds/groups/default/full') |
|---|
| 208 | query.max_results = 2 |
|---|
| 209 | feed = self.gd_client.GetGroups() |
|---|
| 210 | for entry in feed.entry: |
|---|
| 211 | if entry.system_group is not None: |
|---|
| 212 | if entry.system_group.id == "Contacts": |
|---|
| 213 | self.maingroup = entry.id.text |
|---|
| 214 | continue |
|---|
| 215 | self.groups[entry.id.text] = entry.title.text |
|---|
| 216 | |
|---|
| 217 | |
|---|
| 218 | |
|---|
| 219 | def parse_opts(): |
|---|
| 220 | usage = "%prog [--user email_address] [--password password] [--filename vcard_file]" |
|---|
| 221 | parser = OptionParser(usage) |
|---|
| 222 | parser.add_option("-u", "--user", help="full email address") |
|---|
| 223 | parser.add_option("-p", "--password") |
|---|
| 224 | parser.add_option("-f", "--filename", help="VCard file to write to") |
|---|
| 225 | opts, args = parser.parse_args() |
|---|
| 226 | while not opts.user: |
|---|
| 227 | opts.user = raw_input("Please enter your username: ") |
|---|
| 228 | while not opts.password: |
|---|
| 229 | print "Please enter your password: ", |
|---|
| 230 | opts.password = getpass.getpass() |
|---|
| 231 | if not opts.password: |
|---|
| 232 | print "Password cannot be blank." |
|---|
| 233 | while not opts.filename: |
|---|
| 234 | opts.filename = raw_input("Please enter the VCard file: ") |
|---|
| 235 | return opts |
|---|
| 236 | |
|---|
| 237 | |
|---|
| 238 | def main(): |
|---|
| 239 | opts = parse_opts() |
|---|
| 240 | |
|---|
| 241 | try: |
|---|
| 242 | contacts = Contacts(opts.user, opts.password) |
|---|
| 243 | except gdata.service.BadAuthentication: |
|---|
| 244 | print 'Invalid user credentials given.' |
|---|
| 245 | return |
|---|
| 246 | |
|---|
| 247 | contacts.dump(opts.filename) |
|---|
| 248 | |
|---|
| 249 | |
|---|
| 250 | |
|---|
| 251 | if __name__ == '__main__': |
|---|
| 252 | main() |
|---|