1 """
2 Syncronize with Google Contacts
3
4 This file is part of Pisi.
5
6 Google provides a really straight forward API for accessing their contact information (gdata).
7 U{http://code.google.com/p/gdata-python-client/}. It is used for this implementation - the site package
8 has to be installed.
9
10 Pisi is free software: you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation, either version 3 of the License, or
13 (at your option) any later version.
14
15 Pisi is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with Pisi. If not, see <http://www.gnu.org/licenses/>.
22 """
23
24 import sys,os,datetime
25
26 sys.path.insert(0,os.path.abspath(__file__+"/../.."))
27 from contacts import contacts
28 from pisiconstants import *
29 import pisiprogress
30 import pisitools
31
32 import atom
33 import gdata.contacts
34 import gdata.contacts.service
35
37 """
38 The implementation of the interface L{contacts.AbstractContactSynchronizationModule} for the Google Contacts backend
39 """
40
41 - def __init__( self, modulesString, config, configsection, folder, verbose=False, soft=False):
42 """
43 Constructor
44
45 Super class constructor (L{contacts.AbstractContactSynchronizationModule.__init__}) is called.
46 Local variables are initialized.
47 The settings from the configuration file are loaded.
48 The connection to the Google Gdata backend is established.
49
50 @param modulesString: is a small string to help you make a unique id. It is the two modules configuration-names concatinated.
51 @param config: is the configuration from ~/.pisi/conf. Use like config.get(configsection,'user')
52 @param configsection: is the configuration from ~/.pisi/conf. Use like config.get(configsection,'user')
53 @param folder: is a string with the location for where your module can save its own files if necessary
54 @param verbose: should make your module "talk" more
55 @param soft: should tell you if you should make changes (ie. save)
56 """
57 contacts.AbstractContactSynchronizationModule.__init__(self, verbose, soft, modulesString, config, configsection, "Google contacts")
58 self.verbose = verbose
59 self._user = config.get(configsection,'user')
60 self._password = config.get(configsection,'password')
61 pisiprogress.getCallback().verbose("Google contacts module loaded")
62
63 self._idMappingInternalGlobal = {}
64 self._idMappingGlobalInternal = {}
65
66 self._gd_client = gdata.contacts.service.ContactsService()
67 self._gd_client.email = self._user
68 self._gd_client.password = self._password
69 self._gd_client.source = GOOGLE_CONTACTS_APPNAME
70 self._gd_client.ProgrammaticLogin()
71
73 """
74 Takes the components of a Google Title apart (all names)
75
76 The whole thing is about guessing - check code for details.
77 """
78 pisiprogress.getCallback().verbose("Google Contacts: Loading")
79 title, first, last, middle = pisitools.parseFullName(gtitle)
80 atts['title'] = title
81 atts['firstname'] = first
82 atts['lastname'] = last
83 atts['middlename'] = middle
84
85 - def _unpackPostalCity(self, line):
86 """
87 Takes one address line apart and identifies postal code and locality
88
89 This is really tricky - you never know how many parts belong to which side - and it's all in one line.
90 We do it this way:
91 - only one item indicates city only
92 - two items one each
93 - more than two - depending on the length of the first item, another item is applied to the postal code; all the remaining stuff is city
94 """
95 items = line.strip().split(' ')
96 if len(items) == 1:
97 city = items[0]
98 postalCode = ''
99 elif len(items) == 2:
100 postalCode = items[0]
101 city = items[1]
102 else:
103 if len(items[0]) > 4:
104 postalCode = items[0]
105 else:
106 postalCode = items[0] + " " + items[1]
107 city = line[len(postalCode):].strip()
108
109 return city, postalCode
110
111 - def _unpackGooglePostalAddress(self, atts, addressText, type):
112 """
113 Takes the components of a Google address apart
114
115 The whole thing is about guessing - check code for details.
116 """
117 text = addressText.strip()
118 lines = text.split("\n")
119 try:
120 street = lines[0].strip()
121 if type == 'home':
122 atts['homeStreet'] = street
123 elif type == 'work':
124 atts['businessStreet'] = street
125 del lines[0]
126 city, postalCode = self._unpackPostalCity(lines[0])
127 if type == 'home':
128 atts['homeCity'] = city
129 if postalCode:
130 atts['homePostalCode'] = postalCode
131 elif type == 'work':
132 atts['businessCity'] = city
133 if postalCode:
134 atts['businessPostalCode'] = postalCode
135 del lines[0]
136 if len(lines)>1:
137 state = lines[0].strip()
138 if type == 'home':
139 atts['homeState'] = state
140 elif type == 'work':
141 atts['businessState'] = state
142 del lines[0]
143 country = lines[0].strip()
144 if type == 'home':
145 atts['homeCountry'] = country
146 elif type == 'work':
147 atts['businessCountry'] = country
148
149 except IndexError:
150 pass
151
153 """
154 Load all data from backend
155
156 A single query is performed and the result set is parsed afterwards.
157 """
158 query = gdata.contacts.service.ContactsQuery()
159 query.max_results = GOOGLE_CONTACTS_MAXRESULTS
160 feed = self._gd_client.GetContactsFeed(query.ToUri())
161 for i, entry in enumerate(feed.entry):
162 atts = {}
163 if not entry.title or not entry.title.text:
164 pisiprogress.getCallback().verbose('** In Googlecontacts account is an entry with no title - I cannot process this account and will skip it.')
165 continue
166 self._unpackGoogleTitle(atts, entry.title.text.decode("utf-8"))
167
168 if len(entry.email) > 0:
169 email = entry.email[0]
170
171 if email.primary and email.primary == 'true':
172 if email.address:
173 atts['email'] = email.address.decode("utf-8")
174
175 for phone in entry.phone_number:
176 if phone.rel == gdata.contacts.PHONE_HOME:
177 atts['phone'] = phone.text
178 if phone.rel == gdata.contacts.PHONE_MOBILE:
179 atts['mobile'] = phone.text
180 if phone.rel == gdata.contacts.PHONE_WORK:
181 atts['officePhone'] = phone.text
182 if phone.rel == gdata.contacts.PHONE_WORK_FAX:
183 atts['fax'] = phone.text
184
185 for address in entry.postal_address:
186 if address.text:
187 value = address.text.decode("utf-8")
188 type = None
189 if address.rel == gdata.contacts.REL_HOME:
190 type = 'home'
191 elif address.rel == gdata.contacts.REL_WORK:
192 type = 'work'
193 if type:
194 self._unpackGooglePostalAddress(atts, value, type)
195
196 if entry.organization:
197 if entry.organization.org_name and entry.organization.org_name.text:
198 atts['businessOrganisation'] = entry.organization.org_name.text.decode("utf-8")
199 if entry.organization.org_title and entry.organization.org_title.text:
200 atts['businessDepartment'] = entry.organization.org_title.text.decode("utf-8")
201
202 id = contacts.assembleID(atts)
203 c = contacts.Contact(id, atts)
204 self._allContacts[id] = c
205
206 self._idMappingGlobalInternal[id] = entry.GetEditLink().href
207 self._idMappingInternalGlobal[entry.GetEditLink().href] = id
208
210 """
211 Assembles all information from a contact entry for the packed version of a title in google contacts
212 """
213 return pisitools.assembleFullName(contactEntry)
214
216 """
217 Integrates all saving tranformations for phone attributes
218 """
219 if contact.attributes.has_key('phone'):
220 phone = contact.attributes['phone']
221 if phone and phone != '':
222 new_contact.phone_number.append(gdata.contacts.PhoneNumber(text=phone, rel=gdata.contacts.PHONE_HOME))
223 if contact.attributes.has_key('mobile'):
224 mobile = contact.attributes['mobile']
225 if mobile and mobile != '':
226 new_contact.phone_number.append(gdata.contacts.PhoneNumber(text=mobile, rel=gdata.contacts.PHONE_MOBILE))
227 if contact.attributes.has_key('officePhone'):
228 phone = contact.attributes['officePhone']
229 if phone and phone != '':
230 new_contact.phone_number.append(gdata.contacts.PhoneNumber(text=phone, rel=gdata.contacts.PHONE_WORK))
231 if contact.attributes.has_key('fax'):
232 fax = contact.attributes['fax']
233 if fax and fax != '':
234 new_contact.phone_number.append(gdata.contacts.PhoneNumber(text=fax, rel=gdata.contacts.PHONE_WORK_FAX))
235
237 """
238 Integrates all saving tranformations for address attributes
239 """
240
241 if contact.attributes.has_key('homeCity'):
242 homeCity = contact.attributes['homeCity']
243 if homeCity and homeCity != None:
244 text = ''
245 if contact.attributes.has_key('homeStreet'):
246 homeStreet = contact.attributes['homeStreet']
247 if homeStreet and homeStreet != '':
248 text += homeStreet + "\n"
249 if contact.attributes.has_key('homePostalCode'):
250 homePostalCode = contact.attributes['homePostalCode']
251 if homePostalCode and homePostalCode != '':
252 text += homePostalCode + " "
253 text += homeCity +"\n"
254 if contact.attributes.has_key('homeState'):
255 homeState = contact.attributes['homeState']
256 if homeState and homeState != '':
257 text += homeState + "\n"
258 if contact.attributes.has_key('homeCountry'):
259 homeCountry = contact.attributes['homeCountry']
260 if homeCountry and homeCountry != '':
261 text += homeCountry + "\n"
262 new_contact.postal_address.append(gdata.contacts.PostalAddress(text=text, rel=gdata.contacts.REL_HOME))
263
264 if contact.attributes.has_key('businessCity'):
265 businessCity = contact.attributes['businessCity']
266 if businessCity and businessCity != None:
267 text = ''
268 if contact.attributes.has_key('businessStreet'):
269 businessStreet = contact.attributes['businessStreet']
270 if businessStreet and businessStreet != '':
271 text += businessStreet + "\n"
272 if contact.attributes.has_key('businessPostalCode'):
273 businessPostalCode = contact.attributes['businessPostalCode']
274 if businessPostalCode and businessPostalCode != '':
275 text += businessPostalCode + " "
276 text += businessCity +"\n"
277 if contact.attributes.has_key('businessState'):
278 businessState = contact.attributes['businessState']
279 if businessState and businessState != '':
280 text += businessState + "\n"
281 if contact.attributes.has_key('businessCountry'):
282 businessCountry = contact.attributes['businessCountry']
283 if businessCountry and businessCountry != '':
284 text += businessCountry + "\n"
285 new_contact.postal_address.append(gdata.contacts.PostalAddress(text=text, rel=gdata.contacts.REL_WORK))
286
288 """
289 Creates an entry for business organzation und unit.
290 """
291 if c.attributes.has_key('businessOrganisation'):
292 o = c.attributes['businessOrganisation']
293 if o and o != '':
294 ou = None
295 if c.attributes.has_key('businessDepartment'):
296 ou = c.attributes['businessDepartment']
297
298 new_contact.organization = gdata.contacts.Organization(org_name = gdata.contacts.OrgName(o), org_title = gdata.contacts.OrgTitle(ou))
299
301 """
302 Save all changes to Google contacts that have come by
303 """
304 contact = self.getContact(id)
305 new_contact = gdata.contacts.ContactEntry(title=atom.Title(text=self._assembleTitle(contact)))
306 if contact.attributes.has_key('email'):
307 email = contact.attributes['email']
308 if email and email != '':
309 new_contact.email.append(gdata.contacts.Email(address=email,primary='true', rel=gdata.contacts.REL_HOME))
310 self._savePhones(contact, new_contact)
311 self._saveAddresses(contact, new_contact)
312 self._saveBusinessDetails(contact, new_contact)
313
314 contact_entry = self._gd_client.CreateContact(new_contact)
315
317 """
318 Finally deletes the contact entry identified by its ID in the google contact backend
319 """
320 link = self._idMappingGlobalInternal[id]
321 self._gd_client.DeleteContact(link)
322
324 """
325 Applies changes for an entry to the Google contacts backend
326
327 In order to keep things simple the old value is simply erased using the method L{_saveOperationDelete} and a new one is inserted (L{_saveOperationAdd}).
328 """
329 self._saveOperationDelete(id)
330 self._saveOperationAdd(id)
331
354