#!/usr/bin/env python ## ## Name: rsig ## Purpose: Randomize e-mail signature with a quotation. ## Author: M. J. Fromberger ## Info: $Id: rsig 810 2008-10-10 13:03:16Z sting $ ## ## Copyright (C) 1994-2008 Michael J. Fromberger. ## ## Permission is hereby granted, free of charge, to any person obtaining ## a copy of this software and associated documentation files (the ## "Software"), to deal in the Software without restriction, including ## without limitation the rights to use, copy, modify, merge, publish, ## distribute, sublicense, and/or sell copies of the Software, and to ## permit persons to whom the Software is furnished to do so, subject to ## the following conditions: ## ## The above copyright notice and this permission notice shall be ## included in all copies or substantial portions of the Software. ## ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. ## IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY ## CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, ## TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE ## SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## from __future__ import with_statement import cgi, getopt, os, plistlib, random, re, sys, subprocess, uuid # {{ usage(verbose) def usage(verbose = False): print >> sys.stderr, "Usage: rsig [options]" if verbose: print >> sys.stderr, "\nCommand-line options:\n" \ " --base/-b : base signature tag\n" \ " --copy/-c : copy output to pasteboard.\n" \ " --dashes/-d : include signature dashes (-- )\n" \ " --help/-h : display this help message\n" \ " --output/-o : write output to this file" \ " ('-' = stdout)\n" \ " --pad/-p : insert blank before quotation\n" \ " --print/-P : print quotation and exit\n" \ " --quotes/-q : path of quotations file\n" \ " --silent/-s : suppress output entirely\n" \ " --xml/-x : write output in XML for Mail.app\n" else: print >> sys.stderr, " [use `rsig --help' for options]" # }} # {{ main(argv) def main(argv): # Where quotations may be found quote_file = os.getenv('RSIG_QUOTES', os.path.expanduser('~/.rsigquotes')) # Where the base signature content may be found base_file = os.getenv('RSIG_BASE', os.path.expanduser('~/.rsigbase')) # Where the output signature file should be written sig_file = os.getenv('RSIG_OUTPUT', os.path.expanduser('~/.sig')) # How quotations are delimited in the quotes file # Edit this regular expression as needed quote_delim = re.compile(r'\n\.\n') # Configuration variables cf_basetag = None # selector for base signature text cf_dashes = False # include signature dashes? cf_extra = False # add extra line before quotation? cf_save = True # write a signature file? cf_copy = False # copy to pasteboard? cf_format = 'text' # output format, 'none', 'text' or 'xml' try: opts, args = getopt.gnu_getopt(argv, 'b:cdf:hno:pPq:sx', ('base=', 'copy', 'dashes', 'help', 'output=', 'pad', 'print', 'quotes=', 'silent', 'xml')) except getopt.GetoptError, e: print >> sys.stderr, "Error in command line: %s." % e usage() sys.exit(1) for opt, arg in opts: if opt in ('--base', '-b'): cf_basetag = arg elif opt in ('--copy', '-c'): cf_copy = True elif opt in ('--dashes', '-d'): cf_dashes = True elif opt in ('--help', '-h'): usage(True) sys.exit(0) elif opt in ('--output', '-o'): sig_file = arg elif opt in ('--pad', '-p'): cf_extra = True elif opt in ('--print', '-P'): cf_save = False elif opt in ('--quotes', '-q'): quote_file = arg elif opt in ('--silent', '-s'): cf_format = 'none' elif opt in ('--xml', '-x'): cf_format = 'xml' # The quotations file should be plain text, with a '.' on a line # by itself between each pair of quotations. try: quotes = read_qfile(quote_file) if quotes and quotes[-1].strip() == '': quotes.pop() except (IOError, OSError), e: print >> sys.stderr, 'Error reading quotations file "%s":\n' \ '-- %s' % (quote_file, e) sys.exit(1) quote = random.choice(quotes) if not cf_save: if cf_copy: send_to_pasteboard(quote) print quote return # The base file should be plain text, with a '.' on a line by # itself between each pair of entries. The first line of each # entry may be a label such as "@foo", which can be used to # identify the entry; if no entry is specified, the first one in # the file is used. # # Trailing whitespace is removed from the entry before use. # try: base = read_base(base_file, cf_basetag) except (IOError, OSError), e: print >> sys.stderr, 'Error reading base file "%s":\n' \ '-- %s' % (base_file, e) sys.exit(1) # Build the signature content sig_text = '' if cf_dashes: sig_text += '-- \n' sig_text += base + '\n' if cf_extra: sig_text += '\n' sig_text += quote + '\n' try: if cf_copy: send_to_pasteboard(sig_text) if cf_format == 'xml': write_xml(sig_text) elif cf_format == 'text': if sig_file == '-': ofp = sys.stdout else: ofp = file(sig_file, 'w') write_text(ofp, sig_text) ofp.close() except (IOError, OSError), e: print >> sys.stderr, 'Error writing signature file "%s":\n' \ '-- %s' % (sig_file, e) sys.exit(2) # }} # {{ read_base(path, tag) def read_base(path, tag): """Fetch the base signature entry specified by the given tag, using the first entry in the file if tag is None or does not match any other entry in the file. """ lab_re = re.compile('^@(\w+)\n') bases = read_qfile(path) bmap = {} for pos, elt in enumerate(bases): m = lab_re.match(elt) if m: bmap[m.group(1)] = pos bases[pos] = elt.split('\n', 1)[1] return bases[bmap.get(tag, 0)].rstrip() # }} # {{ read_qfile(path[, delim]) def read_qfile(path, delim = re.compile(r'\n\.\n')): """Load a file containing elements separated by single-line delimiters, and return the resulting elements as a list. """ with file(path, 'rU') as fp: elts = delim.split(fp.read()) if elts and elts[-1].strip() == '': elts.pop() return elts # }} # {{ send_to_pasteboard(txt) def send_to_pasteboard(txt): """On a MacOS system with the "pbcopy" utility, write the contents of the given text buffer to it, so that the user can paste the results somewhere else. Errors are suppressed. """ try: p = subprocess.Popen(['pbcopy'], stdin = subprocess.PIPE, close_fds = True) except OSError: return p.stdin.write(txt) p.stdin.close() p.wait() # }} # {{ write_xml(sig) def write_xml(sig): """This function writes a signature that can be read by Apple's Mail application. Unfortunately, the process of doing so is a bit bizarre; the signature has to be written to one file in XML format, while a table-of-contents must be written to another XML file. ~/Library/Mail/Signatures/SignaturesByAccount.plist This is the table-of-contents file. It is modified to add a new entry for the random signature, or created as necessary. ~/Library/Mail/Signatures/.webarchive This is the text of the signature itself. The same archive is reused for each new signature generated. To use: Run "rsig -x" to generate the initial signature, then go into Mail.app and drag the "Random Signature" entry from the list of all signatures to each account where you want it to be available. This is a pain, but need only be done once. """ sig_base = os.path.expanduser('~/Library/Mail/Signatures') if not os.path.isdir(sig_base): os.mkdir(sig_base, 0700) # Load existing signature data if it exists, or else create it. sig_list = os.path.join(sig_base, 'SignaturesByAccount.plist') if not os.path.isfile(sig_list): sig_data = {'AllSignaturesKey': [], 'FileVersionNumber': 6} else: fp = file(sig_list, 'r') try: sig_data = plistlib.readPlist(fp) if 'AllSignaturesKey' not in sig_data: sig_data['AllSignaturesKey'] = [] finally: fp.close() # Look for an existing "random signature", or add a new one. for elt in sig_data['AllSignaturesKey']: if elt.get('SignatureName') == 'Random Signature': elt['SignatureIsRich'] = True break else: elt = {'SignatureIsRich': False, 'SignatureName': 'Random Signature', 'SignatureUniqueId': str(uuid.uuid1()).upper()} sig_data['AllSignaturesKey'].append(elt) plistlib.writePlist(sig_data, sig_list) # Construct the signature file itself, and write it out. sig_data = cgi.escape(sig) \ .replace(' ', ' ') \ .replace('\n', '
\n') code = {'WebMainResource': {'WebResourceData': plistlib.Data(sig_data), 'WebResourceMIMEType': 'text/html', 'WebResourceTextEncodingName': 'UTF-8', 'WebResourceURL': 'data:'}} sig_file = os.path.join(sig_base, '%s.webarchive' % elt['SignatureUniqueId']) fp = file(sig_file, 'w') try: plistlib.writePlist(code, fp) finally: fp.close() # }} # {{ write_text(ofp, sig) def write_text(ofp, sig): """Write a plain-text signature to a plain-text output file.""" ofp.write(sig) # }} if __name__ == "__main__": main(sys.argv[1:]) # Here there be dragons