#!/usr/bin/python
# -*- coding: utf-8 -*-
#
-# mailplate — apply templates to mail drafts.
+# mailplate — reformat mail drafts according to templates
#
-# This script reformats mail drafts according to a given template. The
-# template may be specified on the command line, but mailplate can also use
-# control information from the template files to automatically select an
-# appropriate template (--auto). A selection menu feature is planned (--menu).
-#
-# Applying a template means obtainined select data from an existing mail
-# message (unless --new is specified) and to fill it into appropriate slots in
-# the template. Messages are processed in three parts: headers, body, and
-# signature.
-#
-# The template can define two types of headers: mandatory and preservatory.
-# Mandatory headers take precedence over headers in the existing message and
-# thus overwrite them. Preservatory headers instruct mailplate to port their
-# data from the existing mail message. Headers in the existing message but not
-# defined in the template are dropped, unless --keep-unknown is given.
-#
-# Body and signature are separated by '-- '. If this sentinel is not found,
-# no signature is extracted.
-#
-# Templates can be interpolated and data filled into slots. Helper slots are
-# filled with the output of helper commands (which must be defined in the
-# configuration), environment variable slots are just that, and mail variable
-# slots can be filled with data obtained by running regexps or commands over
-# the message.
-#
-# This script can be run in multiple ways:
-#
-# As a filter, it applies a template to data from stdin and writes the result
-# to stdout.
-#
-# Given a file, it modifies the file, unless it cannot write to the file, in
-# which case it writes to stdout.
-#
-# When --editor is passed, the script spawns sensible-editor on the result. It
-# may thus be used as the editor for your mail user agent.
+# Please see the mailplate(1) manpage or the homepage for more information:
+# http://madduck.net/code/mailplate/
#
# TODO: if headers like From are absent from the mail, they should not be kept
# but replaced with a default.
#
-# Copyright © martin f. krafft <madduck@debian.org>
+# Copyright © martin f. krafft <madduck@madduck.net>
# Released under the terms of the Artistic Licence 2.0
#
+__name__ = 'mailplate'
+__description__ = 'reformat mail drafts according to templates'
+__version__ = '0.1'
+__author__ = 'martin f. krafft <madduck@madduck.net>'
+__copyright__ = 'Copyright © ' + __author__
+__licence__ = 'Artistic Licence 2.0'
+
import email
import os
import posix
STD_HEADERS = ('From', 'To', 'Cc', 'Bcc', 'Subject', 'Reply-To', 'In-Reply-To')
KEEP_HEADERS = { 'KEEP_FROM_HEADER' : STD_HEADERS[:1]
, 'KEEP_STD_HEADERS' : STD_HEADERS[1:]
+ , 'KEEP_ALL_HEADERS' : STD_HEADERS
}
SIG_DELIM='\n-- \n'
###
def err(s):
- sys.stderr.write(s + '\n')
+ sys.stderr.write('E: ' + s + '\n')
+
+def warn(s):
+ sys.stderr.write('W: ' + s + '\n')
+
+def info(s):
+ if not options.verbose: return
+ sys.stderr.write('I: ' + s + '\n')
# obtain a regexp from a line, run it, return score/dict if matched
def exec_regexp(line, rawmsg, name):
else:
return 0
except OSError:
- err("W: command '%s' (template '%s') failed to run." % (r, name))
+ warn("command '%s' (template '%s') failed to run." % (r, name))
return 0
def interpolate_helpers(s):
out = proc.communicate()[0]
s = s[:helper_begin] + out.strip() + s[helper_end+1:]
except KeyError:
- err('E: unknown helper: ' + helper)
+ err('unknown helper: ' + helper)
sys.exit(posix.EX_DATAERR)
return s
headers = {}
payload = None
+###
+### COMMAND LINE PARSING
+###
+
+parser = OptionParser()
+parser.prog = __name__
+parser.version = __version__
+parser.description = __description__
+parser.usage = '%prog [options] <message>'
+parser.add_option('-a', '--auto', dest='auto',
+ default=False, action='store_true',
+ help='turn on template auto-discovery')
+parser.add_option('-m', '--menu', dest='menu',
+ default=False, action='store_true',
+ help='choose from a list of templates (not yet implemented)')
+parser.add_option('-n', '--new', dest='new',
+ default=False, action='store_true',
+ help='create a new message')
+parser.add_option('-e', '--editor', dest='edit',
+ default=False, action='store_true',
+ help='spawn editor once template is applied')
+parser.add_option('-k', '--keep-unknown', dest='keep_unknown',
+ default=False, action='store_true',
+ help='preserve mail headers not specified in template')
+parser.add_option('-v', '--verbose', dest='verbose',
+ default=False, action='store_true',
+ help='write informational messages to stderr')
+parser.add_option('-d', '--debug', dest='debug',
+ default=False, action='store_true',
+ help='start a debugger after initialisation')
+parser.add_option('-V', '--version', dest='version',
+ default=False, action='store_true',
+ help='display version information')
+
+options, args = parser.parse_args()
+
+if options.version:
+ print __name__, __version__ + ' — ' + __description__
+ print
+ print 'Written by ' + __author__
+ print __copyright__
+ print 'Released under the ' + __licence__
+ sys.exit(posix.EX_OK)
+
###
### CONFIGURATION FILE PARSING
###
MAILPLATEDIR = os.path.expanduser(MAILPLATEDIR)
# defaults
-config = { 'default_template' : ''
+config = { 'default_template' : 'default'
, 'template_path' : TEMPLATEDIR
}
helpers = { 'get_quote' : 'fortune -s' }
if not os.path.exists(CONFFILE):
# conffile does not exist, let's create it with defaults.
+ options.verbose = True
if not os.path.isdir(MAILPLATEDIR):
+ info('configuration directory not found, creating: ' + MAILPLATEDIR)
os.mkdir(MAILPLATEDIR, 0700)
if not os.path.isfile(CONFFILE):
+ info('creating a default configuration file: ' + CONFFILE)
f = file(CONFFILE, 'w')
f.write('# mailplate configuration\n[%s]\n' % SECTION_GENERAL)
for kvpair in config.iteritems():
f.close()
if not os.access(CONFFILE, os.R_OK):
- err('E: cannot read configuration file: %s' % CONFFILE)
+ err('cannot read configuration file: %s' % CONFFILE)
sys.exit(posix.EX_OSFILE)
# now parse
try:
config[key] = parser.get(SECTION_GENERAL, key)
except ConfigParser.NoSectionError, ConfigParser.MissingSectionHeaderError:
- err("E: no section '%s' in %s" % (SECTION_GENERAL, CONFFILE))
+ err("no section '%s' in %s" % (SECTION_GENERAL, CONFFILE))
sys.exit(posix.EX_CONFIG)
+ except ConfigParser.NoOptionError:
+ continue
except ConfigParser.DuplicateSectionError, ConfigParser.ParseError:
- err('E: parse error on %s' % CONFFILE)
+ err('parse error on %s' % CONFFILE)
sys.exit(posix.EX_CONFIG)
# all HELPERS into the helpers dict
helpers.update(parser.items(SECTION_HELPERS))
TPATH = os.path.expanduser(config['template_path'])
-
-###
-### COMMAND LINE PARSING
-###
-
-parser = OptionParser()
-parser.usage = '%prog [options] <message>'
-parser.add_option('-a', '--auto', dest='auto',
- default=False, action='store_true',
- help='turn on template auto-discovery')
-parser.add_option('-m', '--menu', dest='menu',
- default=False, action='store_true',
- help='choose from a list of template')
-parser.add_option('-n', '--new', dest='new',
- default=False, action='store_true',
- help='create a new message')
-parser.add_option('-e', '--editor', dest='edit',
- default=False, action='store_true',
- help='spawn editor once template is applied')
-parser.add_option('-k', '--keep-unknown', dest='keep_unknown',
- default=False, action='store_true',
- help='preserve mail headers not specified in template')
-parser.add_option('-d', '--debug', dest='debug',
- default=False, action='store_true',
- help='start a debugger after initialisation')
-
-options, args = parser.parse_args()
+if not os.path.isdir(TPATH):
+ info('creating template directory: ' + TPATH)
+ os.mkdir(TPATH, 0700)
+
+default_templname = config['default_template']
+if default_templname is not None:
+ default_templpath = os.path.join(TPATH, default_templname)
+ if not os.path.isfile(default_templpath):
+ info('creating the default template: ' + default_templpath)
+ f = file(default_templpath, 'w')
+ f.write('@KEEP_STD_HEADERS\n\n@KEEP_BODY\n')
+ f.close()
if options.debug:
import pdb
# argument referenced an existing template
templname = arg
else:
- err('E: unknown argument: %s' % arg)
+ err('unknown argument, and cannot find a template by this name: %s' % arg)
sys.exit(posix.EX_USAGE)
# sanity checks
if options.auto and options.menu:
- err('E: cannot combine --auto and --menu')
+ err('cannot combine --auto and --menu')
sys.exit(posix.EX_USAGE)
elif (options.auto or options.menu) and templname:
- err('E: cannot specify a template with --auto or --menu')
+ err('cannot specify a template with --auto or --menu')
sys.exit(posix.EX_USAGE)
elif not templname and not (options.auto or options.menu):
- err('E: no template specified')
- sys.exit(posix.EX_USAGE)
+ if default_templname is not None:
+ templname = default_templname
+ else:
+ err('no template specified')
+ sys.exit(posix.EX_USAGE)
elif options.menu:
- err('E: --menu mode not yet implemented')
+ err('--menu mode not yet implemented')
sys.exit(posix.EX_USAGE)
###
# read message into buffer, or preinitialise the buffer if --new is given
if options.new:
- rawmsg = '\n'.join((header + ': ' for k in STD_HEADERS)) + '\n'
+ rawmsg = '\n'.join((header + ': ' for header in STD_HEADERS)) + '\n'
else:
rawmsg = ''.join(inf.readlines())
if options.auto:
- best_score = (0, config['default_template'], {})
+ best_score = (0, default_templname, {})
for tf in os.listdir(TPATH):
tp = os.path.join(TPATH, tf)
if not os.path.isfile(tp) or not os.access(tp, os.R_OK): continue
best_score = (score, tf, vars)
templname = best_score[1]
- print >>sys.stderr, \
- 'I: Chose profile %s with score %d.' % (templname, best_score[0])
+
+ if templname is None:
+ err('could not determine a template to use and no default is set')
+ sys.exit(posix.EX_CONFIG)
+
+ info('chose profile %s with score %d.' % (templname, best_score[0]))
vars = best_score[2]
# now read in the template
-templ = file(os.path.join(TPATH, templname), 'r', 1)
+templpath = os.path.join(TPATH, templname)
+
+if not os.path.isfile(templpath):
+ err('not a template: ' + templpath)
+ sys.exit(posix.EX_OSFILE)
+
+elif not os.access(templpath, os.R_OK):
+ err('template ' + templpath + ' could not be read.')
+ sys.exit(posix.EX_OSFILE)
+
+templ = file(templpath, 'r', 1)
+
for line in templ:
if not options.auto and line[0] == REGEXPCHAR:
# obtain variables from the regexps
l = line[:-1]
if len(l) == 0:
payload = '' # end of headers
- elif l[0] == KEEP_SLOT_LEADER and KEEP_HEADERS.has_key(l[1:]):
- # found predefined header slot keyword
- for header in KEEP_HEADERS[l[1:]]:
- headers[header.lower()] = (header, _keep_header)
+ elif l[0] == KEEP_SLOT_LEADER:
+ if KEEP_HEADERS.has_key(l[1:]):
+ # found predefined header slot keyword
+ for header in KEEP_HEADERS[l[1:]]:
+ headers[header.lower()] = (header, _keep_header)
+ else:
+ err('unknown header slot ' + l + ' found')
+ sys.exit(posix.EX_CONFIG)
else:
header, content = l.split(':', 1)
content = content.strip()
if options.edit:
# finally, spawn the editor, if we wrote into a file
if outfname is None:
- err('E: cannot use --edit without an output file.')
+ err('cannot use --edit without an output file.')
sys.exit(posix.EX_USAGE)
os.execlp('sensible-editor', 'sensible-editor', outfname)