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()