├── .gitignore ├── README.md ├── plugin └── notmuch_abook.vim ├── pythonx ├── __init__.py └── notmuch_abook.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Notmuch Addressbook manager for vim 2 | =================================== 3 | 4 | DEPENDENCES 5 | ----------- 6 | 7 | * notmuch with python bindings 8 | 9 | INSTALL 10 | ------- 11 | 12 | * Standalone install 13 | 14 | if you do not want to use the vim script file, you can install the module as 15 | follows: 16 | 17 | ```sh 18 | python setup.py install 19 | ``` 20 | 21 | or using: 22 | 23 | ```sh 24 | pip install notmuch_abook 25 | ``` 26 | 27 | * Vimscript install 28 | 29 | Use vundle to install this script, add to your vimrc: 30 | 31 | ```vim 32 | Bundle "guyzmo/notmuch-abook" 33 | ``` 34 | 35 | for convenience, you can create a symlink to your bin directory: 36 | 37 | ```sh 38 | ln -s $HOME/.vim/bundle/notmuch-abook/pythonx/notmuch_abook.py ~/bin/notmuch-abook 39 | ``` 40 | 41 | CONFIGURATION 42 | ------------- 43 | 44 | Open notmuch configuration file (usually $HOME/.notmuch-config) and add: 45 | 46 | ```ini 47 | [addressbook] 48 | path=/home/USER/.notmuch-abook.db 49 | backend=sqlite3 50 | ``` 51 | 52 | where USER is your username (or at any other place). 53 | 54 | The default notmuch query string is: 55 | 56 | ``` 57 | NOT tag:junk AND NOT folder:drafts AND NOT tag:deleted 58 | ``` 59 | 60 | If you prefer something else, you can specify it in notmuch configuration file: 61 | 62 | ```ini 63 | [addressbook] 64 | path=/home/USER/.notmuch-abook.db 65 | backend=sqlite3 66 | query=folder:Inbox OR folder:Sent 67 | ``` 68 | 69 | If you use a non-default notmuch configuration file, you should set the 70 | NOTMUCH_CONFIG environment variable (see notmuch man page). This can even be 71 | done inside the vimrc file, with: 72 | 73 | ```vim 74 | let $NOTMUCH_CONFIG = expand("~/.notmuch-config-custom") 75 | ``` 76 | 77 | In your favorite mail filter program, add a rule such as (for procmail), so all 78 | new mail will be parsed: 79 | 80 | ``` 81 | :0 Wh 82 | | python $HOME/.vim/bundle/notmuch-abook/pythonx/notmuch_abook.py update 83 | ``` 84 | 85 | If you can't use procmail (eg if you are using offlineimap) then you could put 86 | the following few lines at the start of the [post-new 87 | hook](http://notmuchmail.org/manpages/notmuch-hooks-5/) (**before** you remove 88 | the new tag). Also note this is shell syntax, so you'll have to adapt if your 89 | hook is in another language. 90 | 91 | ```sh 92 | # first update notmuch-abook 93 | for file in $(notmuch search --output=files tag:new) ; do 94 | cat "$file" | $HOME/bin/notmuch-abook update 95 | done 96 | ``` 97 | 98 | USAGE 99 | ----- 100 | 101 | For the first time, you shall launch the script as follows to create the 102 | addresses database (it takes ~20 seconds for 10000 mails): 103 | 104 | ```sh 105 | python $HOME/.vim/bundle/notmuch-abook/pythonx/notmuch_abook.py create 106 | ``` 107 | 108 | and then, to lookup an address, either you use the vimscript to complete 109 | (``) the name in a header field, or you can call it from commandline: 110 | 111 | ```sh 112 | python $HOME/.vim/bundle/notmuch-abook/pythonx/notmuch_abook.py lookup Guyz 113 | ``` 114 | 115 | the script will match any word beginning with the pattern in the name and 116 | address parts of the entry. 117 | 118 | CONTRIBUTORS 119 | ------------ 120 | 121 | - Maintainer: [Bernard _Guyzmo_ Pratz](https://github.com/guyzmo) 122 | - Contributors: 123 | - [Lucas Hoffmann](https://github.com/lucc) 124 | - [Hamish Downer](https://github.com/foobacca) 125 | - [Tomas Tomecek](https://github.com/TomasTomecek) 126 | - [David Edmondson](https://github.com/dme) 127 | - [Jesse Rosenthal](https://github.com/jkr) 128 | 129 | LICENSE 130 | ------- 131 | 132 | (c)2013, Bernard Guyzmo Pratz, guyzmo at m0g dot net 133 | 134 | Even though it is a WTFPL license, if you do improve that code, it's great, but 135 | if you don't tell me about that, you're just a moron. And if you like that 136 | code, you have the right to buy me a beer, thank me, or 137 | [flattr](http://flattr.com/profile/guyzmo)/[gittip](http://gittip.com/guyzmo) 138 | me. 139 | 140 | ``` 141 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 142 | Version 2, December 2004 143 | 144 | Copyright (C) 2004 Sam Hocevar 145 | 146 | Everyone is permitted to copy and distribute verbatim or modified 147 | copies of this license document, and changing it is allowed as long 148 | as the name is changed. 149 | 150 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 151 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 152 | 153 | 0. You just DO WHAT THE FUCK YOU WANT TO. 154 | ``` 155 | -------------------------------------------------------------------------------- /plugin/notmuch_abook.vim: -------------------------------------------------------------------------------- 1 | " Address book management 2 | 3 | 4 | if exists("g:notmuch_addressbook") 5 | finish 6 | else 7 | let g:notmuch_addressbook = 1 8 | endif 9 | 10 | if !has('python') 11 | echoerr "Error: Notmuch Addressbook plugin requires Vim to be compiled with +python" 12 | finish 13 | endif 14 | 15 | " Init link to Addressbook database 16 | fun! InitAddressBook() 17 | py import vim 18 | py import notmuch_abook 19 | py cfg = notmuch_abook.NotMuchConfig(None) 20 | py db = notmuch_abook.SQLiteStorage(cfg) if cfg.get("addressbook", "backend") == "sqlite3" else None 21 | endfun 22 | 23 | " Addressbook completion 24 | fun! CompleteAddressBook(findstart, base) 25 | let curline = getline('.') 26 | if curline =~ '^From: ' || curline =~ '^To: ' || curline =~ 'Cc: ' || curline =~ 'Bcc: ' 27 | if a:findstart 28 | " locate the start of the word 29 | let start = col('.') - 1 30 | while start > 0 && curline[start - 2] != ":" && curline[start - 2] != "," 31 | let start -= 1 32 | endwhile 33 | return start 34 | else 35 | python << EOP 36 | encoding = vim.eval("&encoding") 37 | if db: 38 | for addr in db.lookup(vim.eval('a:base')): 39 | if addr[0] == "": 40 | addr = addr[1] 41 | else: 42 | addr = addr[0]+" <"+addr[1]+">" 43 | vim.command('call complete_check()') 44 | vim.command(('call complete_add("{}")'.format(addr.replace('"', ""))).encode( encoding )) 45 | else: 46 | vim.command('echoerr "No backend found."') 47 | EOP 48 | return [] 49 | endif 50 | endif 51 | endfun 52 | 53 | augroup notmuchabook 54 | au! 55 | au FileType mail,notmuch-compose call InitAddressBook() 56 | au FileType mail,notmuch-compose setlocal completefunc=CompleteAddressBook 57 | augroup END 58 | 59 | -------------------------------------------------------------------------------- /pythonx/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyzmo/notmuch-abook/390d37ad19f2629c9f7436784403899ed483a3fa/pythonx/__init__.py -------------------------------------------------------------------------------- /pythonx/notmuch_abook.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ## Filename: notmuch_addresses.py 4 | ## Copyright (C) 2010-11 Jesse Rosenthal 5 | ## Author: Jesse Rosenthal 6 | 7 | ## This file is free software; you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published 9 | ## by the Free Software Foundation; either version 2, or (at your 10 | ## option) any later version. 11 | 12 | ## This program is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | 17 | ## NOTE: This script requires the notmuch python bindings. 18 | """ 19 | Notmuch Addressbook utility 20 | 21 | Usage: 22 | notmuch_abook.py -h 23 | notmuch_abook.py [-v] [-c CONFIG] create 24 | notmuch_abook.py [-v] [-c CONFIG] update 25 | notmuch_abook.py [-v] [-c CONFIG] lookup [ -f FORMAT ] 26 | notmuch_abook.py [-v] [-c CONFIG] changename
27 | notmuch_abook.py [-v] [-c CONFIG] delete [-n] 28 | notmuch_abook.py [-v] [-c CONFIG] export [ -f FORMAT ] [ -s SORT ] [] 29 | notmuch_abook.py [-v] [-c CONFIG] import [ -f FORMAT ] [ -r ] [] 30 | 31 | Options: 32 | -h --help Show this help message and exit 33 | -v --verbose Show full stacktraces on error 34 | -c CONFIG, --config CONFIG Path to notmuch configuration file 35 | -f FORMAT, --format FORMAT Format for name/address (see below) [default: email] 36 | -n, --noinput Don't ask for confirmation 37 | -s SORT, --sort SORT Whether to sort by name or address [default: name] 38 | -r, --replace If present, then replace the current contents with 39 | the imported contents. If not then merge - add new 40 | addresses, and update the name associated with 41 | existing addresses. 42 | 43 | Commands: 44 | 45 | create Create a new database. 46 | update Update the database with a new email (on stdin). 47 | lookup Lookup an address in the database. The match can be 48 | an email address or part of a name. 49 | changename
50 | Change the name associated with an email address. 51 | delete Delete all entries that match the given pattern - matched 52 | against both name and email address. The matches will be 53 | displayed and confirmation will be asked for, unless the 54 | --noinput flag is used. 55 | export [] Export database, to filename if given or to stdout if not. 56 | import [] Import into database, from filename if given or from stdin 57 | if not. 58 | 59 | Valid values for the FORMAT are: 60 | 61 | * abook - Give output in abook compatible format so it can be easily parsed 62 | by other programs. The format is EMAILNAME 63 | * csv - Give output as CSV (comma separated values). NAME,EMAIL 64 | * email - Give output in a format that can be used when composing an email. 65 | So NAME 66 | 67 | The database to use is set in the notmuch config file. 68 | """ 69 | 70 | from __future__ import print_function 71 | 72 | import os 73 | import re 74 | import sys 75 | import docopt 76 | import notmuch 77 | import sqlite3 78 | import email.utils 79 | import email.parser 80 | 81 | from io import open 82 | 83 | if sys.version_info.major == 3: 84 | import configparser 85 | else: 86 | input = raw_input 87 | import ConfigParser as configparser 88 | # use unicode csv if available 89 | try: 90 | import unicodecsv as csv 91 | except ImportError: 92 | import csv 93 | 94 | VALID_FORMATS = ['abook', 'csv', 'email'] 95 | 96 | 97 | class InvalidOptionError(Exception): 98 | """An option wasn't valid.""" 99 | 100 | 101 | class NotMuchConfig(object): 102 | def __init__(self, config_file): 103 | if config_file is None: 104 | config_file = os.environ.get('NOTMUCH_CONFIG', '~/.notmuch-config') 105 | 106 | # set a default for ignorefile 107 | self.config = configparser.ConfigParser({'ignorefile': None}) 108 | self.config.read(os.path.expanduser(config_file)) 109 | 110 | def get(self, section, key): 111 | return self.config.get(section, key) 112 | 113 | 114 | class Ignorer(object): 115 | def __init__(self, config): 116 | self.ignorefile = config.get('addressbook', 'ignorefile') 117 | self.ignore_regexes = None 118 | self.ignore_substrings = None 119 | 120 | def create_regexes(self): 121 | if self.ignorefile is None: 122 | return 123 | self.ignore_regexes = [] 124 | self.ignore_substrings = [] 125 | for line in open(self.ignorefile): 126 | line = line.strip() 127 | if not line or line.startswith('#'): 128 | continue # skip blank lines and comments 129 | if line.startswith('/') and line.endswith('/'): 130 | self.ignore_regexes.append(re.compile(line.strip('/'), re.IGNORECASE)) 131 | else: 132 | self.ignore_substrings.append(line) 133 | 134 | def ignore_address(self, address): 135 | """Check if this email address should be ignored. 136 | 137 | Return True if it should be ignored, or False otherwise.""" 138 | if self.ignorefile is None: 139 | return False 140 | if self.ignore_regexes is None: 141 | self.create_regexes() 142 | substring_match = any(substr in address for substr in self.ignore_substrings) 143 | if substring_match: 144 | return True 145 | return any(regex.search(address) for regex in self.ignore_regexes) 146 | 147 | 148 | class MailParser(object): 149 | def __init__(self): 150 | self.addresses = dict() 151 | 152 | def parse_mail(self, m): 153 | """ 154 | function used to extract headers from a email.message or 155 | notmuch.message email object yields address tuples 156 | """ 157 | addrs = [] 158 | if isinstance(m, email.message.Message): 159 | get_header = m.get 160 | else: 161 | get_header = m.get_header 162 | for h in ('to', 'from', 'cc', 'bcc'): 163 | v = get_header(h) 164 | if v: 165 | addrs.append(v) 166 | for addr in email.utils.getaddresses(addrs): 167 | name = addr[0].strip('; ') 168 | address = addr[1].lower().strip(';\'" ') 169 | if (address and address not in self.addresses): 170 | self.addresses[address] = name 171 | yield (name, address) 172 | 173 | 174 | class NotmuchAddressGetter(object): 175 | """Get all addresses from notmuch, based on information information from 176 | the user's $HOME/.notmuch-config file. 177 | """ 178 | 179 | def __init__(self, config): 180 | """ 181 | """ 182 | self.db_path = config.get("database", "path") 183 | try: 184 | self.query = config.get("addressbook", "query") 185 | except configparser.NoOptionError: 186 | self.query = "NOT tag:junk AND NOT folder:drafts AND NOT tag:deleted" 187 | self._mp = MailParser() 188 | 189 | def _get_all_messages(self): 190 | notmuch_db = notmuch.Database(self.db_path) 191 | query = notmuch.Query(notmuch_db, self.query) 192 | return query.search_messages() 193 | 194 | def generate(self): 195 | msgs = self._get_all_messages() 196 | for m in msgs: 197 | for addr in self._mp.parse_mail(m): 198 | yield addr 199 | 200 | 201 | class SQLiteStorage(): 202 | """SQL Storage backend""" 203 | def __init__(self, config): 204 | self.__path = config.get("addressbook", "path") 205 | self.ignorer = Ignorer(config) 206 | 207 | def connect(self): 208 | """ 209 | creates a new connection to the database and returns a cursor 210 | throws an error if the database does not exists 211 | """ 212 | if not os.path.exists(self.__path): 213 | raise IOError("Database '{}' does not exists".format(self.__path)) 214 | return sqlite3.connect(self.__path, isolation_level="DEFERRED") 215 | 216 | def create(self): 217 | """ 218 | create a new database 219 | """ 220 | if os.path.exists(self.__path): 221 | raise IOError("Can't create database at '%s'. File exists." % 222 | (self.__path,)) 223 | else: 224 | with sqlite3.connect(self.__path) as c: 225 | cur = c.cursor() 226 | cur.execute("CREATE VIRTUAL TABLE AddressBook USING fts4(Name, Address)") 227 | cur.execute("CREATE VIEW AddressBookView AS SELECT * FROM addressbook") 228 | cur.executescript( 229 | "CREATE TRIGGER insert_into_ab " + 230 | "INSTEAD OF INSERT ON AddressBookView " + 231 | "BEGIN" + 232 | " SELECT RAISE(ABORT, 'column name is not unique')" + 233 | " FROM addressbook" + 234 | " WHERE address = new.address;" + 235 | " INSERT INTO addressbook VALUES(new.name, new.address);" + 236 | "END;") 237 | 238 | def init(self, gen): 239 | """ 240 | populates the database with all addresses from address book 241 | """ 242 | n = 0 243 | with self.connect() as cur: 244 | cur.execute("PRAGMA synchronous = OFF") 245 | for elt in gen(): 246 | try: 247 | cur.execute("INSERT INTO AddressBookView VALUES(?,?)", elt) 248 | n += 1 249 | except sqlite3.IntegrityError: 250 | pass 251 | cur.commit() 252 | return n 253 | 254 | def update(self, addr, replace=False): 255 | """ 256 | updates the database with a new mail address tuple 257 | 258 | replace: if the email address already exists then replace the name with the new name 259 | """ 260 | if self.ignorer.ignore_address(addr[1]): 261 | return False 262 | try: 263 | with self.connect() as c: 264 | cur = c.cursor() 265 | if replace: 266 | present = cur.execute("SELECT 1 FROM AddressBook WHERE address = ?", [addr[1]]) 267 | if present: 268 | cur.execute("UPDATE AddressBook SET name = ? WHERE address = ?", addr) 269 | else: 270 | cur.execute("INSERT INTO AddressBookView VALUES(?,?)", addr) 271 | else: 272 | cur.execute("INSERT INTO AddressBookView VALUES(?,?)", addr) 273 | return True 274 | except sqlite3.IntegrityError: 275 | return False 276 | 277 | def create_query(self, query_start, pattern): 278 | return query_start + """ FROM AddressBook WHERE AddressBook MATCH '"{}*"'""".format(pattern) 279 | 280 | def lookup(self, pattern): 281 | """ 282 | lookup an address from the given match in database 283 | """ 284 | with self.connect() as c: 285 | # so we can access results via dictionary 286 | c.row_factory = sqlite3.Row 287 | cur = c.cursor() 288 | for res in cur.execute(self.create_query("SELECT *", pattern)).fetchall(): 289 | yield res 290 | 291 | def delete_matches(self, pattern): 292 | """ 293 | Delete all entries that match the pattern 294 | """ 295 | with self.connect() as c: 296 | cur = c.cursor() 297 | cur.execute(self.create_query("DELETE", pattern)) 298 | 299 | def fetchall(self, order_by): 300 | """ 301 | Fetch all entries from the database. 302 | """ 303 | with self.connect() as c: 304 | c.row_factory = sqlite3.Row 305 | cur = c.cursor() 306 | for res in cur.execute("SELECT * FROM AddressBook ORDER BY {}".format(order_by)).fetchall(): 307 | yield res 308 | 309 | def change_name(self, address, name): 310 | """ 311 | Change the name associated with an email address 312 | """ 313 | with self.connect() as c: 314 | cur = c.cursor() 315 | cur.execute("UPDATE AddressBook SET name = '{}' WHERE address = '{}'".format(name, address)) 316 | return True 317 | 318 | def delete_db(self): 319 | """ 320 | Delete the database 321 | """ 322 | if os.path.exists(self.__path): 323 | os.remove(self.__path) 324 | 325 | 326 | def format_address(address, output_format): 327 | if output_format == 'abook': 328 | return "{}\t{}".format(address['Address'], address['Name']) 329 | elif output_format == 'email': 330 | return email.utils.formataddr((address['Name'], address['Address'])) 331 | else: 332 | raise InvalidOptionError('Unknown format: {}'.format(output_format)) 333 | 334 | 335 | def decode_line(line, input_format): 336 | if input_format == 'abook': 337 | if '\t' in line: 338 | address, name = line.split('\t') 339 | else: 340 | address, name = line, '' 341 | elif input_format == 'email': 342 | name, address = email.utils.parseaddr(line) 343 | else: 344 | raise InvalidOptionError('Unknown format: {}'.format(input_format)) 345 | return name, address 346 | 347 | 348 | def print_address_list(address_list, output_format, out=None): 349 | if out is None: 350 | out = sys.stdout 351 | if output_format == 'csv': 352 | try: 353 | writer = csv.writer(out) 354 | for address in address_list: 355 | writer.writerow((address['Name'], address['Address'])) 356 | except UnicodeEncodeError as e: 357 | print("Caught UnicodeEncodeError: {}".format(e), file=sys.stderr) 358 | print("Installing unicodecsv will probably fix this", file=sys.stderr) 359 | return 360 | else: 361 | for address in address_list: 362 | output = format_address(address, output_format) 363 | out.write(output + '\n') 364 | 365 | 366 | def import_address_list_from_csv(db, replace_all, infile): 367 | try: 368 | reader = csv.reader(infile) 369 | for row in reader: 370 | db.update(row, replace=(not replace_all)) 371 | except UnicodeEncodeError as e: 372 | print("Caught UnicodeEncodeError: {}".format(e), file=sys.stderr) 373 | print("Installing unicodecsv will probably fix this", file=sys.stderr) 374 | return 375 | 376 | 377 | def import_address_list(db, replace_all, input_format, infile=None): 378 | if infile is None: 379 | infile = sys.stdin 380 | if replace_all: 381 | db.delete_db() 382 | db.create() 383 | if input_format == 'csv': 384 | import_address_list_from_csv(db, replace_all, infile) 385 | else: 386 | for line in infile: 387 | name_addr = decode_line(line.strip(), input_format) 388 | db.update(name_addr, replace=(not replace_all)) 389 | 390 | 391 | def create_action(db, nm_config): 392 | db.create() 393 | nm_mailgetter = NotmuchAddressGetter(nm_config) 394 | n = db.init(nm_mailgetter.generate) 395 | print("added {} addresses".format(n)) 396 | 397 | 398 | def update_action(db, verbose): 399 | n = 0 400 | m = email.message_from_file(sys.stdin) 401 | for addr in MailParser().parse_mail(m): 402 | if db.update(addr): 403 | n += 1 404 | if verbose: 405 | print("added {} addresses".format(n)) 406 | 407 | 408 | def lookup_action(db, match, output_format): 409 | print_address_list(db.lookup(match), output_format) 410 | 411 | 412 | def delete_action(db, pattern, noinput): 413 | matches = list(db.lookup(pattern)) 414 | if len(matches) == 0: 415 | print("Nothing to delete") 416 | return 417 | print("The following entries match:") 418 | print() 419 | print_address_list(matches, 'email') 420 | if not noinput: 421 | print() 422 | response = input('Are you sure you want to delete all these entries? (y/n) ') 423 | if response.lower() != 'y': 424 | return 425 | db.delete_matches(pattern) 426 | print() 427 | print("{} entries deleted".format(len(matches))) 428 | 429 | 430 | def export_action(db, output_format, sort, filename=None): 431 | out = None 432 | try: 433 | if filename: 434 | out = open(filename, mode='w', encoding='utf-8') 435 | print_address_list(db.fetchall(sort), output_format, out) 436 | finally: 437 | if filename and out: 438 | out.close() 439 | 440 | 441 | def import_action(db, input_format, replace, filename=None): 442 | infile = None 443 | try: 444 | if filename: 445 | infile = open(filename, mode='r', encoding='utf-8') 446 | import_address_list(db, replace, input_format, infile) 447 | finally: 448 | if filename and infile: 449 | infile.close() 450 | 451 | 452 | def run(): 453 | options = docopt.docopt(__doc__) 454 | 455 | if options['--format'] not in VALID_FORMATS: 456 | print('{} is not a valid output option.'.format(options['--format']), file=sys.stderr) 457 | return 2 458 | 459 | try: 460 | nm_config = NotMuchConfig(options['--config']) 461 | if nm_config.get("addressbook", "backend") == "sqlite3": 462 | db = SQLiteStorage(nm_config) 463 | else: 464 | print("Database backend '{}' is not implemented.".format(nm_config.get("addressbook", "backend"))) 465 | 466 | if options['create']: 467 | create_action(db, nm_config) 468 | elif options['update']: 469 | update_action(db, options['--verbose']) 470 | elif options['lookup']: 471 | lookup_action(db, options[''], options['--format']) 472 | elif options['changename']: 473 | db.change_name(options['
'], options['']) 474 | elif options['delete']: 475 | delete_action(db, options[''], options['--noinput']) 476 | elif options['export']: 477 | export_action(db, options['--format'], options['--sort'], options['']) 478 | elif options['import']: 479 | import_action(db, options['--format'], options['--replace'], options['']) 480 | except Exception as exc: 481 | if options['--verbose']: 482 | import traceback 483 | traceback.print_exc() 484 | else: 485 | print(exc) 486 | return 1 487 | return 0 488 | 489 | if __name__ == '__main__': 490 | sys.exit(run()) 491 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | def read(*names): 5 | values = dict() 6 | for name in names: 7 | if os.path.isfile(name): 8 | value = open(name).read() 9 | else: 10 | value = '' 11 | values[name] = value 12 | return values 13 | 14 | long_description=""" 15 | Notmuch Addressbook Utility 16 | 17 | %(README)s 18 | 19 | """ % read('README') 20 | 21 | setup(name='notmuch_abook', 22 | version="v1.7", 23 | description="Notmuch addressbook", 24 | long_description=long_description, 25 | classifiers=["Development Status :: 4 - Beta", 26 | "Environment :: Console", 27 | "License :: Freely Distributable", 28 | "Topic :: Communications :: Email :: Address Book"], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers 29 | keywords='notmuch addressbook vim', 30 | author='Bernard `Guyzmo` Pratz', 31 | author_email='guyzmo+notmuch@m0g.net', 32 | url='https://github.com/guyzmo/notmuch-abook/', 33 | license='WTFPL', 34 | package_dir={'notmuch_abook': 'pythonx'}, 35 | data_files=[('plugin', ['plugin/notmuch_abook.vim'])], 36 | packages=['notmuch_abook'], #find_packages(exclude=['plugin']), 37 | include_package_data=True, 38 | namespace_packages = [], 39 | zip_safe=False, 40 | install_requires=[ 41 | # -*- Extra requirements: -*- 42 | "pysqlite", 43 | "docopt" 44 | ], 45 | entry_points=""" 46 | # -*- Entry points: -*- 47 | [console_scripts] 48 | notmuch_abook = notmuch_abook.notmuch_abook:run 49 | """, 50 | ) 51 | --------------------------------------------------------------------------------