diff --git ConfParser.py ConfParser.py index df14743..636a4c3 100755 --- ConfParser.py +++ ConfParser.py @@ -41,7 +41,7 @@ This module is similar, except: I welcome questions and comments at . ''' -__version__ = '3.1' +__version__ = '3.1-filter3-20180804' __author__ = 'Charles Cazabon ' # @@ -49,11 +49,14 @@ __author__ = 'Charles Cazabon ' # import string -import UserDict import sys import shlex -import cStringIO +import io from types import * +try: + from UserDict import UserDict +except ImportError: + from collections import UserDict # @@ -124,15 +127,15 @@ def log (msg): # ####################################### -class SmartDict (UserDict.UserDict): +class SmartDict (UserDict): '''Dictionary class which handles lists and singletons intelligently. ''' ####################################### def __init__ (self, initialdata = {}): '''Constructor. ''' - UserDict.UserDict.__init__ (self, {}) - for (key, value) in initialdata.items (): + UserDict.__init__ (self, {}) + for (key, value) in list(initialdata.items ()): self.__setitem (key, value) ####################################### @@ -144,14 +147,14 @@ class SmartDict (UserDict.UserDict): if len (value) == 1: return value[0] return value - except KeyError, txt: - raise KeyError, txt + except KeyError as txt: + raise KeyError(txt) ####################################### def __setitem__ (self, key, value): ''' ''' - if type (value) in (ListType, TupleType): + if type (value) in (list, tuple): self.data[key] = list (value) else: self.data[key] = [value] @@ -176,17 +179,17 @@ class ConfParser: self.__defaults = SmartDict () try: - for key in defaults.keys (): + for key in list(defaults.keys ()): self.__defaults[key] = defaults[key] except AttributeError: - raise ParsingError, 'defaults not a dictionary (%s)' % defaults + raise ParsingError('defaults not a dictionary (%s)' % defaults) ####################################### def read (self, filelist): '''Read configuration file(s) from list of 1 or more filenames. ''' - if type (filelist) not in (ListType, TupleType): + if type (filelist) not in (list, tuple): filelist = [filelist] try: @@ -196,8 +199,8 @@ class ConfParser: self.__rawdata = self.__rawdata + f.readlines () f.close () - except IOError, txt: - raise ParsingError, 'error reading configuration file (%s)' % txt + except IOError as txt: + raise ParsingError('error reading configuration file (%s)' % txt) self.__parse () return self @@ -206,8 +209,8 @@ class ConfParser: def __parse (self): '''Parse the read-in configuration file. ''' - config = string.join (self.__rawdata, '\n') - f = cStringIO.StringIO (config) + config = u'\n'.join (self.__rawdata) + f = io.StringIO (config) lex = shlex.shlex (f) lex.wordchars = lex.wordchars + '|/.,$^\\():;@-+?<>!%&*`~' section_name = '' @@ -221,38 +224,36 @@ class ConfParser: if not (section_name): if token != '[': - raise ParsingError, 'expected section start, got %s' % token + raise ParsingError('expected section start, got %s' % token) section_name = '' while 1: token = lex.get_token () if token == ']': break if token == '': - raise ParsingError, 'expected section end, hit EOF' + raise ParsingError('expected section end, hit EOF') if section_name: section_name = section_name + ' ' section_name = section_name + token if not section_name: - raise ParsingError, 'expected section name, got nothing' + raise ParsingError('expected section name, got nothing') section = SmartDict () # Collapse case on section names - section_name = string.lower (section_name) + section_name = section_name.lower () if section_name in self.__sectionlist: - raise DuplicateSectionError, \ - 'duplicate section (%s)' % section_name + raise DuplicateSectionError('duplicate section (%s)' % section_name) section['__name__'] = section_name continue if token == '=': - raise ParsingError, 'expected option name, got =' + raise ParsingError('expected option name, got =') if token == '[': # Start new section lex.push_token (token) if section_name in self.__sectionlist: - raise DuplicateSectionError, \ - 'duplicate section (%s)' % section_name + raise DuplicateSectionError('duplicate section (%s)' % section_name) if section['__name__'] == 'default': self.__defaults.update (section) self.__sectionlist.append (section_name) @@ -264,18 +265,18 @@ class ConfParser: option_name = token token = lex.get_token () if token != '=': - raise ParsingError, 'Expected =, got %s' % token + raise ParsingError('Expected =, got %s' % token) token = lex.get_token () if token in ('[', '='): - raise ParsingError, 'expected option value, got %s' % token + raise ParsingError('expected option value, got %s' % token) option_value = token if option_value[0] in ('"', "'") and option_value[0] == option_value[-1]: option_value = option_value[1:-1] - if section.has_key (option_name): - if type (section[option_name]) == ListType: + if option_name in section: + if type (section[option_name]) == list: section[option_name].append (option_value) else: section[option_name] = [section[option_name], option_value] @@ -287,15 +288,14 @@ class ConfParser: # Done parsing if section_name: if section_name in self.__sectionlist: - raise DuplicateSectionError, \ - 'duplicate section (%s)' % section_name + raise DuplicateSectionError('duplicate section (%s)' % section_name) if section['__name__'] == 'default': self.__defaults.update (section) self.__sectionlist.append (section_name) self.__sections.append (section.copy ()) if not self.__sectionlist: - raise MissingSectionHeaderError, 'no section headers in file' + raise MissingSectionHeaderError('no section headers in file') ####################################### def defaults (self): @@ -308,7 +308,7 @@ class ConfParser: '''Indicates whether the named section is present in the configuration. The default section is not acknowledged. ''' - section = string.lower (section) + section = section.lower () if section not in self.sections (): return 0 return 1 @@ -333,12 +333,12 @@ class ConfParser: '''Return list of options in section. ''' try: - s = self.__sectionlist.index (string.lower (section)) + s = self.__sectionlist.index (section.lower ()) except ValueError: - raise NoSectionError, 'missing section: "%s"' % section + raise NoSectionError('missing section: "%s"' % section) - return self.__sections[s].keys () + return list(self.__sections[s].keys ()) ####################################### def get (self, section, option, raw=0, _vars={}): @@ -349,19 +349,19 @@ class ConfParser: ''' try: - s = self.__sectionlist.index (string.lower (section)) + s = self.__sectionlist.index (section.lower ()) options = self.__sections[s] except ValueError: - raise NoSectionError, 'missing section (%s)' % section + raise NoSectionError('missing section (%s)' % section) expand = self.__defaults.copy () expand.update (_vars) - if not options.has_key (option): - if expand.has_key (option): + if option not in options: + if option in expand: return expand[option] - raise NoOptionError, 'section [%s] missing option (%s)' \ - % (section, option) + raise NoOptionError('section [%s] missing option (%s)' \ + % (section, option)) rawval = options[option] @@ -370,7 +370,7 @@ class ConfParser: try: value = [] - if type (rawval) != ListType: + if type (rawval) != list: rawval = [rawval] for part in rawval: try: @@ -381,12 +381,12 @@ class ConfParser: if len (value) == 1: return value[0] return value - except KeyError, txt: - raise NoOptionError, 'section [%s] missing option (%s)' \ - % (section, option) - except TypeError, txt: - raise InterpolationError, 'invalid conversion or specification' \ - ' for option %s (%s (%s))' % (option, rawval, txt) + except KeyError as txt: + raise NoOptionError('section [%s] missing option (%s)' \ + % (section, option)) + except TypeError as txt: + raise InterpolationError('invalid conversion or specification' \ + ' for option %s (%s (%s))' % (option, rawval, txt)) ####################################### def getint (self, section, option): @@ -397,8 +397,8 @@ class ConfParser: try: return int (val) except ValueError: - raise InterpolationError, 'option %s not an integer (%s)' \ - % (option, val) + raise InterpolationError('option %s not an integer (%s)' \ + % (option, val)) ####################################### def getfloat (self, section, option): @@ -409,8 +409,8 @@ class ConfParser: try: return float (val) except ValueError: - raise InterpolationError, 'option %s not a float (%s)' \ - % (option, val) + raise InterpolationError('option %s not a float (%s)' \ + % (option, val)) ####################################### def getboolean (self, section, option): @@ -434,11 +434,11 @@ class ConfParser: options.sort () for option in options: values = self.get (section, option) - if type (values) == ListType: + if type (values) == list: sys.stderr.write (' %s:\n' % option) for value in values: sys.stderr.write (' %s\n' % value) else: sys.stderr.write (' %s: %s\n' % (option, values)) sys.stderr.write ('\n') - \ No newline at end of file + diff --git pymsgauth-filter pymsgauth-filter new file mode 100755 index 0000000..9b90b07 --- /dev/null +++ pymsgauth-filter @@ -0,0 +1,9 @@ +#!/usr/bin/python + +from pymsgauth import * + +import io +import sys + +msg = tokenize_message_if_needed (io.StringIO (u'' + sys.stdin.read ())) +sys.stdout.write (msg) diff --git pymsgauth.py pymsgauth.py index 941bc09..d8b4162 100755 --- pymsgauth.py +++ pymsgauth.py @@ -19,7 +19,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ''' -__version__ = '2.1.0' +__version__ = '2.1.0-filter3-20180804' __author__ = 'Charles Cazabon ' @@ -30,12 +30,16 @@ __author__ = 'Charles Cazabon ' import sys import os import string -import rfc822 -import cStringIO +import email +import io import time import types import ConfParser +if sys.version_info[0] < 3: + import codecs + sys.stdin = codecs.getreader('utf-8')(sys.stdin) + # # Configuration constants # @@ -49,7 +53,7 @@ loglevels = { 'ERROR' : 5, 'FATAL' : 6, } -(TRACE, DEBUG, INFO, WARN, ERROR, FATAL) = range (1, 7) +(TRACE, DEBUG, INFO, WARN, ERROR, FATAL) = list(range(1, 7)) # Build-in default values defaults = { @@ -116,7 +120,7 @@ FILENAME, LINENO, FUNCNAME = 0, 1, 2 #SOURCELINE = 3 ; not used logfd = None ############################# -class pymsgauthError (StandardError): +class pymsgauthError (Exception): pass ############################# @@ -127,6 +131,52 @@ class DeliveryError (pymsgauthError): class ConfigurationError (pymsgauthError): pass +############################# +class RFC822Message: + def __init__(self, buf, seekable=1): + self.message = email.message_from_file(buf) + self.headers = self.init_headers() + self.fp = self.init_fp(buf) + + def init_headers(self): + headers = [] + for field, value in self.message.items(): + headers.extend(field + ': ' + value + '\n') + return headers + + def init_fp(self, buf): + if buf != sys.stdin: + buf.seek(0) + while 1: + line = buf.readline() + if line == '\n' or line == '': + break + return buf + + def getaddr(self, field): + value = self.message.get(field) + if value == None: + name = None + addr = None + else: + name, addr = email.utils.parseaddr(value) + return name, addr + + def getheader(self, field, default): + return self.message.get(field, '') + + def getaddrlist(self, field): + addrlist = [] + values = self.message.get_all(field) + if values: + for value in values: + name_addr = email.utils.parseaddr(value) + addrlist.append(name_addr) + return addrlist + + def rewindbody(self): + self.init_fp(self.fp) + ####################################### def log (level=INFO, msg=''): global logfd @@ -147,9 +197,9 @@ def log (level=INFO, msg=''): if not logfd: try: logfd = open (os.path.expanduser (config['log_file']), 'a') - except IOError, txt: - raise ConfigurationError, 'failed to open log file %s (%s)' \ - % (config['log_file'], txt) + except IOError as txt: + raise ConfigurationError('failed to open log file %s (%s)' \ + % (config['log_file'], txt)) t = time.localtime (time.time ()) logfd.write ('%s %s' % (time.strftime ('%d %b %Y %H:%M:%S', t), s)) logfd.flush () @@ -182,17 +232,17 @@ def read_config (): try: value = loglevels[value] except KeyError: - raise ConfigurationError, \ - '"%s" not a valid logging level' % value + raise ConfigurationError('"%s" not a valid logging level' % value) config[option] = value if option == 'secret': log (TRACE, 'option secret == %s...' % value[:20]) else: log (TRACE, 'option %s == %s...' % (option, config[option])) - except (ConfigurationError, ConfParser.ConfParserException), txt: - log (FATAL, 'Fatal: exception reading %s (%s)' % (config_file, txt)) - raise - if type (config['token_recipient']) != types.ListType: + except (ConfigurationError, ConfParser.ConfParserException) as txt: + if not os.environ.get ('PYMSGAUTH_TOLERATE_UNCONFIGURED'): + log (FATAL, 'Fatal: exception reading %s (%s)' % (config_file, txt)) + raise + if type (config['token_recipient']) != list: config['token_recipient'] = [config['token_recipient']] log (TRACE) @@ -214,27 +264,26 @@ def extract_original_message (msg): del lines[0] # Strip blank line(s) - while lines and string.strip (lines[0]) == '': + while lines and lines[0].strip () == '': del lines[0] - buf = cStringIO.StringIO (string.join (lines, '')) + buf = io.StringIO (''.join (lines)) buf.seek (0) - orig_msg = rfc822.Message (buf) + orig_msg = RFC822Message (buf) return orig_msg ############################# def gen_token (msg): - import sha + import hashlib lines = [] - token = sha.new('%s,%s,%s,%s' - % (os.getpid(), time.time(), string.join (msg.headers), - config['secret'])).hexdigest() + contents = '%s,%s,%s,%s' % (os.getpid(), time.time(), ''.join (msg.headers), config['secret']) + token = hashlib.sha1(contents.encode('utf-8')).hexdigest() # Record token p = os.path.join (config['pymsgauth_dir'], '.%s' % token) try: open (p, 'wb') log (TRACE, 'Recorded token %s.' % p) - except IOError, txt: + except IOError as txt: log (FATAL, 'Fatal: exception creating %s (%s)' % (p, txt)) raise return token @@ -252,7 +301,7 @@ def check_token (msg, token): log (INFO, 'Matched token %s, removing.' % token) os.unlink (p) log (TRACE, 'Removed token %s.' % token) - except OSError, txt: + except OSError as txt: log (FATAL, 'Fatal: error handling token %s (%s)' % (token, txt)) log_exception () # Exit 0 so qmail delivers the qsecretary notice to user @@ -261,19 +310,19 @@ def check_token (msg, token): ############################# def send_mail (msgbuf, mailcmd): - import popen2 - popen2._cleanup() + import subprocess log (TRACE, 'Mail command is "%s".' % mailcmd) - cmd = popen2.Popen3 (mailcmd, 1, bufsize=-1) - cmdout, cmdin, cmderr = cmd.fromchild, cmd.tochild, cmd.childerr + cmd = subprocess.Popen (mailcmd, shell=True, bufsize=-1, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) + cmdout, cmdin, cmderr = cmd.stdout, cmd.stdin, cmd.stderr + cmdin.write (msgbuf) cmdin.flush () cmdin.close () log (TRACE) - err = string.strip (cmderr.read ()) + err = cmderr.read ().strip () cmderr.close () - out = string.strip (cmdout.read ()) + out = cmdout.read ().strip () cmdout.close () r = cmd.wait () @@ -292,18 +341,18 @@ def send_mail (msgbuf, mailcmd): errtext = ', err: "%s"' % err else: errtext = '' - raise DeliveryError, 'mail command %s exited %s, %s%s' \ - % (mailcmd, exitcode, exitsignal, errtext) + raise DeliveryError('mail command %s exited %s, %s%s' \ + % (mailcmd, exitcode, exitsignal, errtext)) else: exitcode = 127 - raise DeliveryError, 'mail command %s did not exit (rc == %s)' \ - % (mailcmd, r) + raise DeliveryError('mail command %s did not exit (rc == %s)' \ + % (mailcmd, r)) if err: - raise DeliveryError, 'mail command %s error: "%s"' % (mailcmd, err) + raise DeliveryError('mail command %s error: "%s"' % (mailcmd, err)) exitcode = 1 - except DeliveryError, txt: + except DeliveryError as txt: log (FATAL, 'Fatal: failed sending mail (%s)' % txt) log_exception () sys.exit (exitcode or 1) @@ -336,12 +385,12 @@ def clean_old_tokens (): if s[stat.ST_CTIME] < oldest: log (INFO, 'Removing old token %s.' % filename) os.unlink (p) - except OSError, txt: + except OSError as txt: log (ERROR, 'Error: error handling token %s (%s)' % (filename, txt)) raise - except StandardError, txt: + except Exception as txt: log (FATAL, 'Fatal: caught exception (%s)' % txt) log_exception () sys.exit (1) @@ -361,10 +410,25 @@ def sendmail_wrapper (args): mailcmd += config['extra_mail_args'] mailcmd += args log (TRACE, 'mailcmd == %s' % mailcmd) - buf = cStringIO.StringIO (sys.stdin.read()) - msg = rfc822.Message (buf, seekable=1) + buf = io.StringIO (u'' + sys.stdin.read()) + new_buf = tokenize_message_if_needed (buf, args) + send_mail (new_buf, mailcmd) + if (new_buf != buf.getvalue ()): + log (TRACE, 'Sent tokenized mail.') + else: + log (TRACE, 'Passed mail through unchanged.') + + except Exception as txt: + log (FATAL, 'Fatal: caught exception (%s)' % txt) + log_exception () + sys.exit (1) + +############################# +def should_tokenize_message (msg, *args): + try: sign_message = 0 + for arg in args: if arg in config['token_recipient']: sign_message = 1 @@ -373,22 +437,34 @@ def sendmail_wrapper (args): recips = [] for field in ('to', 'cc', 'bcc', 'resent-to', 'resent-cc', 'resent-bcc'): recips.extend (msg.getaddrlist (field)) - recips = map (lambda (name, addr): addr, recips) + recips = [name_addr[1] for name_addr in recips] for recip in recips: if recip in config['token_recipient']: sign_message = 1 break - if sign_message: + + return sign_message + + except Exception as txt: + log (FATAL, 'Fatal: caught exception (%s)' % txt) + log_exception () + sys.exit (1) + +############################# +def tokenize_message_if_needed (buf, *args): + try: + read_config () + log (TRACE) + msg = RFC822Message (buf, seekable=1) + + if should_tokenize_message (msg, args): token = gen_token (msg) log (INFO, 'Generated token %s.' % token) - new_buf = '%s: %s\n' % (config['auth_field'], token) + buf.getvalue () - send_mail (new_buf, mailcmd) - log (TRACE, 'Sent tokenized mail.') + return '%s: %s\n' % (config['auth_field'], token) + buf.getvalue () else: - send_mail (buf.getvalue (), mailcmd) - log (TRACE, 'Passed mail through unchanged.') + return buf.getvalue () - except StandardError, txt: + except Exception as txt: log (FATAL, 'Fatal: caught exception (%s)' % txt) log_exception () sys.exit (1) @@ -398,8 +474,8 @@ def process_qsecretary_message (): try: read_config () log (TRACE) - buf = cStringIO.StringIO (sys.stdin.read()) - msg = rfc822.Message (buf, seekable=1) + buf = io.StringIO (u'' + sys.stdin.read()) + msg = RFC822Message (buf, seekable=1) from_name, from_addr = msg.getaddr ('from') if from_name != 'The qsecretary program': # Not a confirmation message, just quit @@ -409,9 +485,9 @@ def process_qsecretary_message (): # Verify the message came from a domain we recognize confirm = 0 - domain = string.split (from_addr, '@')[-1] + domain = from_addr.split ('@')[-1] cdomains = config['confirm_domain'] - if type (cdomains) != types.ListType: cdomains = [cdomains] + if type (cdomains) != list: cdomains = [cdomains] for cd in cdomains: if cd == domain: confirm = 1 if not confirm: @@ -422,7 +498,8 @@ def process_qsecretary_message (): # check message here orig_msg = extract_original_message (msg) - orig_token = string.strip (orig_msg.getheader (config['auth_field'], '')) + orig_header = orig_msg.getheader (config['auth_field'], '') + orig_token = orig_header.strip () if orig_token: log (TRACE, 'Received qsecretary notice with token %s.' % orig_token) @@ -435,7 +512,7 @@ def process_qsecretary_message (): try: source_addr = config['confirmation_address'] except: - raise ConfigurationError, 'no confirmation_address configured' + raise ConfigurationError('no confirmation_address configured') # Confirm this confirmation notice #confirm_cmd = config['mail_prog'] \ # + ' ' + '-f "%s" "%s"' % (source_addr, from_addr) @@ -446,14 +523,14 @@ def process_qsecretary_message (): log (INFO, 'Authenticated qsecretary notice, from "%s", token "%s"' % (from_addr, orig_token)) sys.exit (99) - except ConfigurationError, txt: + except ConfigurationError as txt: log (ERROR, 'Error: failed sending confirmation notice (%s)' % txt) else: log (ERROR, 'Error: did not find matching token file (%s)' % orig_token) - except StandardError, txt: + except Exception as txt: log (FATAL, 'Fatal: caught exception (%s)' % txt) log_exception ()