1#!/usr/local/bin/python3.8 2# Derived from dkim-milter.py code: 3# Author: Stuart D. Gathman <stuart@bmsi.com> 4# Copyright 2007 Business Management Systems, Inc. 5# This code is under GPL. See COPYING for details. 6# and: 7# dkimpy-milter: A DKIM signing/verification Milter application 8# Author: Scott Kitterman <scott@kitterman.com> 9# Copyright 2018 Scott Kitterman 10# spf-milter.py: 11# Author: Scott Kitterman <scott@kitterman.com> 12# Copyright 2019 Scott Kitterman 13""" This program is free software; you can redistribute it and/or modify 14 it under the terms of the GNU General Public License as published by 15 the Free Software Foundation; either version 2 of the License, or 16 (at your option) any later version. 17 18 This program is distributed in the hope that it will be useful, 19 but WITHOUT ANY WARRANTY; without even the implied warranty of 20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 GNU General Public License for more details. 22 23 You should have received a copy of the GNU General Public License along 24 with this program; if not, write to the Free Software Foundation, Inc., 25 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.""" 26 27import sys 28import syslog 29import Milter 30import authres 31import os 32import tempfile 33import re 34from Milter.utils import parse_addr, parseaddr 35import spf_engine 36import spf_engine.policydspfsupp as config 37from spf_engine.util import drop_privileges 38from spf_engine.policydspfsupp import _setExceptHook 39from spf_engine.util import write_pid 40from spf_engine.util import fold 41 42__version__ = "2.9.2" 43FWS = re.compile(r'\r?\n[ \t]+') 44 45 46class spfMilter(Milter.Base): 47 "Milter to check SPF. Each connection gets its own instance." 48 49 def __init__(self): 50 self.mailfrom = None 51 self.id = Milter.uniqueID() 52 self.instance_dict = {'0':'init',} 53 self.instance_dict.clear() 54 self.data = {} 55 56 @Milter.noreply 57 def connect(self, hostname, unused, hostaddr): 58 self.internal_connection = False 59 self.external_connection = False 60 self.hello_name = None 61 # sometimes people put extra space in sendmail config, so we strip 62 self.receiver = self.getsymval('j').strip() 63 try: 64 self.Authserv_Id = milterconfig['Authserv_Id'] 65 except: 66 self.Authserv_Id = self.receiver 67 if hostaddr and len(hostaddr) > 0: 68 ipaddr = hostaddr[0] 69 if milterconfig['IntHosts']: 70 if milterconfig['IntHosts'].match(ipaddr): 71 self.internal_connection = True 72 else: 73 ipaddr = '' 74 self.connectip = ipaddr 75 self.data['client_address'] = ipaddr 76 self.data['helo_name'] = hostname 77 self.data['recipient'] = '<UNKNOWN>' 78 if milterconfig.get('MacroList') and not self.internal_connection: 79 macrolist = milterconfig.get('MacroList') 80 for macro in macrolist: 81 macroname = macro.split('|')[0] 82 macroname = '{' + macroname + '}' 83 macroresult = self.getsymval(macroname) 84 if ((len(macro.split('|')) == 1 and macroresult) or macroresult 85 in macro.split('|')[1:]): 86 self.external_connection = True 87 if self.internal_connection: 88 connecttype = 'INTERNAL' 89 else: 90 connecttype = 'EXTERNAL' 91 if milterconfig.get('debugLevel') >= 1: 92 syslog.syslog("connect from {0} at {1} {2}" 93 .format(hostname, hostaddr, connecttype)) 94 return Milter.CONTINUE 95 96 # multiple messages can be received on a single connection 97 # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start 98 # of each message. 99 def envfrom(self, f, *str): 100 if milterconfig.get('debugLevel') >= 2: 101 syslog.syslog("mail from: {0} {1}".format(f, str)) 102 self.mailfrom = f 103 t = parse_addr(f) 104 if len(t) == 2: 105 t[1] = t[1].lower() 106 domain = t[1] 107 else: 108 domain = 'localhost.localdomain' 109 self.canon_from = '@'.join(t) 110 self.arheaders = [] 111 self.arresults = [] 112 self.data['sender'] = self.canon_from 113 if (not self.internal_connection or self.external_connection) and self.connectip: 114 return self.check_spf() 115 return Milter.CONTINUE 116 117 @Milter.noreply 118 def header(self, name, val): 119 lname = name.lower() 120 if lname == 'authentication-results': 121 self.arheaders.append(val) 122 return Milter.CONTINUE 123 124 @Milter.noreply 125 def eoh(self): 126 self.bodysize = 0 127 return Milter.CONTINUE 128 129 @Milter.noreply 130 def body(self, chunk): # copy body to temp file 131 return Milter.CONTINUE 132 133 def eom(self): 134 if self.arresults: 135 h = results=self.arresults[0] 136 h = fold(str(h)) 137 if milterconfig.get('debugLevel') >= 2: 138 syslog.syslog(str(h)) 139 name, val = str(h).split(': ', 1) 140 self.addheader(name, val, 0) 141 return Milter.CONTINUE 142 143 144 def check_spf(self): 145 peruser = False 146 perusermilterconfig = [] 147 rejectAction = '' 148 deferAction = '' 149 if not self.data.get('recipient'): 150 self.data['recipient'] = 'none' 151 if milterconfig.get('debugLevel') >= 3: syslog.syslog('Config: %s' % str(milterconfig)) 152 # run the checkers {{{3 153 checkerValue = None 154 checkerReason = None 155 checkerValue, checkerReason, self.instance_dict, iserror = \ 156 spf_engine._spfcheck(self.data, self.instance_dict, milterconfig, 157 peruser, perusermilterconfig) 158 159 TestOnly = milterconfig.get('TestOnly') 160 if TestOnly == 0 and checkerValue != 'prepend': 161 checkerValue = None 162 checkerReason = None 163 164 if milterconfig.get('SPF_Enhanced_Status_Codes') == 'No': 165 rejectAction = '' 166 deferAction = 'defer_if_permit' 167 else: 168 if iserror: 169 rejectAction = '5.7.24' 170 deferAction = '4.7.24' 171 else: 172 rejectAction = '5.7.23' 173 deferAction = '4.7.24' 174 175 # handle results {{{3 176 if milterconfig.get('Header_Type') != 'None' and checkerValue != 'reject': 177 if milterconfig.get('debugLevel') >= 3: syslog.syslog('{0} {1}'.format('Header text', checkerReason)) 178 self.arresults.append(checkerReason) 179 if milterconfig.get('debugLevel') >= 3: syslog.syslog('Action: {0}: Text: {1} Reject action: {2}'.format(checkerValue, checkerReason, rejectAction)) 180 181 if checkerValue == 'reject': 182 if milterconfig.get('debugLevel') >= 1: syslog.syslog('{0} {1}'.format(rejectAction, checkerReason)) 183 self.setreply(str(550), rejectAction, checkerReason) 184 return Milter.REJECT 185 186 elif checkerValue == 'prepend': 187 if milterconfig.get('Prospective'): 188 return Milter.CONTINUE 189 else: 190 if milterconfig.get('Header_Type') != 'None': 191 if milterconfig.get('debugLevel') >= 1: syslog.syslog('prepend {0}'.format(checkerReason)) 192 else: 193 if milterconfig.get('debugLevel') >= 1: syslog.syslog('Header field not prepended: {0}'.format(checkerReason)) 194 return Milter.CONTINUE 195 196 elif checkerValue == 'defer': 197 if milterconfig.get('debugLevel') >= 1: syslog.syslog('{0} {1}'.format(deferAction, checkerReason)) 198 self.setreply(str(450), deferAction, checkerReason) 199 return Milter.TEMPFAIL 200 201 elif checkerValue == 'result_only': 202 return Milter.SKIP 203 else: 204 return Milter.CONTINUE 205 206 207def main(): 208 # Ugh, but there's no easy way around this. 209 global milterconfig 210 configFile = '/usr/local/etc/python-policyd-spf/policyd-spf.conf' 211 if len(sys.argv) > 1: 212 if sys.argv[1] in ('-?', '--help', '-h'): 213 print('usage: pyspf-milter [<configfilename>]') 214 sys.exit(1) 215 configFile = sys.argv[1] 216 milterconfig = config._processConfigFile(filename=configFile) 217 # Unlike policyd, milter interface only suppports authres 218 if milterconfig.get('Header_Type') != 'None': 219 milterconfig['Header_Type'] = 'AR' 220 # Per user configurations not supported on milter interface 221 milterconfig['Per_User'] = False 222 facility = eval("syslog.LOG_{0}".format('MAIL')) 223 syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, facility) 224 _setExceptHook() 225 pid = write_pid(milterconfig) 226 Milter.factory = spfMilter 227 Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS) 228 miltername = 'pyspf-filter' 229 socketname = milterconfig.get('Socket') 230 syslog.syslog('pyspf-milter started:{0} user:{1}' 231 .format(pid, milterconfig.get('UserID'))) 232 sys.stdout.flush() 233 drop_privileges(milterconfig) 234 Milter.runmilter(miltername, socketname, 240) 235 236if __name__ == "__main__": 237 main() 238