+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 status-config.json in branch '{branch}' of"
+ '{nmbgit}. Add the file or explicitly set --config.'
+ ).format(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('<ul>\n')
+ for view in views:
+ if 'id' not in view:
+ view['id'] = self._slug(view['title'])
+ stream.write(
+ '<li><a href="#{id}">{title}</a></li>\n'.format(**view))
+ stream.write('</ul>\n')
+
+ def _write_view_header(self, view, stream):
+ stream.write('<h3 id="{id}">{title}</h3>\n'.format(**view))
+ stream.write('<p>\n')
+ if 'comment' in view:
+ stream.write(view['comment'])
+ stream.write('\n')
+ for line in [
+ 'The view is generated from the following query:',
+ '</p>',
+ '<p>',
+ ' <code>',
+ view['query-string'],
+ ' </code>',
+ '</p>',
+ ]:
+ stream.write(line)
+ stream.write('\n')
+
+ def _write_threads(self, threads, stream):
+ if not threads:
+ return
+ stream.write('<table>\n')
+ for thread in threads:
+ stream.write(' <tbody>\n')
+ for message_display_data in thread:
+ stream.write((
+ ' <tr class="message-first">\n'
+ ' <td>{date}</td>\n'
+ ' <td><code>{message-id-term}</code></td>\n'
+ ' </tr>\n'
+ ' <tr class="message-last">\n'
+ ' <td>{from}</td>\n'
+ ' <td>{subject}</td>\n'
+ ' </tr>\n'
+ ).format(**message_display_data))
+ stream.write(' </tbody>\n')
+ if thread != threads[-1]:
+ stream.write(
+ ' <tbody><tr><td colspan="2"><br /></td></tr></tbody>\n')
+ stream.write('</table>\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'] = (
+ '<a href="http://mid.gmane.org/{message-id}">{subject}</a>'
+ ).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__)