#!/usr/bin/env python # vim: ts=3 sw=3 ai # # Log information about incoming SMTP connections. # # Copyright (c) 2004-2005, Sean Reifschneider, tummy.com, ltd. # All Rights Reserved # S_rcsid = '$Id: tumgreyspf,v 1.19 2006/11/29 04:20:36 jafo Exp $' import syslog, os, sys, string, re, time, popen2, urllib, stat, errno import tumgreyspfsupp syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, syslog.LOG_MAIL) tumgreyspfsupp.setExceptHook() ############################################# def spfcheck(data, configData, configGlobal): #{{{1 debugLevel = configGlobal.get('debugLevel', 0) ip = data.get('client_address') if ip == None: if debugLevel: syslog.syslog('spfcheck: No client address, exiting') return(( None, None )) if ip[:4] == '127.': return (( None, 'SPF check N/A for local connections' )) sender = data.get('sender') if not sender: if debugLevel: syslog.syslog('spfcheck: No sender, exiting') return(( None, None )) # if no helo name sent, use domain from sender helo = data.get('helo_name') if not helo: foo = string.split(sender, '@', 1) if len(foo) < 2: helo = 'unknown' else: helo = foo[1] # start query spfResult = None spfReason = None # try to use pyspf try: import spf ret = spf.check(i = ip, s = sender, h = helo) spfResult = string.strip(ret[0]) spfReason = string.strip(ret[2]) if debugLevel: syslog.syslog('spfcheck: pyspf result: "%s"' % str(ret)) except ImportError: pass # try spfquery if not spfResult: # check for spfquery spfqueryPath = configGlobal['spfqueryPath'] if not os.path.exists(spfqueryPath): if debugLevel: syslog.syslog('spfcheck: No spfquery at "%s", exiting' % spfqueryPath) return(( None, None )) # open connection to spfquery fpIn, fpOut = popen2.popen2('%s -file -' % spfqueryPath) fpOut.write('%s %s %s\n' % ( ip, sender, helo )) fpOut.close() spfData = fpIn.readlines() fpIn.close() if debugLevel: syslog.syslog('spfcheck: spfquery result: "%s"' % str(spfData)) spfResult = string.strip(spfData[0]) spfReason = string.strip(spfData[1]) # read result if spfResult == 'fail' or spfResult == 'deny': syslog.syslog('SPF fail: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s" REASON="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), spfReason ) ) return(( 'reject', 'SPF Reports: %s' % str(spfReason) )) return(( None, None )) ################################################## def greylistcheck(data, configData, configGlobal): #{{{1 greylistDir = configGlobal['greylistDir'] ip = data.get('client_address') if ip == None: return(( None, None )) ipBytes = string.split(ip, '.') if configGlobal['ignoreLastByte'] > 0: ipBytes = ipBytes[:-1] ipPath = string.join(ipBytes, '/') sender = data.get('sender') recipient = data.get('recipient') if not sender or not recipient: return(( None, None )) sender = tumgreyspfsupp.quoteAddress(sender) recipient = tumgreyspfsupp.quoteAddress(recipient) allowTime = configData.get('GREYLISTTIME', 600) dir = os.path.join(greylistDir, 'client_address', ipPath, 'greylist', sender) path = os.path.join(dir, recipient) if not os.path.exists(path): if not os.path.exists(dir): # if multiple messages come in at once # it can cause multiple makedirs for i in xrange(10): try: os.makedirs(dir) break except OSError, msg: if msg.errno != errno.EEXIST: raise # still didn't succeed if not os.path.exists(dir): syslog.syslog(('ERROR: Could not create directory after ' '10 seconds: "%s"') % dir) return(( 'defer', 'Service unavailable, error creating data ' 'directory. See /var/log/maillog for more information.' )) # create file open(path, 'w').close() now = time.time() mtime = now + allowTime os.utime(path, ( now, mtime )) if configGlobal.get('defaultSeedOnly'): syslog.syslog( 'Training greylisting: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), ) ) return(( None, None )) syslog.syslog('Initial greylisting: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), ) ) return(( 'defer', 'Service unavailable, greylisted ' '(http://projects.puremagic.com/greylisting/).' )) # is it time to allow yet mtime = os.stat(path)[8] now = time.time() os.utime(path, ( now, mtime )) if mtime > now and not configGlobal.get('defaultSeedOnly'): syslog.syslog('Pending greylisting: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), ) ) return(( 'defer', 'Service unavailable, greylisted.' )) syslog.syslog('Allowed greylisting: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), ) ) return(( None, None )) ################################################### def blackholecheck(data, configData, configGlobal): #{{{1 blackholeDir = configGlobal['blackholeDir'] ip = data.get('client_address') if ip == None: return(( None, None )) ipPath = string.join(string.split(ip, '.'), '/') dir = os.path.join(blackholeDir, 'ips', ipPath) recipient = data.get('recipient') if not recipient: return(( None, None )) recipient = tumgreyspfsupp.quoteAddress(recipient) # add blackhole recipientPath = os.path.join(blackholeDir, 'addresses', recipient) if os.path.exists(recipientPath): if not os.path.exists(dir): os.path.makedirs(dir) # check for existing blackhole entry if os.path.exists(dir): syslog.syslog('Blackholed: REMOTEIP="%s" HELO="%s" SENDER="%s" ' 'RECIPIENT="%s" QUEUEID="%s"' % ( data.get('client_address', ''), data.get('helo_name', ''), data.get('sender', ''), data.get('recipient', ''), data.get('queue_id', ''), ) ) return(( 'reject', 'Service unavailable, blackholed.' )) return(( None, None )) ################### # load config file {{{1 configFile = tumgreyspfsupp.defaultConfigFilename if len(sys.argv) > 1: if sys.argv[1] in ( '-?', '--help', '-h' ): print 'usage: tumgreyspf []' sys.exit(1) configFile = sys.argv[1] configGlobal = tumgreyspfsupp.processConfigFile(filename = configFile) # loop reading data {{{1 debugLevel = configGlobal.get('debugLevel', 0) if debugLevel >= 2: syslog.syslog('Starting') data = {} lineRx = re.compile(r'^\s*([^=\s]+)\s*=(.*)$') while 1: line = sys.stdin.readline() if not line: break line = string.rstrip(line) if debugLevel >= 4: syslog.syslog('Read line: "%s"' % line) # end of entry {{{2 if not line: if debugLevel >= 4: syslog.syslog('Found the end of entry') configData = tumgreyspfsupp.lookupConfig(configGlobal.get('configPath'), data, configGlobal) if debugLevel >= 2: syslog.syslog('Config: %s' % str(configData)) # run the checkers {{{3 checkerValue = None checkerReason = None for checkerType in string.split(configData.get('CHECKERS', ''), ','): checkerType = string.strip(checkerType) if checkerType == 'greylist': checkerValue, checkerReason = greylistcheck(data, configData, configGlobal) if checkerValue != None: break elif checkerType == 'spf': checkerValue, checkerReason = spfcheck(data, configData, configGlobal) if configData.get('SPFSEEDONLY', 0): checkerValue = None checkerReason = None if checkerValue != None: break elif checkerType == 'blackhole': checkerValue, checkerReason = blackholecheck(data, configData, configGlobal) if checkerValue != None: break # handle results {{{3 if checkerValue == 'defer': sys.stdout.write('action=defer_if_permit %s\n\n' % checkerReason) elif checkerValue == 'reject': sys.stdout.write('action=550 %s\n\n' % checkerReason) else: sys.stdout.write('action=dunno\n\n') # end of record {{{3 sys.stdout.flush() data = {} continue # parse line {{{2 m = lineRx.match(line) if not m: syslog.syslog('ERROR: Could not match line "%s"' % line) continue # save the string {{{2 key = m.group(1) value = m.group(2) if key not in [ 'protocol_state', 'protocol_name', 'queue_id' ]: value = string.lower(value) data[key] = value