Back to my blog Back to my projects

Scripts

source: gmail-contacts-to-vcf.py @ fc7bbfc

Revision fc7bbfc, 8.6 KB checked in by Aurélien Bompard <aurelien@…>, 15 months ago (diff)

gmail-contacts-to-vcf: add an option to save profile pictures

  • Property mode set to 100755
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3u"""
4GMail contacts to VCF
5---------------------
6
7Exports Gmail contacts to a vcard (VCF) file. This can be done through the
8Gmail web interface, but this script is more complete (more fields are
9exported) and can be run automatically and periodically, for example as a
10backup 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
21from __future__ import with_statement # compat python 2.5
22
23import os
24import sys
25import getpass
26import base64
27from urlparse import urlparse
28from cStringIO import StringIO
29from optparse import OptionParser
30
31import atom
32import gdata.contacts
33import gdata.contacts.service
34import gdata.contacts.client
35import vobject
36
37
38
39class 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
224def 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
244def 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
257if __name__ == '__main__':
258    main()
Note: See TracBrowser for help on using the repository browser.