diff options
Diffstat (limited to 'mutt/goobook/build')
3 files changed, 400 insertions, 0 deletions
| diff --git a/mutt/goobook/build/lib.linux-x86_64-2.7/goobook/__init__.py b/mutt/goobook/build/lib.linux-x86_64-2.7/goobook/__init__.py new file mode 100644 index 0000000..27c5034 --- /dev/null +++ b/mutt/goobook/build/lib.linux-x86_64-2.7/goobook/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: UTF-8 -*- +# vim: fileencoding=UTF-8 filetype=python ff=unix et ts=4 sw=4 sts=4 tw=120 +# author: Christer Sjöholm -- hcs AT furuvik DOT net + + diff --git a/mutt/goobook/build/lib.linux-x86_64-2.7/goobook/goobook.py b/mutt/goobook/build/lib.linux-x86_64-2.7/goobook/goobook.py new file mode 100644 index 0000000..f0f5edf --- /dev/null +++ b/mutt/goobook/build/lib.linux-x86_64-2.7/goobook/goobook.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python2 +# vim: fileencoding=UTF-8 filetype=python ff=unix expandtab sw=4 sts=4 tw=120 +# maintainer: Christer Sjöholm -- goobook AT furuvik DOT net +# +# Copyright (C) 2009  Carlos José Barroso +# Copyright (C) 2010  Christer Sjöholm +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +'''\ +The idea is make an interface to google contacts that mimics the behaviour of +abook for mutt. It's developed in python and uses the fine +google data api (gdata). +''' + +import email.header +import locale +import logging +import optparse +import getpass +import sys +import os +import subprocess +import re +import time +import ConfigParser +from netrc import netrc +from os.path import realpath, expanduser +from storage import Storage + +try: +    import simplejson +    json = simplejson # this hushes pyflakes +except ImportError: +    import json + +import gdata +from gdata.contacts.client import ContactsClient, ContactsQuery +from gdata.contacts.data import ContactEntry +from gdata.data import Email, Name, FullName + +log = logging.getLogger('goobook') + +CONFIG_FILE = '~/.goobookrc' +CONFIG_TEMPLATE = '''\ +# "#" or ";" at the start of a line makes it a comment. +[DEFAULT] +# If not given here, email and password is taken from .netrc using +# machine google.com +;email: user@gmail.com +;password: top secret +# The following are optional, defaults are shown +;max_results: 9999 +;cache_filename: ~/.goobook_cache +;cache_expiry_hours: 24 +''' + +ENCODING = locale.getpreferredencoding() + +class GooBook(object): +    '''This class can't be used as a library as it looks now, it uses sys.stdin +       print, sys.exit() and getpass().''' +    def __init__ (self, config): +        self.config = config +        self.__client = None +        self.contacts = {} +        ''' This is where all the contacts is stored +        {'contacts': {contact_id: <contact>} +         'groups': {'group': [contact_id] ] +        } +        <contact> is a {'name':'', 'email':''} +        ''' + +    @property +    def password(self): +        if not self.config.password: +            self.config.password = getpass.getpass() +        return self.config.password + +    def __get_client(self): +        '''Login to Google and return a ContactsClient object. + +        ''' +        if not self.__client: +            if not self.config.email or not self.password: +                print >> sys.stderr, "ERROR: Missing email or password" +                sys.exit(1) +            client = ContactsClient() +            client.ssl = True +            client.ClientLogin(email=self.config.email, password=self.password, service='cp', source='goobook') +            self.__client = client +        return self.__client + +    def __query_contacts(self, query): +        match = re.compile(query, re.I).search +        for contact in self.contacts['contacts'].itervalues(): +            for field in ('name', 'nick', 'emails'): +                value = contact.get(field, None) +                if not value: +                    pass +                elif isinstance(value, basestring): +                    if value and match(value): +                        yield contact +                        break +                else: #value is list +                    found_one = False +                    for value2 in value: +                        if value2 and match(value2): +                            yield contact +                            found_one = True +                            break +                    if found_one: +                        break + +    def __query_groups(self, query): +        match = re.compile(query, re.I).search +        for (group_id, group_name) in self.contacts['groups'].iteritems(): +            if match(group_name): +                yield (group_name, list(self.__get_group_contacts(group_id))) + +    def query(self, query): +        """Do the query, and print it out in + +        """ +        self.load() +        #query contacts +        matching_contacts = sorted(self.__query_contacts(query), key=lambda c: c['name']) +        #query groups +        matching_groups = sorted(self.__query_groups(query), key=lambda g: g[0]) +        # mutt's query_command expects the first line to be a message, +        # which it discards. +        print "\n", +        for contact in matching_contacts: +            if 'emails' in contact and contact['emails']: +                emailaddrs = sorted(contact['emails']) +                for emailaddr in emailaddrs: +                    print (u'%s\t%s' % (emailaddr, contact['name'])).encode(ENCODING) +        for group_name, contacts in matching_groups: +            emails = ['%s <%s>' % (c['name'], c['emails'][0]) for c in contacts if c['emails']] +            emails = ', '.join(emails) +            if not emails: +                continue +            print (u'%s\t%s (group)' % (emails, group_name)).encode(ENCODING) + +    def __get_group_contacts(self, group_id): +        for contact in self.contacts['contacts'].itervalues(): +            if group_id in contact['groups']: +                yield contact + +    def load(self): +        """Load the cached addressbook feed, or fetch it (again) if it is +        old or missing or invalid or anyting + +        """ +        contacts = None + +        # if cache older than cache_expiry_hours +        if (not os.path.exists(self.config.cache_filename) or +                ((time.time() - os.path.getmtime(self.config.cache_filename)) > +                    (self.config.cache_expiry_hours * 60 *60))): +            contacts = self.fetch() +            self.store(contacts) +        if not contacts: +            try: +                contacts = json.load(open(self.config.cache_filename)) +                if contacts.get('goobook_cache') != '1.1': +                    contacts = None # Old cache format +            except ValueError: +                pass # Failed to read JSON file. +        if not contacts: +            contacts = self.fetch() +            self.store(contacts) +        if not contacts: +            raise Exception('Failed to find any contacts') # TODO +        self.contacts = contacts + + +    def fetch_contacts(self): +        client = self.__get_client() +        query = ContactsQuery(max_results=self.config.max_results) +        entries = client.get_contacts(query=query).entry +        return dict([self.__parse_contact(ent) for ent in entries]) + +    @staticmethod +    def __parse_contact(ent): +        '''takes a gdata contact entry and returns a parsed contact. +        on the form (contact_id, {fieldname:content}) + +        ''' +        contact = {} +        contact['name'] = ent.title.text +        contact['emails'] = [emailent.address for emailent in ent.email] +        if ent.nickname: +            contact['nick'] = ent.nickname.text +        contact['groups'] = [g.href for g in ent.group_membership_info] +        return (ent.id.text, contact) + +    def fetch_contact_groups(self): +        client = self.__get_client() +        return dict([(g.id.text, g.title.text) for g in client.get_groups().entry]) + +    def fetch(self): +        """Actually go out on the wire and fetch the addressbook. +        Returns the contacts data structure. + +        """ +        contacts = self.fetch_contacts() +        groups = self.fetch_contact_groups() +        return {'contacts':contacts, 'groups':groups, 'goobook_cache': '1.1'} + +    def store(self, contacts): +        """Pickle the addressbook and a timestamp + +        """ +        if contacts: # never write a empty addressbook +            json.dump(contacts, open(self.config.cache_filename, 'w'), indent=2) + +    def add(self): +        """Add an address from From: field of a mail. +        This assumes a single mail file is supplied through stdin. + +        """ +        from_line = "" +        for line in sys.stdin: +            if line.startswith("From: "): +                from_line = line +                break +        if from_line == "": +            print "Not a valid mail file!" +            sys.exit(2) +        #Parse From: line +        #Take care of non ascii header +        from_line = unicode(email.header.make_header(email.header.decode_header(from_line))) +        #Parse the From line +        (name, mailaddr) = email.utils.parseaddr(from_line) +        if not name: +            name = mailaddr +        #save to contacts +        client = self.__get_client() +        new_contact = ContactEntry(name=Name(full_name=FullName(text=name))) +        new_contact.email.append(Email(address=mailaddr, rel='http://schemas.google.com/g/2005#home', primary='true')) +        client.create_contact(new_contact) +        print 'Created contact:', name.encode(ENCODING), mailaddr.encode(ENCODING) + +def read_config(config_file): +    '''Reads the ~/.goobookrc and ~/.netrc. +    returns the configuration as a dictionary. + +    ''' +    config = Storage({ # Default values +        'email': '', +        'password': '', +        'max_results': '9999', +        'cache_filename': '~/.goobook_cache', +        'cache_expiry_hours': '24', +        }) +    config_file = os.path.expanduser(config_file) +    if os.path.lexists(config_file) or os.path.lexists(config_file + '.gpg'): +        try: +            parser = ConfigParser.SafeConfigParser() +            if os.path.lexists(config_file): +                log.info('Reading config: %s', config_file) +                f = open(config_file) +            else: +                log.info('Reading config: %s', config_file + '.gpg') +                sp = subprocess.Popen(['gpg', '--no-tty', '-q', '-d', config_file + ".gpg"], stdout=subprocess.PIPE) +                f = sp.stdout +            parser.readfp(f) +            config.update(dict(parser.items('DEFAULT', raw=True))) +        except (IOError, ConfigParser.ParsingError), e: +            print >> sys.stderr, "Failed to read configuration %s\n%s" % (config_file, e) +            sys.exit(1) +    if not config.get('email') or not config.get('password'): +        log.info('email or password missing from config, checking .netrc') +        auth = netrc().authenticators('google.com') +        if auth: +            login = auth[0] +            password = auth[2] +            if not config.get('email'): +                config['email'] = login +            if not config.get('password'): +                config['password'] = password +        else: +            log.info('No match in .netrc') + +        config.cache_filename = realpath(expanduser(config.cache_filename)) + +        log.debug(config) +    return config + +def main(): +    class MyParser(optparse.OptionParser): +        def format_epilog(self, formatter): +            return self.epilog +    usage = 'usage: %prog [options] <command> [<arg>]' +    description = 'Search you Google contacts from mutt or the command-line.' +    epilog = '''\ +Commands: +  add              Add the senders address to contacts, reads a mail from STDIN. +  reload           Force reload of the cache. +  query <query>    Search contacts using query (regex). +  config-template  Prints a template for .goobookrc to STDOUT + +''' +    parser = MyParser(usage=usage, description=description, epilog=epilog) +    parser.set_defaults(config_file=CONFIG_FILE) +    parser.add_option("-c", "--config", dest="config_file", +                    help="Specify alternative configuration file.", metavar="FILE") +    parser.add_option("-v", "--verbose", dest="logging_level", default=logging.ERROR, +                    help="Specify alternative configuration file.", +                    action='store_const', const=logging.INFO) +    parser.add_option("-d", "--debug", dest="logging_level", +                    help="Specify alternative configuration file.", +                    action='store_const', const=logging.DEBUG) +    (options, args) = parser.parse_args() +    if len(args) == 0: +        parser.print_help() +        sys.exit(1) +    logging.basicConfig(level=options.logging_level) +    config = read_config(options.config_file) +    goobk = GooBook(config) +    try: +        cmd = args.pop(0) +        if cmd == "query": +            if len(args) != 1: +                parser.error("incorrect number of arguments") +            goobk.query(args[0].decode(ENCODING)) +        elif cmd == "add": +            goobk.add() +        elif cmd == "reload": +            goobk.store(goobk.fetch()) +        elif cmd == "config-template": +            print CONFIG_TEMPLATE +        else: +            parser.error('Command not recognized: %s' % cmd) +    except gdata.client.BadAuthentication, e: +        print >> sys.stderr, e # Incorrect username or password +        sys.exit(1) + +if __name__ == '__main__': +    main() diff --git a/mutt/goobook/build/lib.linux-x86_64-2.7/goobook/storage.py b/mutt/goobook/build/lib.linux-x86_64-2.7/goobook/storage.py new file mode 100644 index 0000000..3314327 --- /dev/null +++ b/mutt/goobook/build/lib.linux-x86_64-2.7/goobook/storage.py @@ -0,0 +1,43 @@ +# -*- coding: UTF-8 -*- +# vim: fileencoding=UTF-8 filetype=python ff=unix et ts=4 sw=4 sts=4 tw=120 +# author: Christer Sjöholm -- hcs AT furuvik DOT net + +class Storage(dict): +    """ +    A Storage object is like a dictionary except `obj.foo` can be used +    in addition to `obj['foo']`. + +        >>> o = storage(a=1) +        >>> o.a +        1 +        >>> o['a'] +        1 +        >>> o.a = 2 +        >>> o['a'] +        2 +        >>> del o.a +        >>> o.a +        Traceback (most recent call last): +            ... +        AttributeError: 'a' + +        Storage comes from web.py (public domain) +    """ +    def __getattr__(self, key): +        try: +            return self[key] +        except KeyError, k: +            raise AttributeError, k + +    def __setattr__(self, key, value): +        self[key] = value + +    def __delattr__(self, key): +        try: +            del self[key] +        except KeyError, k: +            raise AttributeError, k + +    def __repr__(self): +        return '<Storage ' + dict.__repr__(self) + '>' + | 
