def comparevcards(vcard, localvcard, auth): """ look for local version of this vcard and compare should return tuple(action, xml, id, name) """ id = vcard.uid.value name = vcard.fn.value # compare if vcard.serialize() == localvcard.serialize(): return localvcard # compare REV strings if 'rev' in vcard.contents and 'rev' in localvcard.contents: # TODO google returns utc times - should make this timezone aware vcardrev = datetime.strptime(vcard.rev.value, _dtformat) localvcardrev = datetime.strptime(localvcard.rev.value, _dtformat) logger.debug(u'Comparing revision times: R{0}, L{1}.'.format( vcard.rev.value, localvcard.rev.value)) if vcardrev > localvcardrev: # write new local vcard logger.info(u'Local version of contact "{0}" updated.'.format(name)) return vcard elif localvcardrev > vcardrev: # update remote xmlobj = vcf2xml.toXml(localvcard) xml = vcf2xml.ET.tostring(xmlobj, encoding=_encoding) response = sendcontact(options['user'], auth, xml, id) localvcard = xml2vcf.readXml(response)[0] logger.info(u'Remote version of contact "{0}" updated.'.format(name)) return localvcard else: logger.debug(u'Revision times are equal.') else: # use default resolution logger.debug(u'One or both versions missing revision time, ' + 'choosing default resolution method.') r = u'Contact "{0}" differs: '.format(name) if options['defaultresolution'] == 'prefer local' \ or runoptions.preferlocal is True: xmlobj = vcf2xml.toXml(localvcard) xml = vcf2xml.ET.tostring(xmlobj, encoding=_encoding) response = sendcontact(options['user'], auth, xml, id) localvcard = xml2vcf.readXml(response)[0] logger.info(r + u'remote version updated.') return localvcard elif options['defaultresolution'] == 'prefer remote' \ or runoptions.preferlocal is False: logger.info(r + u'local version updated.') return vcard elif options['defaultresolution'] == 'do nothing': logger.warning(r + u'unable to resolve.') versions = u'Local version:\n{0}Remoteversion:\n{1}'.format( unicode(localvcard.serialize(), _encoding), unicode(vcard.serialize(), _encoding)) logger.warning(versions) return localvcard
def execute(): localcontacts = getlocalcontacts() localadditions, localchanges, localdeletions = getlocalchanges(localcontacts) # get (recently changed) contacts from google data = {'max-results': len(localcontacts) + 50} if 'lastsync' in contactdb.keys() and not runoptions.getall: data['updated-min'] = contactdb['lastsync'] logger.debug(u'Logging into Google Contacts.') auth = authenticate(options['user'], options['password']) logger.debug(u'Retrieving contact list.') contactsxml = getcontacts(options['user'], auth, data=data) # store xml for reference if options['loglevel'] == 'debug': savexml(contactsxml) # parse into individual vcards logger.debug(u'Parsing contacts from Google.') contacts = xml2vcf.readXml(contactsxml) logger.info(u'Received {0} contacts from Google.'.format(len(contacts))) for c in contacts: if c.uid.value in localdeletions: # don't bother comparing if we're going to delete it anyway logger.debug(u'Ignoring "{0}": in deletion list.'.format(c.fn.value)) continue if c.uid.value in localcontacts: #logger.debug(u'Comparing "{0}".'.format(c.fn.value)) localcontacts[c.uid.value] = comparevcards(c, localcontacts[c.uid.value], auth) elif 'fn' in c.contents: localcontacts[c.uid.value] = c logger.info(u'New contact "{0}" added.'.format(c.fn.value)) else: logger.debug(u'New unparseable remote contact ' + u'"{0}" ignored.'.format(c.uid.value)) if c.uid.value in localchanges: # already compared, so delete from localchanges list del localchanges[localchanges.index(c.uid.value)] # local additions # TODO: additions go to general contact list, not My Contacts, and have to # be moved manually in Gmail. Fix this. logger.debug(u'Sending local additions.') for n in localadditions: xmlobj = vcf2xml.toXml(localcontacts[n]) xml = vcf2xml.ET.tostring(xmlobj, encoding=_encoding) response = sendcontact(options['user'], auth, xml) localvcard = xml2vcf.readXml(response)[0] # replace original with uid/etagged version from google del localcontacts[n] localcontacts[localvcard.uid.value] = localvcard logger.info(u'Local contact "{0}" added to Google.'.format(n)) # TODO: deal with remote deletions # remote deletions should have only <atom:id> and <gd:deleted> for 30 days # need special query? xml2vcf.readXml might fail as it's invalid vcard # without N, FN for cuid in localdeletions: #logger.debug(u'Deleting "{0}": deleted locally.'.format(c.fn.value)) #response = sendcontact(options['user'], auth, contactxml, contactid, True) # TODO: deal with response # sendcontact returns False if no contactid specified # add to list to delete until this is worked out logger.debug(u'Recording deletion of contact {0}.'.format(cuid)) try: contactdb['todelete'].append(cuid) # NB Shelf.append() is dependent on writeback=True except KeyError: contactdb['todelete'] = [cuid] pass # remaining localchanges logger.debug(u'Examining local changes.') for cuid in localchanges: logger.debug(u'Retrieving "{0}".'.format(localcontacts[cuid].fn.value)) contactxml = getcontacts(options['user'], auth, cuid) if contactxml is not None: logger.debug(u'Parsing contact from Google.') contact = xml2vcf.readXml(contactxml)[0] logger.debug(u'Comparing "{0}".'.format(localcontacts[cuid].fn.value)) localcontacts[cuid] = comparevcards(contact, localcontacts[cuid], auth) else: logger.error(u'Contact not found at Google.') # write out contacts file logger.debug(u'Writing new local file.') contactsfile = codecs.open(os.path.expanduser(options['contacts']), 'w', _encoding) # sort list by cuid so diffs are easier for cuid, c in sorted(localcontacts.items()): contactsfile.write(c.serialize().decode(_encoding)) # should this be? #contactsfile.write(unicode(c.serialize(), _encoding)) contactsfile.close() # set last sync time in config: now() or utcnow()? logger.debug(u'Recording sync details.') contactdb['lastsync'] = datetime.strftime(datetime.utcnow(), _dtformat) contactdb['cuids'] = localcontacts.keys() contactdb.close()