X-Git-Url: https://git.madduck.net/code/mailplate.git/blobdiff_plain/27ca9e642e5f289e874095eb0217dd14c2c902ef..2e14575539d313d75020defedcdb115b8792e1c0:/mailplate?ds=sidebyside diff --git a/mailplate b/mailplate index 4d0b28c..490ba8e 100755 --- a/mailplate +++ b/mailplate @@ -1,51 +1,25 @@ #!/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 +# Copyright © martin f. krafft # 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 ' +__copyright__ = 'Copyright © ' + __author__ +__licence__ = 'Artistic Licence 2.0' + import email import os import posix @@ -79,6 +53,7 @@ HELPER_SLOT_TRAILER = ')' # character ending a helper slot 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' @@ -88,7 +63,14 @@ 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): @@ -110,7 +92,7 @@ def exec_command(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): @@ -125,7 +107,7 @@ 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 @@ -162,6 +144,50 @@ vars = {} headers = {} payload = None +### +### COMMAND LINE PARSING +### + +parser = OptionParser() +parser.prog = __name__ +parser.version = __version__ +parser.description = __description__ +parser.usage = '%prog [options] ' +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 ### @@ -170,18 +196,21 @@ CONFFILE = os.path.expanduser(CONFFILE) 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(): @@ -196,7 +225,7 @@ if not os.path.exists(CONFFILE): 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 @@ -208,43 +237,30 @@ for key in config.keys(): 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] ' -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 @@ -265,24 +281,27 @@ for arg in args: # 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) ### @@ -300,7 +319,7 @@ 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 @@ -325,12 +344,27 @@ if options.auto: 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 @@ -348,10 +382,14 @@ for line in templ: 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() @@ -425,7 +463,7 @@ outf.close() 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)