#!/usr/bin/python3 # -*- coding: utf-8 -*- # # Check SPF results and provide recommended action back to Postfix. # # Tumgreyspf source # Copyright © 2004-2005, Sean Reifschneider, tummy.com, ltd. # # # pypolicyd-spf # Copyright © 2007-16, Scott Kitterman ''' Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' def main(): __version__ = "2.9.3" import syslog import os import socket import sys import re import spf_engine import spf_engine.policydspfsupp as policydspfsupp # import policydspfuser - Only imported if per-user settings are activated for # efficiency. if int(sys.version_info.major) < 3 or (int(sys.version_info.major) == 3 and \ int(sys.version_info.minor) < 3): raise ImportError("Python 3.3 or later is required") syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, syslog.LOG_MAIL) policydspfsupp._setExceptHook() # load config file {{{1 # Default location: configFile = '/etc/python-policyd-spf/policyd-spf.conf' if len(sys.argv) > 1: if sys.argv[1] in ( '-?', '--help', '-h' ): print('usage: policyd-spf []') sys.exit(1) configFile = sys.argv[1] configGlobal = policydspfsupp._processConfigFile(filename = configFile) # loop reading data {{{1 debugLevel = configGlobal.get('debugLevel', 1) if debugLevel >= 3: syslog.syslog('Starting') instance_dict = {'0':'init',} instance_dict.clear() data = {} lineRx = re.compile(r'^\s*([^=\s]+)\s*=(.*)$') while 1: # Python readline assumes ascii here, but sometimes it's not lineraw = sys.stdin.buffer.readline() line = lineraw.decode('UTF-8',errors='replace') if not line: break line = line.rstrip() if debugLevel >= 4: syslog.syslog('Read line: "%s"' % line) # end of entry {{{2 if not line: peruser = False peruserconfigData = [] rejectAction = '' deferAction = '' if debugLevel >= 4: syslog.syslog('Found the end of entry') configData = dict(list(configGlobal.items())) if configData.get('defaultSeedOnly') is not None: syslog.syslog(syslog.LOG_ERR,'WARNING: Deprecated Config Option defaultSeedOnly in use in: {0}'.format(configFile )) if configData.get('Mock'): sys.stdout.write('action=dunno mock header field that should be ignored\n\n') continue if not data.get('recipient'): data['recipient'] = 'none' if configData.get('Per_User'): import spf_engine.policydspfuser peruserconfigData, peruser = spf_engine.policydspfuser._datacheck(configData, data.get('recipient')) if debugLevel >= 2 and peruser: syslog.syslog('Per user configuration data in use for: %s' \ % str(data.get('recipient'))) if configData.get('Hide_Receiver') != 'No': data['recipient'] = '' if debugLevel >= 3: syslog.syslog('Config: %s' % str(configData)) # run the checkers {{{3 checkerValue = None checkerReason = None checkerValue, checkerReason, instance_dict, iserror = \ spf_engine._spfcheck(data, instance_dict, configData, peruser, peruserconfigData) if not peruser: TestOnly = configData.get('defaultSeedOnly') TestOnly = configData.get('TestOnly') else: TestOnly = peruserconfigData.get('defaultSeedOnly') TestOnly = peruserconfigData.get('TestOnly') if TestOnly == 0 and checkerValue != 'prepend': checkerValue = None checkerReason = None if configData.get('SPF_Enhanced_Status_Codes') == 'No': rejectAction = '550' deferAction = 'defer_if_permit' else: if iserror: rejectAction = '550 5.7.24' deferAction = 'defer_if_permit 4.7.24' else: rejectAction = '550 5.7.23' deferAction = 'defer_if_permit 4.7.24' # handle results {{{3 if debugLevel >= 3: syslog.syslog('Action: {0}: Text: {1} Reject action: {2}'.format(checkerValue, checkerReason, rejectAction)) if checkerValue == 'reject': if debugLevel >= 1: syslog.syslog('{0} {1}'.format(rejectAction, checkerReason)) sys.stdout.write('action={0} {1}\n\n'.format(rejectAction, checkerReason)) elif checkerValue == 'prepend': if configData.get('Prospective'): sys.stdout.write('action=dunno\n\n') else: if configData.get('Header_Type') != 'None': if debugLevel >= 1: syslog.syslog('prepend {0}'.format(checkerReason)) try: sys.stdout.write('action=prepend %s\n\n' % checkerReason) except UnicodeEncodeError: sys.stdout.write('action=prepend %s\n\n' % str(checkerReason.encode("UTF-8"))[1:].strip("'")) else: if debugLevel >= 1: syslog.syslog('Header field not prepended: {0}'.format(checkerReason)) sys.stdout.write('action=dunno\n\n') elif checkerValue == 'defer': if debugLevel >= 1: syslog.syslog('{0} {1}'.format(deferAction, checkerReason)) try: sys.stdout.write('action={0} {1}\n\n'.format(deferAction, checkerReason)) except UnicodeEncodeError: sys.stdout.write('action={0} {1}\n\n'.format(deferAction, str(checkerReason.encode("UTF-8"))[1:].strip("'"))) elif checkerValue == 'warn': try: sys.stdout.write('action=warn %s\n\n' % checkerReason) except UnicodeEncodeError: sys.stdout.write('action=warn %s\n\n' % str(checkerReason.encode("UTF-8"))[1:].strip("'")) elif checkerValue == 'result_only': if debugLevel >= 1: syslog.syslog('result_only {0}'.format(checkerReason)) try: sys.stdout.write('action=%s\n\n' % checkerReason) except UnicodeEncodeError: sys.stdout.write('action=%s\n\n' % str(checkerReason.encode("UTF-8"))[1:].strip("'")) 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: if debugLevel >= 0: 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 = value.lower() data[key] = value if debugLevel >= 3: syslog.syslog('Normal exit')