#!/usr/bin/env /usr/local/bin/python2.3 # # Copyright (c) 2004-2005, Sean Reifschneider, tummy.com, ltd. # All Rights Reserved. S_rcsid = '$Id: tumgreyspfsupp.py,v 1.7 2006/05/30 10:24:11 jafo Exp $' import syslog, os, sys, string, re, time, popen2, urllib, stat # default values defaultConfigFilename = '/usr/local/etc/tumgreyspf.conf' defaultConfigData = { 'debugLevel' : 0, 'defaultSeedOnly' : 0, 'defaultAllowTime' : 600, 'configPath' : 'file:///var/db/tumgreyspf/config', 'greylistDir' : '/var/db/tumgreyspf/data', 'blackholeDir' : '/var/db/tumgreyspf/blackhole', 'spfqueryPath' : '/usr/local/bin/spfquery', 'ignoreLastByte' : 0, } ################################# class ConfigException(Exception): '''Exception raised when there's a configuration file error.''' pass ################################# def loadConfigFile(file, values): '''Load the specified config file if it exists, raise ValueError if there is an error in the config file. "values" is a dictionary of default config values. "values" is modified in place, and nothing is returned.''' if not os.path.exists(file): return try: execfile(file, {}, values) except Exception, e: import traceback etype, value, tb = sys.exc_info() raise ConfigException, ('Error reading config file "%s": %s' % ( file, sys.exc_info()[1] )) return() #################################################################### def processConfigFile(filename = None, config = None, useSyslog = 1, useStderr = 0): '''Load the specified config file, exit and log errors if it fails, otherwise return a config dictionary.''' import tumgreyspfsupp if config == None: config = tumgreyspfsupp.defaultConfigData if filename == None: filename = tumgreyspfsupp.defaultConfigFilename try: loadConfigFile(filename, config) except Exception, e: if useSyslog: syslog.syslog(e.args[0]) if useStderr: sys.stderr.write('%s\n' % e.args[0]) sys.exit(1) return(config) ################# class ExceptHook: def __init__(self, useSyslog = 1, useStderr = 0): self.useSyslog = useSyslog self.useStderr = useStderr def __call__(self, etype, evalue, etb): import traceback, string tb = traceback.format_exception(*(etype, evalue, etb)) tb = map(string.rstrip, tb) tb = string.join(tb, '\n') for line in string.split(tb, '\n'): if self.useSyslog: syslog.syslog(line) if self.useStderr: sys.stderr.write(line + '\n') #################### def setExceptHook(): sys.excepthook = ExceptHook(useSyslog = 1, useStderr = 1) #################### def quoteAddress(s): '''Quote an address so that it's safe to store in the file-system. Address can either be a domain name, or local part. Returns the quoted address.''' s = urllib.quote(s, '@_-+') if len(s) > 0 and s[0] == '.': s = '%2e' + s[1:] return(s) ###################### def unquoteAddress(s): '''Undo the quoting of an address. Returns the unquoted address.''' return(urllib.unquote(s)) ############################################################### commentRx = re.compile(r'^(.*)#.*$') def readConfigFile(path, configData = None, configGlobal = {}): '''Reads a configuration file from the specified path, merging it with the configuration data specified in configData. Returns a dictionary of name/value pairs based on configData and the values read from path.''' debugLevel = configGlobal.get('debugLevel', 0) if debugLevel >= 3: syslog.syslog('readConfigFile: Loading "%s"' % path) if configData == None: configData = {} nameConversion = { 'SPFSEEDONLY' : int, 'GREYLISTTIME' : int, 'CHECKERS' : str, 'OTHERCONFIGS' : str, 'GREYLISTEXPIREDAYS' : float, } # check to see if it's a file try: mode = os.stat(path)[0] except OSError, e: syslog.syslog('ERROR stating "%s": %s' % ( path, e.strerror )) return(configData) if not stat.S_ISREG(mode): syslog.syslog('ERROR: is not a file: "%s", mode=%s' % ( path, oct(mode) )) return(configData) # load file fp = open(path, 'r') while 1: line = fp.readline() if not line: break # parse line line = string.strip(string.split(line, '#', 1)[0]) if not line: continue data = map(string.strip, string.split(line, '=', 1)) if len(data) != 2: syslog.syslog('ERROR parsing line "%s" from file "%s"' % ( line, path )) continue name, value = data # check validity of name conversion = nameConversion.get(name) if conversion == None: syslog.syslog('ERROR: Unknown name "%s" in file "%s"' % ( name, path )) continue if debugLevel >= 4: syslog.syslog('readConfigFile: Found entry "%s=%s"' % ( name, value )) configData[name] = conversion(value) fp.close() return(configData) #################################################### def lookupConfig(configPath, msgData, configGlobal): '''Given a path, load the configuration as dictated by the msgData information. Returns a dictionary of name/value pairs.''' debugLevel = configGlobal.get('debugLevel', 0) # set up default config configData = { 'SPFSEEDONLY' : configGlobal.get('defaultSeedOnly'), 'GREYLISTTIME' : configGlobal.get('defaultAllowTime'), 'CHECKGREYLIST' : 1, 'CHECKSPF' : 1, 'OTHERCONFIGS' : 'envelope_sender,envelope_recipient', } # load directory-based config information if configPath[:8] == 'file:///': if debugLevel >= 3: syslog.syslog('lookupConfig: Starting file lookup from "%s"' % configPath) basePath = configPath[7:] configData = {} # load default config path = os.path.join(basePath, '__default__') if os.path.exists(path): if debugLevel >= 3: syslog.syslog('lookupConfig: Loading default config: "%s"' % path) configData = readConfigFile(path, configData, configGlobal) else: syslog.syslog(('lookupConfig: No default config found in "%s", ' 'this is probably an install problem.') % path) # load other configs from OTHERCONFIGS configsAlreadyLoaded = {} didLoad = 1 while didLoad: didLoad = 0 otherConfigs = string.split(configData.get('OTHERCONFIGS', ''), ',') if not otherConfigs or otherConfigs == ['']: break if debugLevel >= 3: syslog.syslog('lookupConfig: Starting load of configs: "%s"' % str(otherConfigs)) # SENDER/RECIPIENT for cfgType in otherConfigs: cfgType = string.strip(cfgType) # skip if already loaded if configsAlreadyLoaded.get(cfgType) != None: continue configsAlreadyLoaded[cfgType] = 1 didLoad = 1 if debugLevel >= 3: syslog.syslog('lookupConfig: Trying config "%s"' % cfgType) # SENDER/RECIPIENT if cfgType == 'envelope_sender' or cfgType == 'envelope_recipient': # get address if cfgType == 'envelope_sender': address = msgData.get('sender') else: address = msgData.get('recipient') if not address: if debugLevel >= 2: syslog.syslog('lookupConfig: Could not find %s' % cfgType) continue # split address into domain and local data = string.split(address, '@', 1) if len(data) != 2: if debugLevel >= 2: syslog.syslog('lookupConfig: Could not find %s address ' 'from "%s", skipping' % ( cfgType, address )) continue local = quoteAddress(data[0]) domain = quoteAddress(data[1]) # load configs path = os.path.join(basePath, cfgType) domainPath = os.path.join(path, domain, '__default__') localPath = os.path.join(path, domain, local) for name in ( domainPath, localPath ): if debugLevel >= 3: syslog.syslog('lookupConfig: Trying file "%s"' % name) if os.path.exists(name): configData = readConfigFile(name, configData, configGlobal) # CLIENT IP ADDRESS elif cfgType == 'client_address': ip = msgData.get('client_address') if not ip: if debugLevel >= 2: syslog.syslog('lookupConfig: Could not find client ' 'address') else: path = basePath for name in [ 'client_address' ] \ + list(string.split(ip, '.')): path = os.path.join(path, name) defaultPath = os.path.join(path, '__default__') if debugLevel >= 3: syslog.syslog('lookupConfig: Trying file "%s"' % defaultPath) if os.path.exists(defaultPath): configData = readConfigFile(defaultPath, configData, configGlobal) if debugLevel >= 3: syslog.syslog('lookupConfig: Trying file "%s"' % path) if os.path.exists(path): configData = readConfigFile(path, configData, configGlobal) # unknown configuration type else: syslog.syslog('ERROR: Unknown configuration type: "%s"' % cfgType) # unkonwn config path else: syslog.syslog('ERROR: Unknown path type in: "%s", using defaults' % msgData) # return results return(configData)