From 2116a24a17e7e9ed48777533702118a697f268a8 Mon Sep 17 00:00:00 2001
From: "W. Trevor King"
Date: Sat, 2 Jan 2016 22:08:02 +1600
Subject: [PATCH] [PATCH 2/5] notmuch-report: Rename from nmbug-status
---
7a/9a58a895a7643ff592a48b67084d3f7c51b4e9 | 935 ++++++++++++++++++++++
1 file changed, 935 insertions(+)
create mode 100644 7a/9a58a895a7643ff592a48b67084d3f7c51b4e9
diff --git a/7a/9a58a895a7643ff592a48b67084d3f7c51b4e9 b/7a/9a58a895a7643ff592a48b67084d3f7c51b4e9
new file mode 100644
index 000000000..c6e787bcc
--- /dev/null
+++ b/7a/9a58a895a7643ff592a48b67084d3f7c51b4e9
@@ -0,0 +1,935 @@
+Return-Path:
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+ by arlo.cworth.org (Postfix) with ESMTP id B483C6DE1B58
+ for ; Fri, 1 Jan 2016 22:10:42 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at cworth.org
+X-Amavis-Alert: BAD HEADER SECTION, Duplicate header field: "References"
+X-Spam-Flag: NO
+X-Spam-Score: 0.04
+X-Spam-Level:
+X-Spam-Status: No, score=0.04 tagged_above=-999 required=5 tests=[AWL=0.041,
+ DKIM_SIGNED=0.1, DKIM_VALID=-0.1, RCVD_IN_DNSWL_NONE=-0.0001,
+ SPF_PASS=-0.001] autolearn=disabled
+Received: from arlo.cworth.org ([127.0.0.1])
+ by localhost (arlo.cworth.org [127.0.0.1]) (amavisd-new, port 10024)
+ with ESMTP id neE8vqetwSAG for ;
+ Fri, 1 Jan 2016 22:10:39 -0800 (PST)
+Received: from resqmta-po-04v.sys.comcast.net (resqmta-po-04v.sys.comcast.net
+ [96.114.154.163])
+ by arlo.cworth.org (Postfix) with ESMTPS id 4765F6DE1B50
+ for ; Fri, 1 Jan 2016 22:10:02 -0800 (PST)
+Received: from resomta-po-19v.sys.comcast.net ([96.114.154.243])
+ by resqmta-po-04v.sys.comcast.net with comcast
+ id 0uA11s0015FMDhs01uA1rC; Sat, 02 Jan 2016 06:10:01 +0000
+Received: from mail.tremily.us ([73.221.72.168])
+ by resomta-po-19v.sys.comcast.net with comcast
+ id 0u7z1s00L3dr3C901u80VP; Sat, 02 Jan 2016 06:08:01 +0000
+Received: from ullr.tremily.us (unknown [192.168.10.7])
+ by mail.tremily.us (Postfix) with ESMTPS id 6ED401B2F57F;
+ Fri, 1 Jan 2016 22:07:59 -0800 (PST)
+Received: (nullmailer pid 15181 invoked by uid 1000);
+ Sat, 02 Jan 2016 06:08:07 -0000
+From: "W. Trevor King"
+To: notmuch@notmuchmail.org
+Cc: David Bremner ,
+ Tomi Ollila , Jani Nikula ,
+ Carl Worth , "W. Trevor King"
+Subject: [PATCH 2/5] notmuch-report: Rename from nmbug-status
+Date: Fri, 1 Jan 2016 22:08:02 -0800
+Message-Id:
+ <7df3f39aa67ed3862b937c7bf047a59d913e9c44.1451714099.git.wking@tremily.us>
+X-Mailer: git-send-email 2.1.0.60.g85f0837
+In-Reply-To:
+References:
+In-Reply-To:
+References:
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=comcast.net;
+ s=q20140121; t=1451715001;
+ bh=b+eqQb79fXv+mPmYMNt7f5OjvZ4+EPolsU093hd980c=;
+ h=Received:Received:Received:Received:From:To:Subject:Date:
+ Message-Id;
+ b=pA4yTDI161iiniYaUP+XOFA4BsADTNzuzbQ2ktl3DzWNOqDzNT3lHfl3241hAVZaL
+ dQQSgQXW4iIakjEE2IpFYS5bAgG66uukBP5ndleopd3FtQuhbhDv0S02BqtqU6IM3Y
+ zuRjkzqPsVpbSwqudOusiOQtkCn2hoE25e4rDpoOZUF2to1RVUd9u6N5bBGA8RlcV1
+ Iz0T8+ev5ImEGfXuEZqqRMjJ87TFfos1UzfoK8uaBu6rhKD3D2MN1c3H3hFb3CXOBX
+ nCZtUyHqSnkP7XcZezzbSfiGoMWgvE5VGvF2pHReSxOvmJgMwZap65ErCB09X4jptK
+ ywBmS/HtBZkqg==
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.20
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+
+List-Unsubscribe: ,
+
+List-Archive:
+List-Post:
+List-Help:
+List-Subscribe: ,
+
+X-List-Received-Date: Sat, 02 Jan 2016 06:10:42 -0000
+
+This script generates reports based on notmuch queries, and doesn't
+really have anything to do with nmbug, except for sharing the NMBGIT
+environment variable.
+---
+ devel/nmbug/nmbug-status | 419 ---------------------------------------------
+ devel/nmbug/notmuch-report | 419 +++++++++++++++++++++++++++++++++++++++++++++
+ 2 files changed, 419 insertions(+), 419 deletions(-)
+ delete mode 100755 devel/nmbug/nmbug-status
+ create mode 100755 devel/nmbug/notmuch-report
+
+diff --git a/devel/nmbug/nmbug-status b/devel/nmbug/nmbug-status
+deleted file mode 100755
+index 22e3b5b..0000000
+--- a/devel/nmbug/nmbug-status
++++ /dev/null
+@@ -1,419 +0,0 @@
+-#!/usr/bin/python
+-#
+-# Copyright (c) 2011-2012 David Bremner
+-#
+-# dependencies
+-# - python 2.6 for json
+-# - argparse; either python 2.7, or install separately
+-#
+-# 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/ .
+-
+-"""Generate HTML for one or more notmuch searches.
+-
+-Messages matching each search are grouped by thread. Each message
+-that contains both a subject and message-id will have the displayed
+-subject link to the Gmane view of the message.
+-"""
+-
+-from __future__ import print_function
+-from __future__ import unicode_literals
+-
+-import codecs
+-import collections
+-import datetime
+-import email.utils
+-try: # Python 3
+- from urllib.parse import quote
+-except ImportError: # Python 2
+- from urllib import quote
+-import json
+-import argparse
+-import os
+-import re
+-import sys
+-import subprocess
+-import xml.sax.saxutils
+-
+-
+-_ENCODING = 'UTF-8'
+-_PAGES = {}
+-
+-
+-if not hasattr(collections, 'OrderedDict'): # Python 2.6 or earlier
+- class _OrderedDict (dict):
+- "Just enough of a stub to get through Page._get_threads"
+- def __init__(self, *args, **kwargs):
+- super(_OrderedDict, self).__init__(*args, **kwargs)
+- self._keys = [] # record key order
+-
+- def __setitem__(self, key, value):
+- super(_OrderedDict, self).__setitem__(key, value)
+- self._keys.append(key)
+-
+- def values(self):
+- for key in self._keys:
+- yield self[key]
+-
+-
+- collections.OrderedDict = _OrderedDict
+-
+-
+-class ConfigError (Exception):
+- """Errors with config file usage
+- """
+- pass
+-
+-
+-def read_config(path=None, encoding=None):
+- "Read config from json file"
+- if not encoding:
+- encoding = _ENCODING
+- if path:
+- try:
+- with open(path, 'rb') as f:
+- config_bytes = f.read()
+- except IOError as e:
+- raise ConfigError('Could not read config from {}'.format(path))
+- else:
+- nmbhome = os.getenv('NMBGIT', os.path.expanduser('~/.nmbug'))
+- branch = 'config'
+- filename = 'status-config.json'
+-
+- # read only the first line from the pipe
+- sha1_bytes = subprocess.Popen(
+- ['git', '--git-dir', nmbhome, 'show-ref', '-s', '--heads', branch],
+- stdout=subprocess.PIPE).stdout.readline()
+- sha1 = sha1_bytes.decode(encoding).rstrip()
+- if not sha1:
+- raise ConfigError(
+- ("No local branch '{branch}' in {nmbgit}. "
+- 'Checkout a local {branch} branch or explicitly set --config.'
+- ).format(branch=branch, nmbgit=nmbhome))
+-
+- p = subprocess.Popen(
+- ['git', '--git-dir', nmbhome, 'cat-file', 'blob',
+- '{}:{}'.format(sha1, filename)],
+- stdout=subprocess.PIPE)
+- config_bytes, err = p.communicate()
+- status = p.wait()
+- if status != 0:
+- raise ConfigError(
+- ("Missing {filename} in branch '{branch}' of {nmbgit}. "
+- 'Add the file or explicitly set --config.'
+- ).format(filename=filename, branch=branch, nmbgit=nmbhome))
+-
+- config_json = config_bytes.decode(encoding)
+- try:
+- return json.loads(config_json)
+- except ValueError as e:
+- if not path:
+- path = "{} in branch '{}' of {}".format(
+- filename, branch, nmbhome)
+- raise ConfigError(
+- 'Could not parse JSON from the config file {}:\n{}'.format(
+- path, e))
+-
+-
+-class Thread (list):
+- def __init__(self):
+- self.running_data = {}
+-
+-
+-class Page (object):
+- def __init__(self, header=None, footer=None):
+- self.header = header
+- self.footer = footer
+-
+- def write(self, database, views, stream=None):
+- if not stream:
+- try: # Python 3
+- byte_stream = sys.stdout.buffer
+- except AttributeError: # Python 2
+- byte_stream = sys.stdout
+- stream = codecs.getwriter(encoding=_ENCODING)(stream=byte_stream)
+- self._write_header(views=views, stream=stream)
+- for view in views:
+- self._write_view(database=database, view=view, stream=stream)
+- self._write_footer(views=views, stream=stream)
+-
+- def _write_header(self, views, stream):
+- if self.header:
+- stream.write(self.header)
+-
+- def _write_footer(self, views, stream):
+- if self.footer:
+- stream.write(self.footer)
+-
+- def _write_view(self, database, view, stream):
+- # sort order, default to oldest-first
+- sort_key = view.get('sort', 'oldest-first')
+- # dynamically accept all values in Query.SORT
+- sort_attribute = sort_key.upper().replace('-', '_')
+- try:
+- sort = getattr(notmuch.Query.SORT, sort_attribute)
+- except AttributeError:
+- raise ConfigError('Invalid sort setting for {}: {!r}'.format(
+- view['title'], sort_key))
+- if 'query-string' not in view:
+- query = view['query']
+- view['query-string'] = ' and '.join(query)
+- q = notmuch.Query(database, view['query-string'])
+- q.set_sort(sort)
+- threads = self._get_threads(messages=q.search_messages())
+- self._write_view_header(view=view, stream=stream)
+- self._write_threads(threads=threads, stream=stream)
+-
+- def _get_threads(self, messages):
+- threads = collections.OrderedDict()
+- for message in messages:
+- thread_id = message.get_thread_id()
+- if thread_id in threads:
+- thread = threads[thread_id]
+- else:
+- thread = Thread()
+- threads[thread_id] = thread
+- thread.running_data, display_data = self._message_display_data(
+- running_data=thread.running_data, message=message)
+- thread.append(display_data)
+- return list(threads.values())
+-
+- def _write_view_header(self, view, stream):
+- pass
+-
+- def _write_threads(self, threads, stream):
+- for thread in threads:
+- for message_display_data in thread:
+- stream.write(
+- ('{date:10.10s} {from:20.20s} {subject:40.40s}\n'
+- '{message-id-term:>72}\n'
+- ).format(**message_display_data))
+- if thread != threads[-1]:
+- stream.write('\n')
+-
+- def _message_display_data(self, running_data, message):
+- headers = ('thread-id', 'message-id', 'date', 'from', 'subject')
+- data = {}
+- for header in headers:
+- if header == 'thread-id':
+- value = message.get_thread_id()
+- elif header == 'message-id':
+- value = message.get_message_id()
+- data['message-id-term'] = 'id:"{0}"'.format(value)
+- elif header == 'date':
+- value = str(datetime.datetime.utcfromtimestamp(
+- message.get_date()).date())
+- else:
+- value = message.get_header(header)
+- if header == 'from':
+- (value, addr) = email.utils.parseaddr(value)
+- if not value:
+- value = addr.split('@')[0]
+- data[header] = value
+- next_running_data = data.copy()
+- for header, value in data.items():
+- if header in ['message-id', 'subject']:
+- continue
+- if value == running_data.get(header, None):
+- data[header] = ''
+- return (next_running_data, data)
+-
+-
+-class HtmlPage (Page):
+- _slug_regexp = re.compile('\W+')
+-
+- def _write_header(self, views, stream):
+- super(HtmlPage, self)._write_header(views=views, stream=stream)
+- stream.write('\n')
+- for view in views:
+- if 'id' not in view:
+- view['id'] = self._slug(view['title'])
+- stream.write(
+- '- {title}
\n'.format(**view))
+- stream.write('
\n')
+-
+- def _write_view_header(self, view, stream):
+- stream.write('{title}
\n'.format(**view))
+- stream.write('\n')
+- if 'comment' in view:
+- stream.write(view['comment'])
+- stream.write('\n')
+- for line in [
+- 'The view is generated from the following query:',
+- '
',
+- '',
+- ' ',
+- view['query-string'],
+- '
',
+- '
',
+- ]:
+- stream.write(line)
+- stream.write('\n')
+-
+- def _write_threads(self, threads, stream):
+- if not threads:
+- return
+- stream.write('\n')
+- for thread in threads:
+- stream.write(' \n')
+- for message_display_data in thread:
+- stream.write((
+- ' \n'
+- ' {date} | \n'
+- ' {message-id-term} | \n'
+- '
\n'
+- ' \n'
+- ' {from} | \n'
+- ' {subject} | \n'
+- '
\n'
+- ).format(**message_display_data))
+- stream.write(' \n')
+- if thread != threads[-1]:
+- stream.write(
+- '
|
\n')
+- stream.write('
\n')
+-
+- def _message_display_data(self, *args, **kwargs):
+- running_data, display_data = super(
+- HtmlPage, self)._message_display_data(
+- *args, **kwargs)
+- if 'subject' in display_data and 'message-id' in display_data:
+- d = {
+- 'message-id': quote(display_data['message-id']),
+- 'subject': xml.sax.saxutils.escape(display_data['subject']),
+- }
+- display_data['subject'] = (
+- '{subject}'
+- ).format(**d)
+- for key in ['message-id', 'from']:
+- if key in display_data:
+- display_data[key] = xml.sax.saxutils.escape(display_data[key])
+- return (running_data, display_data)
+-
+- def _slug(self, string):
+- return self._slug_regexp.sub('-', string)
+-
+-parser = argparse.ArgumentParser(description=__doc__)
+-parser.add_argument('--text', help='output plain text format',
+- action='store_true')
+-parser.add_argument('--config', help='load config from given file',
+- metavar='PATH')
+-parser.add_argument('--list-views', help='list views',
+- action='store_true')
+-parser.add_argument('--get-query', help='get query for view',
+- metavar='VIEW')
+-
+-args = parser.parse_args()
+-
+-try:
+- config = read_config(path=args.config)
+-except ConfigError as e:
+- print(e, file=sys.stderr)
+- sys.exit(1)
+-
+-header_template = config['meta'].get('header', '''
+-
+-
+-
+- {title}
+-
+-
+-
+-{title}
+-{blurb}
+-
+-Views
+-''')
+-
+-footer_template = config['meta'].get('footer', '''
+-
+-Generated: {datetime}
+-
+-
+-''')
+-
+-now = datetime.datetime.utcnow()
+-context = {
+- 'date': now,
+- 'datetime': now.strftime('%Y-%m-%d %H:%M:%SZ'),
+- 'title': config['meta']['title'],
+- 'blurb': config['meta']['blurb'],
+- 'encoding': _ENCODING,
+- 'inter_message_padding': '0.25em',
+- 'border_radius': '0.5em',
+- }
+-
+-_PAGES['text'] = Page()
+-_PAGES['html'] = HtmlPage(
+- header=header_template.format(**context),
+- footer=footer_template.format(**context),
+- )
+-
+-if args.list_views:
+- for view in config['views']:
+- print(view['title'])
+- sys.exit(0)
+-elif args.get_query != None:
+- for view in config['views']:
+- if args.get_query == view['title']:
+- print(' and '.join(view['query']))
+- sys.exit(0)
+-else:
+- # only import notmuch if needed
+- import notmuch
+-
+-if args.text:
+- page = _PAGES['text']
+-else:
+- page = _PAGES['html']
+-
+-db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+-page.write(database=db, views=config['views'])
+diff --git a/devel/nmbug/notmuch-report b/devel/nmbug/notmuch-report
+new file mode 100755
+index 0000000..22e3b5b
+--- /dev/null
++++ b/devel/nmbug/notmuch-report
+@@ -0,0 +1,419 @@
++#!/usr/bin/python
++#
++# Copyright (c) 2011-2012 David Bremner
++#
++# dependencies
++# - python 2.6 for json
++# - argparse; either python 2.7, or install separately
++#
++# 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/ .
++
++"""Generate HTML for one or more notmuch searches.
++
++Messages matching each search are grouped by thread. Each message
++that contains both a subject and message-id will have the displayed
++subject link to the Gmane view of the message.
++"""
++
++from __future__ import print_function
++from __future__ import unicode_literals
++
++import codecs
++import collections
++import datetime
++import email.utils
++try: # Python 3
++ from urllib.parse import quote
++except ImportError: # Python 2
++ from urllib import quote
++import json
++import argparse
++import os
++import re
++import sys
++import subprocess
++import xml.sax.saxutils
++
++
++_ENCODING = 'UTF-8'
++_PAGES = {}
++
++
++if not hasattr(collections, 'OrderedDict'): # Python 2.6 or earlier
++ class _OrderedDict (dict):
++ "Just enough of a stub to get through Page._get_threads"
++ def __init__(self, *args, **kwargs):
++ super(_OrderedDict, self).__init__(*args, **kwargs)
++ self._keys = [] # record key order
++
++ def __setitem__(self, key, value):
++ super(_OrderedDict, self).__setitem__(key, value)
++ self._keys.append(key)
++
++ def values(self):
++ for key in self._keys:
++ yield self[key]
++
++
++ collections.OrderedDict = _OrderedDict
++
++
++class ConfigError (Exception):
++ """Errors with config file usage
++ """
++ pass
++
++
++def read_config(path=None, encoding=None):
++ "Read config from json file"
++ if not encoding:
++ encoding = _ENCODING
++ if path:
++ try:
++ with open(path, 'rb') as f:
++ config_bytes = f.read()
++ except IOError as e:
++ raise ConfigError('Could not read config from {}'.format(path))
++ else:
++ nmbhome = os.getenv('NMBGIT', os.path.expanduser('~/.nmbug'))
++ branch = 'config'
++ filename = 'status-config.json'
++
++ # read only the first line from the pipe
++ sha1_bytes = subprocess.Popen(
++ ['git', '--git-dir', nmbhome, 'show-ref', '-s', '--heads', branch],
++ stdout=subprocess.PIPE).stdout.readline()
++ sha1 = sha1_bytes.decode(encoding).rstrip()
++ if not sha1:
++ raise ConfigError(
++ ("No local branch '{branch}' in {nmbgit}. "
++ 'Checkout a local {branch} branch or explicitly set --config.'
++ ).format(branch=branch, nmbgit=nmbhome))
++
++ p = subprocess.Popen(
++ ['git', '--git-dir', nmbhome, 'cat-file', 'blob',
++ '{}:{}'.format(sha1, filename)],
++ stdout=subprocess.PIPE)
++ config_bytes, err = p.communicate()
++ status = p.wait()
++ if status != 0:
++ raise ConfigError(
++ ("Missing {filename} in branch '{branch}' of {nmbgit}. "
++ 'Add the file or explicitly set --config.'
++ ).format(filename=filename, branch=branch, nmbgit=nmbhome))
++
++ config_json = config_bytes.decode(encoding)
++ try:
++ return json.loads(config_json)
++ except ValueError as e:
++ if not path:
++ path = "{} in branch '{}' of {}".format(
++ filename, branch, nmbhome)
++ raise ConfigError(
++ 'Could not parse JSON from the config file {}:\n{}'.format(
++ path, e))
++
++
++class Thread (list):
++ def __init__(self):
++ self.running_data = {}
++
++
++class Page (object):
++ def __init__(self, header=None, footer=None):
++ self.header = header
++ self.footer = footer
++
++ def write(self, database, views, stream=None):
++ if not stream:
++ try: # Python 3
++ byte_stream = sys.stdout.buffer
++ except AttributeError: # Python 2
++ byte_stream = sys.stdout
++ stream = codecs.getwriter(encoding=_ENCODING)(stream=byte_stream)
++ self._write_header(views=views, stream=stream)
++ for view in views:
++ self._write_view(database=database, view=view, stream=stream)
++ self._write_footer(views=views, stream=stream)
++
++ def _write_header(self, views, stream):
++ if self.header:
++ stream.write(self.header)
++
++ def _write_footer(self, views, stream):
++ if self.footer:
++ stream.write(self.footer)
++
++ def _write_view(self, database, view, stream):
++ # sort order, default to oldest-first
++ sort_key = view.get('sort', 'oldest-first')
++ # dynamically accept all values in Query.SORT
++ sort_attribute = sort_key.upper().replace('-', '_')
++ try:
++ sort = getattr(notmuch.Query.SORT, sort_attribute)
++ except AttributeError:
++ raise ConfigError('Invalid sort setting for {}: {!r}'.format(
++ view['title'], sort_key))
++ if 'query-string' not in view:
++ query = view['query']
++ view['query-string'] = ' and '.join(query)
++ q = notmuch.Query(database, view['query-string'])
++ q.set_sort(sort)
++ threads = self._get_threads(messages=q.search_messages())
++ self._write_view_header(view=view, stream=stream)
++ self._write_threads(threads=threads, stream=stream)
++
++ def _get_threads(self, messages):
++ threads = collections.OrderedDict()
++ for message in messages:
++ thread_id = message.get_thread_id()
++ if thread_id in threads:
++ thread = threads[thread_id]
++ else:
++ thread = Thread()
++ threads[thread_id] = thread
++ thread.running_data, display_data = self._message_display_data(
++ running_data=thread.running_data, message=message)
++ thread.append(display_data)
++ return list(threads.values())
++
++ def _write_view_header(self, view, stream):
++ pass
++
++ def _write_threads(self, threads, stream):
++ for thread in threads:
++ for message_display_data in thread:
++ stream.write(
++ ('{date:10.10s} {from:20.20s} {subject:40.40s}\n'
++ '{message-id-term:>72}\n'
++ ).format(**message_display_data))
++ if thread != threads[-1]:
++ stream.write('\n')
++
++ def _message_display_data(self, running_data, message):
++ headers = ('thread-id', 'message-id', 'date', 'from', 'subject')
++ data = {}
++ for header in headers:
++ if header == 'thread-id':
++ value = message.get_thread_id()
++ elif header == 'message-id':
++ value = message.get_message_id()
++ data['message-id-term'] = 'id:"{0}"'.format(value)
++ elif header == 'date':
++ value = str(datetime.datetime.utcfromtimestamp(
++ message.get_date()).date())
++ else:
++ value = message.get_header(header)
++ if header == 'from':
++ (value, addr) = email.utils.parseaddr(value)
++ if not value:
++ value = addr.split('@')[0]
++ data[header] = value
++ next_running_data = data.copy()
++ for header, value in data.items():
++ if header in ['message-id', 'subject']:
++ continue
++ if value == running_data.get(header, None):
++ data[header] = ''
++ return (next_running_data, data)
++
++
++class HtmlPage (Page):
++ _slug_regexp = re.compile('\W+')
++
++ def _write_header(self, views, stream):
++ super(HtmlPage, self)._write_header(views=views, stream=stream)
++ stream.write('\n')
++ for view in views:
++ if 'id' not in view:
++ view['id'] = self._slug(view['title'])
++ stream.write(
++ '- {title}
\n'.format(**view))
++ stream.write('
\n')
++
++ def _write_view_header(self, view, stream):
++ stream.write('{title}
\n'.format(**view))
++ stream.write('\n')
++ if 'comment' in view:
++ stream.write(view['comment'])
++ stream.write('\n')
++ for line in [
++ 'The view is generated from the following query:',
++ '
',
++ '',
++ ' ',
++ view['query-string'],
++ '
',
++ '
',
++ ]:
++ stream.write(line)
++ stream.write('\n')
++
++ def _write_threads(self, threads, stream):
++ if not threads:
++ return
++ stream.write('\n')
++ for thread in threads:
++ stream.write(' \n')
++ for message_display_data in thread:
++ stream.write((
++ ' \n'
++ ' {date} | \n'
++ ' {message-id-term} | \n'
++ '
\n'
++ ' \n'
++ ' {from} | \n'
++ ' {subject} | \n'
++ '
\n'
++ ).format(**message_display_data))
++ stream.write(' \n')
++ if thread != threads[-1]:
++ stream.write(
++ '
|
\n')
++ stream.write('
\n')
++
++ def _message_display_data(self, *args, **kwargs):
++ running_data, display_data = super(
++ HtmlPage, self)._message_display_data(
++ *args, **kwargs)
++ if 'subject' in display_data and 'message-id' in display_data:
++ d = {
++ 'message-id': quote(display_data['message-id']),
++ 'subject': xml.sax.saxutils.escape(display_data['subject']),
++ }
++ display_data['subject'] = (
++ '{subject}'
++ ).format(**d)
++ for key in ['message-id', 'from']:
++ if key in display_data:
++ display_data[key] = xml.sax.saxutils.escape(display_data[key])
++ return (running_data, display_data)
++
++ def _slug(self, string):
++ return self._slug_regexp.sub('-', string)
++
++parser = argparse.ArgumentParser(description=__doc__)
++parser.add_argument('--text', help='output plain text format',
++ action='store_true')
++parser.add_argument('--config', help='load config from given file',
++ metavar='PATH')
++parser.add_argument('--list-views', help='list views',
++ action='store_true')
++parser.add_argument('--get-query', help='get query for view',
++ metavar='VIEW')
++
++args = parser.parse_args()
++
++try:
++ config = read_config(path=args.config)
++except ConfigError as e:
++ print(e, file=sys.stderr)
++ sys.exit(1)
++
++header_template = config['meta'].get('header', '''
++
++
++
++ {title}
++
++
++
++{title}
++{blurb}
++
++Views
++''')
++
++footer_template = config['meta'].get('footer', '''
++
++Generated: {datetime}
++
++
++''')
++
++now = datetime.datetime.utcnow()
++context = {
++ 'date': now,
++ 'datetime': now.strftime('%Y-%m-%d %H:%M:%SZ'),
++ 'title': config['meta']['title'],
++ 'blurb': config['meta']['blurb'],
++ 'encoding': _ENCODING,
++ 'inter_message_padding': '0.25em',
++ 'border_radius': '0.5em',
++ }
++
++_PAGES['text'] = Page()
++_PAGES['html'] = HtmlPage(
++ header=header_template.format(**context),
++ footer=footer_template.format(**context),
++ )
++
++if args.list_views:
++ for view in config['views']:
++ print(view['title'])
++ sys.exit(0)
++elif args.get_query != None:
++ for view in config['views']:
++ if args.get_query == view['title']:
++ print(' and '.join(view['query']))
++ sys.exit(0)
++else:
++ # only import notmuch if needed
++ import notmuch
++
++if args.text:
++ page = _PAGES['text']
++else:
++ page = _PAGES['html']
++
++db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
++page.write(database=db, views=config['views'])
+--
+2.1.0.60.g85f0837
+
--
2.26.2