#!/usr/bin/python
#
# Copyright (c) 2011-2012 David Bremner <david@tethera.net>
-# License: Same as notmuch
+#
# 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 collections
import datetime
import email.utils
-import locale
try: # Python 3
from urllib.parse import quote
except ImportError: # Python 2
import re
import sys
import subprocess
+import xml.sax.saxutils
-_ENCODING = locale.getpreferredencoding() or sys.getdefaultencoding()
+_ENCODING = 'UTF-8'
_PAGES = {}
super(_OrderedDict, self).__setitem__(key, value)
self._keys.append(key)
- def __values__(self):
+ def values(self):
for key in self._keys:
yield self[key]
byte_stream = sys.stdout.buffer
except AttributeError: # Python 2
byte_stream = sys.stdout
- stream = codecs.getwriter(encoding='UTF-8')(stream=byte_stream)
+ 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)
return
stream.write('<table>\n')
for thread in threads:
+ stream.write(' <tbody>\n')
for message_display_data in thread:
stream.write((
- '<tr>\n'
- ' <td>{date}</td>\n'
- ' <td><code>{message-id-term}</code></td>\n'
- '</tr>\n'
- '<tr>\n'
- ' <td>{from}</td>\n'
- ' <td>{subject}</td>\n'
- '</tr>\n'
+ ' <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('<tr><td colspan="2"><br /></td></tr>\n')
+ stream.write(
+ ' <tbody><tr><td colspan="2"><br /></td></tr></tbody>\n')
stream.write('</table>\n')
def _message_display_data(self, *args, **kwargs):
if 'subject' in display_data and 'message-id' in display_data:
d = {
'message-id': quote(display_data['message-id']),
- 'subject': display_data['subject'],
+ '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)
-
-_PAGES['text'] = Page()
-_PAGES['html'] = HtmlPage(
- header='''<!DOCTYPE html>
-<html lang="en">
-<head>
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-<title>Notmuch Patches</title>
-</head>
-<body>
-<h2>Notmuch Patches</h2>
-<p>
-Generated: {date}<br />
-For more infomation see <a href="http://notmuchmail.org/nmbug">nmbug</a>
-</p>
-<h3>Views</h3>
-'''.format(date=datetime.datetime.utcnow().date()),
- footer='</body>\n</html>\n',
- )
-
-
-parser = argparse.ArgumentParser()
+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',
config = read_config(path=args.config)
+_PAGES['text'] = Page()
+_PAGES['html'] = HtmlPage(
+ header='''<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset={encoding}" />
+ <title>{title}</title>
+ <style media="screen" type="text/css">
+ table {{
+ border-spacing: 0;
+ }}
+ tr.message-first td {{
+ padding-top: {inter_message_padding};
+ }}
+ tr.message-last td {{
+ padding-bottom: {inter_message_padding};
+ }}
+ td {{
+ padding-left: {border_radius};
+ padding-right: {border_radius};
+ }}
+ tr:first-child td:first-child {{
+ border-top-left-radius: {border_radius};
+ }}
+ tr:first-child td:last-child {{
+ border-top-right-radius: {border_radius};
+ }}
+ tr:last-child td:first-child {{
+ border-bottom-left-radius: {border_radius};
+ }}
+ tr:last-child td:last-child {{
+ border-bottom-right-radius: {border_radius};
+ }}
+ tbody:nth-child(4n+1) tr td {{
+ background-color: #ffd96e;
+ }}
+ tbody:nth-child(4n+3) tr td {{
+ background-color: #bce;
+ }}
+ hr {{
+ border: 0;
+ height: 1px;
+ color: #ccc;
+ background-color: #ccc;
+ }}
+ </style>
+</head>
+<body>
+<h2>{title}</h2>
+{blurb}
+</p>
+<h3>Views</h3>
+'''.format(title=config['meta']['title'],
+ blurb=config['meta']['blurb'],
+ encoding=_ENCODING,
+ inter_message_padding='0.25em',
+ border_radius='0.5em'),
+ footer='''
+<hr>
+<p>Generated: {date}
+</body>
+</html>
+'''.format(date=datetime.datetime.utcnow().date())
+ )
+
if args.list_views:
for view in config['views']:
print(view['title'])